当前位置: 首页 > news >正文

【C++初阶】析构函数超详解(误区、语法、调用时机、析构顺序)

系列文章目录

后续会将类与对象中重要的六个默认成员函数(构造、析构、拷贝构造、赋值重载、普通对象和const对象的取地址重载)链接放在这目录里,有学习需求的可以先点个收藏~


文章目录

  • 系列文章目录
  • 前言
  • 一、析构函数核心定义与作用
    • 1. 核心定位
    • 2. 关键误区纠正(必看)
    • 3. 应用场景区分
  • 二、析构函数语法规则
    • 1. 基础语法格式
    • 2. 七大硬性规则(考点汇总)
      • 规则 1:命名固定,格式唯一
      • 规则 2:无返回值、无参数
      • 规则 3:自动调用,禁止手动常规调用
      • 规则 4:编译器会生成默认析构
      • 规则 5:自定义析构不会影响成员析构
      • 规则 6:局部对象析构顺序
      • 规则 7:资源类必须手动实现析构
    • 3. 无资源析构代码示例:
  • 三、析构函数的四大调用时机
    • 场景 1:局部栈对象(最常用)
    • 场景 2:全局 / 静态对象
    • 场景 3:堆对象(new 创建)
    • 场景 4:临时对象
  • 四、默认析构函数实战:嵌套类成员
    • 1. 代码实现
    • 2. 流程解析
    • 3. 结论
  • 五、有资源析构实战(Stack 栈类)
    • 1. C 语言版本
    • 2. C++ 版本
  • 六、析构顺序(重点!!!)
    • 1. 基础规则
    • 2. 代码示例:
    • 3. 嵌套成员析构顺序
  • 总结

前言

在 C++ 类与对象体系中,构造函数负责对象创建时的初始化,而析构函数则是对象生命周期的 “收尾者”。二者成对出现,是 C++ 六大默认成员函数中最核心的两组函数。

很多初学者会认为:析构函数是用来销毁对象本身的内存。但这是个误区!!!
对象的栈 / 堆内存由操作系统或new/delete管理,析构函数的核心使命是释放对象持有外部资源(堆内存、文件句柄、网络套接字等)。


一、析构函数核心定义与作用

1. 核心定位

析构函数(Destructor)是类的特殊成员函数,属于 C++ 六大默认成员函数之一。

  • 执行时机对象生命周期结束时,系统自动调用,无需手动触发;
  • 核心功能:清理对象在运行过程中申请的外部资源(堆内存、打开的文件、硬件句柄等);
  • 对比 C 语言:C 语言使用结构体时,需要手动编写Destroy销毁函数并主动调用;C++ 依靠析构函数自动完成资源清理,彻底避免 “忘记释放资源导致内存泄漏” 的问题。

2. 关键误区纠正(必看)

  1. 错误认知:析构函数负责释放对象本身的内存。
  2. 正确认知:析构函数仅清理对象内部引用的外部资源(比如栈类中数组指向的堆内存)。

解释:
局部栈对象:内存由函数栈帧管理,函数结束后栈帧自动回收,和析构无关;
堆对象(new创建):对象内存由delete释放,delete会先调用析构函数,再释放内存。

3. 应用场景区分

  • 无外部资源的类(如:Date日期类,成员均为int内置类型):无需手动写析构,编译器会默认生成析构函数;
  • 持有动态内存 / 文件句柄的类(如:Stack栈类):必须手动实现析构函数,否则必然造成内存泄漏。

二、析构函数语法规则

1. 基础语法格式

析构函数命名规则:类名前加波浪号~,格式如下:

