在了解了 C++ 堆、栈运行方式后,我们可以开始一个专题:研究对象(特别是嵌套对象)的创建与销毁,这是一个表面上看很普通但要搞清楚全部细节却又不简单的话题,本文就带大家细致地剖析一下 C++ 对象创建与销毁的全过程。阅读本文需要储备一定的堆栈知识,可先阅读《编程底层概念回顾:虚拟内存、栈、栈帧、堆》一文。
从大的视角上看,对象既可以创建在堆上,又可以创建在栈上,我们想把这两种情形分开讨论,然后然对比一下它们之间的异同。我们先概括地看一下对象在堆和栈上创建、销毁和拷贝的整体描述:
➢ 创建对象
堆上
- 使用 new 操作符会在堆上开辟存储空间,然后调用对象的构造函数创建对象,并返回对象在内存中的地址——指针
- 特别地,若对象含有对象成员(嵌套对象),则分配给对象的堆空间必定也包含了属于对象成员的部分(sizeof()计算对象空间时会把所有成员(包括嵌套对象)的大小加在一起)
栈上
- 使用“直接初始化”语法会在栈上开辟存储空间,然后调用对象的构造函数创建对象,直接得到对象本身——实例
- 特别地,若对象含有对象成员(嵌套对象),则分配给对象的栈空间必定也包含了属于对象成员的部分(sizeof()计算对象空间时会把所有成员(包括嵌套对象)的大小加在一起)
➢ 销毁对象
堆上
在堆上的对象(使用 new 创建的对象)必须显式地使用 delete 操作符销毁对象,否则对象不会被自动清理,进而造成内存泄漏,delete 操作会先调用对象的析构函数释放资源,然后再释放对象占用的内存。在析构对象时,不同类型的成员变量处理方式也不同:
- 堆上对象的普通成员变量不会单独清理,会伴随对象的销毁而自动销毁
- 堆上对象的对象成员(嵌套对象)会在完成对象析构后再由编译器自动调用对象成员的析构函数进行析构,最后和堆上对象作为整体一次性释放内存
- 堆上对象的指针成员与普通成员变量一样,会伴随对象的销毁而自动销毁,指针指向的对象绝不会自动销毁,所以:必须在对象的析构函数中“显式地 delete 指针指向的对象”,否则就会内存泄漏
栈上
在栈上的对象(使用“直接初始化”语法创建)会在栈退出(作用域结束,栈帧弹出)时自动清理,不需要显式地 delete,同样,不同类型的成员变量处理方式也有所不同:
- 栈上对象的普通成员变量不会单独清理,会伴随栈退出自动销毁
- 栈上对象的对象成员(嵌套对象)会在完成对象析构后再由编译器自动调用对象成员的析构函数进行析构,最后会伴随栈退出自动销毁
- 栈上对象的指针成员与普通成员变量一样,会伴随对象的销毁而自动销毁,指针指向的对象绝不会自动销毁,所以:必须在对象的析构函数中“显式地 delete 指针指向的对象”,否则就会内存泄漏
1. 在栈上创建/销毁对象
1.1 示例
我们设计了这样一个可以说明多种情形的示例:一个 Order 对象,包含:
- 一个普通成员变量:orderId
- 一个对象成员(嵌入对象):address
- 一个指针成员:coupon
我们把 Order 创建在栈上,分析一下它的创建和销毁过程。以下是示例代码:
#include<iostream>#include<string>usingnamespacestd;// 优惠券(用作 Order 的指针成员,独立资源,用指针关联)classCoupon{private:string couponCode;intdiscountAmount;public:Coupon(string code,intdiscount):couponCode(code),discountAmount(discount){cout<<"[Coupon] 构造函数执行:创建优惠券对象(地址:"<<this<<")"<<endl;}~Coupon(){cout<<"[Coupon] 析构函数执行:销毁优惠券对象(地址:"<<this<<")"<<endl;}intgetDiscount()const{returndiscountAmount;}};// 收货地址(用作 Order 的对象成员,嵌套在订单中)classAddress{private:string receiver;string detail;public:Address(string r,string d):receiver(r),detail(d){cout<<"[Address] 构造函数执行:创建地址对象(地址:"<<this<<")"<<endl;}~Address(){cout<<"[Address] 析构函数执行:销毁地址对象(地址:"<<this<<")"<<endl;}};// 订单(主类,演示对象的创建与销毁)classOrder{private:intorderId;// 普通成员变量Address address;// 对象成员Coupon*coupon;// 指针成员public:Order(intid,Address addr,Coupon*cp):orderId(id),address(addr),coupon(cp){cout<<"[Order] 构造函数执行:创建订单对象(地址:"<<this<<")"<<endl;}~Order(){// 释放指针指向的堆内存(Coupon对象)if(coupon!=nullptr){deletecoupon;coupon=nullptr;}cout<<"[Order] 析构函数执行:销毁订单对象(地址:"<<this<<")"<<endl;}intgetOrderId()const{returnorderId;}AddressgetAddress()const{returnaddress;}Coupon*getCoupon()const{returncoupon;}};// 测试栈上Order对象的生命周期voidtest(){cout<<">>> 进入 test 函数(栈帧已压入)"<<endl;// 1. 栈上创建Address对象Addressaddr("张三","北京市朝阳区XX小区1号楼101");// 2. 堆上创建Coupon对象Coupon*cp=newCoupon("满100减20",20);// 3. 栈上创建Order对象Orderorder(10001,addr,cp);cout<<">>> test 函数即将返回(对象准备自动销毁)..."<<endl;// 函数结束时,栈上对象自动销毁(析构)}intmain(){test();return0;}程序输出:
>>> 进入 test 函数(栈帧已压入) [Address] 构造函数执行:创建地址对象(地址:0000005CF3EFF810) [Coupon] 构造函数执行:创建优惠券对象(地址:000001DF40C671F0) [Order] 构造函数执行:创建订单对象(地址:0000005CF3EFF880) [Address] 析构函数执行:销毁地址对象(地址:0000005CF3EFF990) >>> test 函数即将返回(对象准备自动销毁)... [Coupon] 析构函数执行:销毁优惠券对象(地址:000001DF40C671F0) [Order] 析构函数执行:销毁订单对象(地址:0000005CF3EFF880) [Address] 析构函数执行:销毁地址对象(地址:0000005CF3EFF888) [Address] 析构函数执行:销毁地址对象(地址:0000005CF3EFF810)1.2 解读
➢ Order 对象的创建过程
- 执行至
test()(第80行)时,函数 test 的栈帧准备压入 - 先处理函数中的变量声明,为
order、addr、cp分配栈空间(注意:cp是指针,仅 8 字节) - 执行至
Order order(10001, addr, cp)(第72行),开始创建 order 对象:- 使用“直接初始化”语法,保证 order 创建在栈上
addr、cp是以值传递方式传给构造函数的,它们分别复制了一份,作为address和coupon,其中address是order的嵌套对象,自然也在栈上,coupon仅为一个指针,目标对象在堆上(因为 coupon 对象是用 new 创建的)。
补充一个可能会让人困惑的知识点:关于函数中出现的“变量名”:像order、orderId、addr、cp这些,在栈上是不会保存这些“变量名”的,这些“变量名”仅仅是程序员和编译器之间使用的“符号”,当代码被编译后,程序只使用“栈基地址 + 偏移量”的方式来标识一个变量。也就是说:“栈基地址 + 偏移量”会定位到栈上的一个内存地址,它是变量的地址,就相当于源代码层面上的“变量名”,地址中存储的数据就是变量的“值”。
➢ Order 对象的销毁过程
- 执行至 test 函数准备返回 (第77行)时,编译器开始自动销毁栈帧上的局部变量。销毁顺序按“声明顺序的逆序”执行(很好的符合了栈的先进后出原则)
- 准备销毁
order对象,编译器会自动调用其析构函数 order的析构函数会主动销毁coupon成员,故,最先销毁的是couponorder的析构函数执行完毕,成为第二个被销毁的对象(其普通成员变量 orderId 不会被针对性处理)- 编译器会保证在对象析构后,会自动调用对象的嵌套对象成员的析构函数,故
address是第三个被销毁的对象 - 最的一个被销毁的对象是最先声明的
addr,编译器同样会自动调用其析构函数 - 栈帧弹出,栈顶指针后移,等同于上述所有变量一次性集体释放内存
2. 在堆上创建/销毁对象
2.1 示例
同样仿照 1.1 的示例,但这次我们把 Order 创建在堆上,分析一下它的创建和销毁过程。以下是示例代码:
#include<iostream>#include<string>usingnamespacestd;// 优惠券(用作 Order 的指针成员,独立资源,用指针关联)classCoupon{private:string couponCode;intdiscountAmount;public:Coupon(string code,intdiscount):couponCode(code),discountAmount(discount){cout<<"[Coupon] 构造函数执行:堆上创建优惠券(地址:"<<this<<")"<<endl;}~Coupon(){cout<<"[Coupon] 析构函数执行:销毁优惠券(地址:"<<this<<")"<<endl;}};// 收货地址(用作 Order 的对象成员,嵌套在订单中)classAddress{private:string receiver;string detail;public:Address(string r,string d):receiver(r),detail(d){cout<<"[Address] 构造函数执行:堆上Order的嵌套地址(地址:"<<this<<")"<<endl;}~Address(){cout<<"[Address] 析构函数执行:销毁嵌套地址(地址:"<<this<<")"<<endl;}};// 订单(主类,演示对象的创建与销毁)classOrder{private:intorderId;// 普通成员变量Address address;// 对象成员Coupon*coupon;// 指针成员public:Order(intid,Address addr,Coupon*cp):orderId(id),address(addr),coupon(cp){cout<<"[Order] 构造函数执行:堆上创建订单(地址:"<<this<<")"<<endl;}~Order(){// 手动释放指针指向的堆内存(Coupon)if(coupon!=nullptr){deletecoupon;coupon=nullptr;}cout<<"[Order] 析构函数执行:销毁堆上订单(地址:"<<this<<")"<<endl;}intgetOrderId()const{returnorderId;}};// 测试堆上Order对象的生命周期voidtest(){cout<<">>> 进入 test 函数(栈帧已压入)"<<endl;// 1. 先创建栈上的Address临时对象(用于初始化堆上Order的嵌套成员)Addressaddr("张三","北京市朝阳区XX小区1号楼101");// 2. 堆上创建Coupon对象(C)Coupon*cp=newCoupon("满100减20",20);// 3. 堆上创建Order对象(核心操作:new关键字)Order*order=newOrder(10001,addr,cp);cout<<"堆上Order对象的指针地址(栈上):"<<&order<<endl;cout<<"堆上Order对象本身的地址:"<<order<<endl;// 4. 手动销毁堆上Order对象(必须显式delete,否则内存泄漏)cout<<">>> 手动调用 delete 销毁堆上 order..."<<endl;deleteorder;order=nullptr;// 避免野指针cout<<">>> test 函数即将返回(对象准备自动销毁)..."<<endl;}intmain(){test();return0;}程序输出:
>>> 进入 test 函数(栈帧已压入) [Address] 构造函数执行:堆上Order的嵌套地址(地址:000000DCB391F950) [Coupon] 构造函数执行:堆上创建优惠券(地址:00000220F5F772C0) [Order] 构造函数执行:堆上创建订单(地址:00000220F5F77000) [Address] 析构函数执行:销毁嵌套地址(地址:000000DCB391FA80) 堆上Order对象的指针地址(栈上):000000DCB391F9B8 堆上Order对象本身的地址:00000220F5F77000 >>> 手动调用 delete 销毁堆上 order... [Coupon] 析构函数执行:销毁优惠券(地址:00000220F5F772C0) [Order] 析构函数执行:销毁堆上订单(地址:00000220F5F77000) [Address] 析构函数执行:销毁嵌套地址(地址:00000220F5F77008) >>> test 函数即将返回(对象准备自动销毁)... [Address] 析构函数执行:销毁嵌套地址(地址:000000DCB391F950)2.2 解读
➢ Order 对象的创建过程
在堆上创建order对象的过程与栈上基本是一样的,唯一的区别是:order 是使用 new 运算符创建的,它是在堆上开辟内存,来存放order对象,返回的是内存地址,只能使用指针来接收。
➢ Order 对象的销毁过程
- 执行至 test 函数的
delete order(第70行)时,准备销毁order对象 - delete 先调用
order的析构函数 order的析构函数会主动销毁coupon成员,故,最先销毁的是couponorder的析构函数执行完毕,接着被 delete 释放了占用的内存(其普通成员变量 orderId 不会被针对性处理),成为第二个被销毁的对象- 编译器会保证在对象析构后,会自动调用对象的嵌套对象成员的析构函数,故
address是第三个被销毁的对象(行为与栈上相同) - 执行至 test 函数准备返回 (第74行)时,编译器开始自动销毁栈帧上的局部变量。销毁顺序按“声明顺序的逆序”执行(很好的符合了栈的先进后出原则)
- 栈上只有一个对象,就是最先声明的
addr,编译器同样会自动调用其析构函数 - 栈帧弹出,栈顶指针后移,等同于上述所有变量一次性集体释放内存
3. 总结
最后,我们再切换一下视角,从普通成员变量、对象成员(嵌入对象)、指针成员的角度再总结一下它们伴随对象销毁的行为准则:
➢ 普通成员变量
不管成员变量所属的对象是在栈上还是在堆上,当它所属的对象被销毁时,它们会都跟随对象一起销毁,无需特别关心。
➢ 指针成员
不管指针成员所属的对象是在栈上还是在堆上,当它所属的对象被销毁时,它们会都跟随对象一起销毁,但是,指针所指的对象绝不会自动销毁,必须手动调用 delete 才能销毁。
➢ 对象成员
一个对象,如果是在栈上,不需要手动 delete,在栈退出时编译器会自动调用它的析构函数释放资源,然后伴随栈顶指针的移动,其所占内存也被直接释放
一个对象中的对象(对象成员、嵌套对象),如果它所属的对象在栈上,那它也必定在栈上,不需要手动 delete,在栈退出时编译器会自动调用其所属对象的析构函数,然后再自动调用嵌套对象的析构函数,最后伴随栈顶指针的移动,其所占内存会随其所属对象的内存被直接释放
一个对象,如果是在堆上,必须手动 delete,编译器会先调用它的析构函数释放资源,然后再释放它在堆上的内存空间
一个对象中的对象(对象成员、嵌套对象),如果它所属的对象在堆上,那它也在必定堆上,你不需要手动 delete 它(也没有机会 delete 它,因为它不是指针),编译器会自动调用其所属对象的析构函数,然后自动调用嵌套对象的析构函数,最后嵌套对象的内存会跟随其所属对象的堆内存一起被 delete 释放