当前位置: 首页 > 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.rkmt.cn/news/1492017.html

相关文章:

  • Python新手必看:用input()和eval()处理用户输入,一个函数搞定五种数学运算
  • Web数据供应链:从爬虫到AI可信数据资产的四层架构
  • AI建站工具全流程攻略:从零开始搭建可商用网站
  • 人文综合素养类赛事解析,文科生的竞赛新赛道
  • 餐饮扫码点餐系统源码:支持外卖+自取、多店独立运营,Java后端+Vue3前端
  • 上市公司空气流通系数(2000-2025)
  • Gemini 3.5逻辑推理与精准度实测:算法题与知识问答场景下的能力边界
  • 企业微信外部群机器人接入 AI:一套能落地的工程方案
  • 2026肇庆市黄金回收铂金回收白银回收彩金回收机构实力:项链+戒指+手镯+吊坠专业鉴定上门服务及联系方式推荐 - 亦辰小黄鸭
  • Bending Spoons 上市声明或揭秘“收购、裁员、然后呢?”策略真相
  • 华为USG6000防火墙升级避坑实录:从V1R1C30到V500R005C20的完整操作指南
  • PHP并发处理与协程入门
  • 成本降87.5%:模具冲头助力3C企业年省28万 - 速递信息
  • OPTICS聚类原理与地理数据实战:破解密度不均聚类难题
  • 无人机管理系统|完整源码交付,支持私有化部署与定制开发
  • 鼻毛剪刀哪个牌子好?鼻毛器哪个牌子最好用?2026鼻毛修剪器第一名
  • 普元EOS平台深度体验:除了快速开发,它的监控治理工具EOS Governor到底有多强?
  • 51单片机控制16×16点阵LED,支持自定义文字滚动显示(含仿真+代码+文档)
  • 逆向工程师的利器:手把手教你将OLLVM-14.x集成到Android NDK(Windows 10环境)
  • 类风湿关节炎 干细胞试验进展怎么样了?
  • 别再只当LCD驱动器了!解锁STM32 FMC的‘隐藏技能’:连接AD7606、OLED等并行总线设备
  • 存量老旧视觉项目智能化升级改造(五):人工全检工位改造 TVA 落地指南|三级报价模板 + 标准工期 + 全维度避坑清单
  • 从GISInternals官网到命令行:一份给Windows用户的GDAL 3.x 最新版避坑配置指南
  • Vue3后台模板:TypeScript + Element Plus 实现多标签页管理界面,零配置开箱即用
  • 小程序毕业设计-基于协同过滤算法的运动场馆服务平台微信小程序基于Springboot+微信小程序的协同过滤算法的运动场馆服务平台设计与实现(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • 别再只会用二极管了!手把手教你用MOSFET搭建一个低压大电流同步整流Buck电路
  • 从“四皇后问题”到“八皇后”:一个Python递归解法,帮你彻底搞懂回溯搜索
  • 让AI成为肌肉记忆:第二自然人机协作工作流
  • 从一根电缆的延时算起:深入理解1553B总线100米长度限制背后的工程考量
  • 别再只会用cv2.blur了!OpenCV均值滤波的3个实战场景与内核大小选择避坑指南