class类名{public:~类名(){// 资源清理逻辑}};

2. 七大硬性规则(考点汇总)

规则 1:命名固定,格式唯一

析构函数名必须是~类名,波浪号是析构函数的专属标识,不能修改。

规则 2:无返回值、无参数

  • 不写返回值(连void都不需要,C++ 语法强制规定);
  • 没有任何形参。

延伸推论:因为没有参数,析构函数无法重载。一个类有且只能有一个析构函数(构造函数可以重载,这是二者核心区别)。

规则 3:自动调用,禁止手动常规调用

对象生命周期结束时系统自动执行,正常开发中不需要手动调用。语法上虽然支持显式调用,但毫无意义且可能引起重复析构问题。

规则 4:编译器会生成默认析构

如果我们没有显式定义析构函数,编译器会自动生成一个默认析构函数
默认析构的行为分两种成员处理:

  1. 内置类型成员(int、char、指针等):默认析构不做任何处理
  2. 自定义类成员(类中嵌套其他类对象):自动调用该成员自身的析构函数,逐层清理。

规则 5:自定义析构不会影响成员析构

哪怕我们手动实现了析构函数,类中的自定义类型成员依然会在当前析构函数执行完毕后,自动调用自身析构。

规则 6:局部对象析构顺序

在同一个局部作用域中,后定义的对象先析构(遵循栈 “后进先出” 的特性)。

规则 7:资源类必须手动实现析构

只要类中通过malloc/new/fopen等申请了外部资源,必须手写析构函数进行释放资源,否则会内存泄漏。

3. 无资源析构代码示例:

Date类成员均为内置int,无外部资源,使用默认析构即可:

#include<iostream>usingnamespacestd;classDate{public:// 全缺省构造函数Date(intyear=1,intmonth=1,intday=1){_year=year;_month=month;_day=day;}voidPrint(){cout<<_year<<"/"<<_month<<"/"<<_day<<endl;}// 编译器自动生成默认析构,此处省略private:int_year;int_month;int_day;};intmain(){Dated1(2026,6,8);d1.Print();// 函数结束,d1生命周期结束,自动调用默认析构return0;}

三、析构函数的四大调用时机

不同存储类型的对象,析构函数触发时机完全不同。

场景 1:局部栈对象(最常用)

触发时机:对象所在局部作用域结束(函数结束、代码块{}结束),自动调用析构。

#include<iostream>usingnamespacestd;classTest{public:~Test(){cout<<"Test 析构函数被调用"<<endl;}};voidFunc(){Test t1;// 定义局部对象cout<<"函数Func执行中"<<endl;}// 作用域结束,t1销毁 → 调用析构intmain(){Func();cout<<"main函数继续执行"<<endl;return0;}

运行结果:

场景 2:全局 / 静态对象

触发时机:整个程序正常退出时,才会调用析构。
全局 / 静态对象生命周期贯穿整个程序,函数结束不会触发析构。

#include<iostream>usingnamespacestd;classTest{public:~Test(){cout<<"Test 析构函数被调用"<<endl;}};Test g_t;// 全局对象voidFunc(){staticTest s_t;// 静态局部对象cout<<"函数Func执行中"<<endl;}intmain(){Func();cout<<"main函数执行完毕"<<endl;return0;// 程序退出,全局、静态对象依次析构}

运行结果:

场景 3:堆对象(new 创建)

触发时机:必须手动使用delete释放对象,delete会先调用析构函数,再释放堆内存
如果只newdelete:析构永远不执行,造成内存泄漏

#include<iostream>usingnamespacestd;classTest{public:~Test(){cout<<"Test 析构函数被调用"<<endl;}};intmain(){Test*p=newTest;// new创建堆对象,仅调用构造cout<<"堆对象使用中"<<endl;deletep;// 先调用析构,再释放堆内存p=nullptr;return0;}

运行结果:

场景 4:临时对象

触发时机:创建临时对象的完整表达式执行完毕,立即调用析构。

#include<iostream>usingnamespacestd;classTest{public:Test(){cout<<"构造"<<endl;}~Test(){cout<<"析构"<<endl;}};intmain(){cout<<"开始"<<endl;Test();// 创建临时对象,表达式结束立即析构cout<<"结束"<<endl;return0;}

运行结果:

#include<iostream>usingnamespacestd;classTest{public:Test(){cout<<"构造"<<endl;}~Test(){cout<<"析构"<<endl;}};intmain(){cout<<"开始"<<endl;Test();// 创建临时对象,表达式结束立即析构cout<<"结束"<<endl;return0;}

四、默认析构函数实战:嵌套类成员

例子:用两个栈实现队列
MyQueue类包含两个Stack类型的成员对象,用来理解自定义成员的析构规则。

1. 代码实现

#include<iostream>#include<cstdlib>usingnamespacestd;typedefintSTDateType;// 栈类:持有堆内存,手动实现析构classStack{public:// 全缺省构造,默认容量4Stack(intn=4){_a=(STDateType*)malloc(sizeof(STDateType)*n);if(nullptr==_a){perror("malloc 申请空间失败");exit(1);}_capacity=n;_top=0;}// 手动实现析构:释放堆内存~Stack(){cout<<"~Stack() 栈析构"<<endl;free(_a);_a=nullptr;_capacity=0;_top=0;}private:STDateType*_a;// 指向堆内存size_t _capacity;size_t _top;};// 队列类:嵌套两个Stack成员(无自定义析构,使用默认析构)classMyQueue{private:Stack pushst;// 入栈成员Stack popst;// 出栈成员};intmain(){MyQueue mq;// 创建队列对象return0;// 作用域结束,mq析构}

2. 流程解析

  1. MyQueue没有手动写析构,使用编译器默认析构
  2. 默认析构不会处理内置成员,但会依次调用所有自定义成员(pushst、popst)的析构函数
  3. 最终两个Stack对象的析构函数被执行,堆内存正常释放,无内存泄漏。

3. 结论

  • 嵌套自定义成员的类,哪怕不写析构,成员的资源也会被自动清理;
  • 只有当前类自身申请外部资源时,才需要手动实现析构。

五、有资源析构实战(Stack 栈类)

通过Stack栈类,对比C 语言手动销毁C++ 析构自动销毁的差异。

1. C 语言版本

#include<stdio.h>#include<stdlib.h>typedefintSTDataType;typedefstructStack{STDataType*_a;size_t_capacity;size_t_top;}ST;// 初始化voidSTInit(ST*st,intn){st->_a=(STDataType*)malloc(sizeof(STDataType)*n);st->_capacity=n;st->_top=0;}// 手动销毁(必须主动调用,容易遗忘)voidSTDestroy(ST*st){free(st->_a);st->_a=NULL;}intmain(){ST st;STInit(&st,4);// 业务逻辑...STDestroy(&st);// 忘记调用会内存泄漏return0;}

2. C++ 版本

#include<iostream>#include<cstdlib>usingnamespacestd;typedefintSTDateType;classStack{public:Stack(intn=4){_a=(STDataType*)malloc(sizeof(STDataType)*n);if(nullptr==_a){perror("malloc 申请空间失败");exit(1);}_capacity=n;_top=0;}// 析构自动释放堆内存,无需手动调用~Stack(){free(_a);_a=nullptr;_capacity=0;_top=0;}voidPush(STDataType x){_a[_top++]=x;}private:STDateType*_a;size_t _capacity;size_t _top;};intmain(){Stack st;// 构造初始化st.Push(10);st.Push(20);// 无需手动销毁,函数结束自动析构释放内存return0;}

六、析构顺序(重点!!!)

1. 基础规则

同一局部作用域内的多个对象:

  • 构造顺序:从上到下(代码书写顺序)
  • 析构顺序:从下到上(后构造的对象先析构)

本质原因:局部对象存储在栈中,栈遵循后进先出(LIFO)规则。

2. 代码示例:

#include<iostream>usingnamespacestd;classTest{public:Test(intid):_id(id){cout<<"构造对象:"<<_id<<endl;}~Test(){cout<<"析构对象:"<<_id<<endl;}private:int_id;};intmain(){Testt1(1);// 第一个构造Testt2(2);// 第二个构造Testt3(3);// 第三个构造cout<<"执行完毕"<<endl;return0;}

运行结果:

可以清晰看到:t3最后构造,最先析构;t1最先构造,最后析构。

3. 嵌套成员析构顺序

对于包含自定义成员的类:

  • 构造顺序:先构造成员 → 再构造当前对象
  • 析构顺序:先析构当前对象 → 再析构成员(逆序)

总结

下一篇讲拷贝构造函数

http://www.jsqmd.com/news/980290/

相关文章:

  • Horizon UAG部署后连接服务器还是红叉?别慌,教你一步步排查(从日志分析到FQDN解析)
  • 萤石 ERTC 如何一站式解决智能家居各类通话需求?
  • SolidWorks许可回收误杀率,对比三款横评
  • 计算机毕业设计之django基于Python的bs架构的进门审批管理系统设计与开发
  • 2026长治市黄金回收铂金回收白银回收彩金回收机构实力:项链+戒指+手镯+吊坠专业鉴定上门服务及联系方式推荐 - 亦辰小黄鸭
  • Web数据供应链:从爬虫到AI可信数据资产的四层架构
  • 每日一Go-76(架构篇)|多集群部署 / 容灾 / Failover / Backup / 热迁移
  • 别再只搜Star数了!用GitHub Topics和高级搜索,5分钟找到真正适合你的开源项目
  • 7.5元包邮的RC522读卡器,手把手教你用Arduino Uno复制小区门禁卡(附完整接线图与代码)
  • Python新手必看:用input()和eval()处理用户输入,一个函数搞定五种数学运算
  • 生成式AI发展现状与中长期技术演进趋势分析
  • 《医院HIS药房模块实战避坑系列》之一:月中药品调价+跨价退药账务处理全解析
  • 别再只用print了!Python格式化输出M和N运算结果的3种高级技巧
  • 本地运行的QQ账号绑定信息扫描器(2025绿色单文件版)
  • 企业AI知识库开发服务商推荐,2026年最新测评
  • AI建站工具全流程攻略:从零开始搭建可商用网站
  • 别再为Aspose.Words水印发愁了!手把手教你用JD-GUI搞定19.1版本本地化部署
  • 2026昭通市黄金回收铂金回收白银回收彩金回收机构实力:项链+戒指+手镯+吊坠专业鉴定上门服务及联系方式推荐 - 亦辰小黄鸭
  • HarmonyOS6 map.calculateDistance vs Haversine:两种距离计算方案对比
  • 跨境多店铺管理混乱,先排查浏览器环境边界
  • 人文综合素养类赛事解析,文科生的竞赛新赛道
  • 使用Perfetto网页直接抓取trace 注意事项
  • 餐饮扫码点餐系统源码:支持外卖+自取、多店独立运营,Java后端+Vue3前端
  • PostgreSQL 技术日报 (6月8日)|索引预取迭代,AI 安全功能上新
  • 从Mathtype到BibTeX:让你的IEEE LaTeX写作效率翻倍的几个隐藏技巧
  • pac4j-jwt 身份验证绕过漏洞分析
  • 上市公司空气流通系数(2000-2025)
  • 别再死记硬背了!用TensorFlow 2.x手把手复现Google的WideDeep推荐模型
  • ASP.NET MVC多租户仓储系统源码:支持多企业隔离库存+采购销售财务全流程管理
  • 企业微信外部群机器人接入 AI:一套能落地的工程方案