news 2026/8/29 20:22:19

C++ 对象和嵌套对象的创建与销毁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ 对象和嵌套对象的创建与销毁

在了解了 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 对象的创建过程

  1. 执行至test()(第80行)时,函数 test 的栈帧准备压入
  2. 先处理函数中的变量声明,为orderaddrcp分配栈空间(注意:cp是指针,仅 8 字节)
  3. 执行至Order order(10001, addr, cp)(第72行),开始创建 order 对象:
    • 使用“直接初始化”语法,保证 order 创建在栈上
    • addrcp是以值传递方式传给构造函数的,它们分别复制了一份,作为addresscoupon,其中addressorder的嵌套对象,自然也在栈上,coupon仅为一个指针,目标对象在堆上(因为 coupon 对象是用 new 创建的)。

补充一个可能会让人困惑的知识点:关于函数中出现的“变量名”:像orderorderIdaddrcp这些,在栈上是不会保存这些“变量名”的,这些“变量名”仅仅是程序员和编译器之间使用的“符号”,当代码被编译后,程序只使用“栈基地址 + 偏移量”的方式来标识一个变量。也就是说:“栈基地址 + 偏移量”会定位到栈上的一个内存地址,它是变量的地址,就相当于源代码层面上的“变量名”,地址中存储的数据就是变量的“值”。

➢ Order 对象的销毁过程

  1. 执行至 test 函数准备返回 (第77行)时,编译器开始自动销毁栈帧上的局部变量。销毁顺序按“声明顺序的逆序”执行(很好的符合了栈的先进后出原则)
  2. 准备销毁order对象,编译器会自动调用其析构函数
  3. order的析构函数会主动销毁coupon成员,故,最先销毁的是coupon
  4. order的析构函数执行完毕,成为第二个被销毁的对象(其普通成员变量 orderId 不会被针对性处理)
  5. 编译器会保证在对象析构后,会自动调用对象的嵌套对象成员的析构函数,故address是第三个被销毁的对象
  6. 最的一个被销毁的对象是最先声明的addr,编译器同样会自动调用其析构函数
  7. 栈帧弹出,栈顶指针后移,等同于上述所有变量一次性集体释放内存

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 对象的销毁过程

  1. 执行至 test 函数的delete order(第70行)时,准备销毁order对象
  2. delete 先调用order的析构函数
  3. order的析构函数会主动销毁coupon成员,故,最先销毁的是coupon
  4. order的析构函数执行完毕,接着被 delete 释放了占用的内存(其普通成员变量 orderId 不会被针对性处理),成为第二个被销毁的对象
  5. 编译器会保证在对象析构后,会自动调用对象的嵌套对象成员的析构函数,故address是第三个被销毁的对象(行为与栈上相同)
  6. 执行至 test 函数准备返回 (第74行)时,编译器开始自动销毁栈帧上的局部变量。销毁顺序按“声明顺序的逆序”执行(很好的符合了栈的先进后出原则)
  7. 栈上只有一个对象,就是最先声明的addr,编译器同样会自动调用其析构函数
  8. 栈帧弹出,栈顶指针后移,等同于上述所有变量一次性集体释放内存

3. 总结

最后,我们再切换一下视角,从普通成员变量、对象成员(嵌入对象)、指针成员的角度再总结一下它们伴随对象销毁的行为准则:

➢ 普通成员变量

不管成员变量所属的对象是在栈上还是在堆上,当它所属的对象被销毁时,它们会都跟随对象一起销毁,无需特别关心。

➢ 指针成员

不管指针成员所属的对象是在栈上还是在堆上,当它所属的对象被销毁时,它们会都跟随对象一起销毁,但是,指针所指的对象绝不会自动销毁,必须手动调用 delete 才能销毁。

➢ 对象成员

  1. 一个对象,如果是在栈上,不需要手动 delete,在栈退出时编译器会自动调用它的析构函数释放资源,然后伴随栈顶指针的移动,其所占内存也被直接释放

  2. 一个对象中的对象(对象成员、嵌套对象),如果它所属的对象在栈上,那它也必定在栈上,不需要手动 delete,在栈退出时编译器会自动调用其所属对象的析构函数,然后再自动调用嵌套对象的析构函数,最后伴随栈顶指针的移动,其所占内存会随其所属对象的内存被直接释放

  3. 一个对象,如果是在堆上,必须手动 delete,编译器会先调用它的析构函数释放资源,然后再释放它在堆上的内存空间

  4. 一个对象中的对象(对象成员、嵌套对象),如果它所属的对象在堆上,那它也在必定堆上,你不需要手动 delete 它(也没有机会 delete 它,因为它不是指针),编译器会自动调用其所属对象的析构函数,然后自动调用嵌套对象的析构函数,最后嵌套对象的内存会跟随其所属对象的堆内存一起被 delete 释放

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 17:13:40

FilterSolutions2019使用手册

双击启动exe左半蓝色按钮对应滤波器的形状&#xff0c;假如你需要elliptic&#xff08;椭圆滤波器&#xff09;点击即可切换滤波器种类1. 巴特沃斯滤波器 (Butterworth Filter)核心特点&#xff1a;最大平坦度响应。通带&#xff1a;在通带内具有最平坦的幅度响应&#xff0c;没…

作者头像 李华
网站建设 2026/7/14 17:13:56

数据结构-队列(链式队列)

数据结构中的链式队列链式队列是一种基于链表实现的队列数据结构&#xff0c;采用先进先出&#xff08;FIFO&#xff09;的原则。与顺序队列不同&#xff0c;链式队列通过动态分配内存存储元素&#xff0c;避免了固定容量限制。链式队列的特点动态扩容&#xff1a;无需预先分配…

作者头像 李华
网站建设 2026/7/14 17:13:41

AI教材生成神器,低查重率保障,快速完成教材创作!

在编写教材的过程中&#xff0c;如何精准满足不同的需求成为了一大挑战。不同年级的学生在认知水平上存在明显差异&#xff0c;内容过于复杂或过于简单都不可行&#xff1b;教师在课堂教学、自主学习等各类场景中对教材的要求各异&#xff0c;这就需要教材的呈现方式灵活多变。…

作者头像 李华
网站建设 2026/7/14 17:13:40

政企智能信创办公解决方案

一、方案总则1.方案背景国家信创战略深化推进&#xff0c;“自主可控、安全可信”成为政企数字化转型核心底线。当前政企信创办公落地面临三大痛点&#xff1a;软硬件兼容性不足、核心数据安全风险突出、智能化与协同效率偏低。本方案以“信创为基、智能为核、安全为要、协同为…

作者头像 李华
网站建设 2026/7/14 17:13:53

自然语言处理与AI编程助手:从四大名著到未来应用

引言自然语言处理&#xff08;NLP&#xff09;是人工智能领域的一颗明珠&#xff0c;它让计算机能够理解、解释和生成人类语言。无论是分析古典文学&#xff0c;还是与智能助手对话&#xff0c;NLP都在悄然改变我们的生活。本文将从三个角度展开&#xff1a;首先&#xff0c;通…

作者头像 李华