【C++初阶】析构函数超详解(误区、语法、调用时机、析构顺序)
系列文章目录
后续会将类与对象中重要的六个默认成员函数(构造、析构、拷贝构造、赋值重载、普通对象和const对象的取地址重载)链接放在这目录里,有学习需求的可以先点个收藏~
文章目录
前言
在 C++ 类与对象体系中,构造函数负责对象创建时的初始化,而析构函数则是对象生命周期的 “收尾者”。二者成对出现,是 C++ 六大默认成员函数中最核心的两组函数。
很多初学者会认为:析构函数是用来销毁对象本身的内存。但这是个误区!!!
对象的栈 / 堆内存由操作系统或new/delete管理,析构函数的核心使命是释放对象持有外部资源(堆内存、文件句柄、网络套接字等)。
一、析构函数核心定义与作用
1. 核心定位
析构函数(Destructor)是类的特殊成员函数,属于 C++ 六大默认成员函数之一。
- 执行时机:对象生命周期结束时,系统自动调用,无需手动触发;
- 核心功能:清理对象在运行过程中申请的外部资源(堆内存、打开的文件、硬件句柄等);
- 对比 C 语言:C 语言使用结构体时,需要手动编写
Destroy销毁函数并主动调用;C++ 依靠析构函数自动完成资源清理,彻底避免 “忘记释放资源导致内存泄漏” 的问题。
2. 关键误区纠正(必看)
- 错误认知:析构函数负责释放对象本身的内存。
- 正确认知:析构函数仅清理对象内部引用的外部资源(比如栈类中数组指向的堆内存)。
解释:
局部栈对象:内存由函数栈帧管理,函数结束后栈帧自动回收,和析构无关;
堆对象(new创建):对象内存由delete释放,delete会先调用析构函数,再释放内存。
3. 应用场景区分
- 无外部资源的类(如:Date日期类,成员均为int内置类型):无需手动写析构,编译器会默认生成析构函数;
- 持有动态内存 / 文件句柄的类(如:Stack栈类):必须手动实现析构函数,否则必然造成内存泄漏。
二、析构函数语法规则
1. 基础语法格式
析构函数命名规则:类名前加波浪号 ~,格式如下:
class 类名
{
public:
~类名()
{
// 资源清理逻辑
}
};
2. 七大硬性规则(考点汇总)
规则 1:命名固定,格式唯一
析构函数名必须是 ~类名,波浪号是析构函数的专属标识,不能修改。
规则 2:无返回值、无参数
- 不写返回值(连void都不需要,C++ 语法强制规定);
- 没有任何形参。
延伸推论:因为没有参数,析构函数无法重载。一个类有且只能有一个析构函数(构造函数可以重载,这是二者核心区别)。
规则 3:自动调用,禁止手动常规调用
对象生命周期结束时系统自动执行,正常开发中不需要手动调用。语法上虽然支持显式调用,但毫无意义且可能引起重复析构问题。
规则 4:编译器会生成默认析构
如果我们没有显式定义析构函数,编译器会自动生成一个默认析构函数。
默认析构的行为分两种成员处理:
- 内置类型成员(int、char、指针等):默认析构不做任何处理;
- 自定义类成员(类中嵌套其他类对象):自动调用该成员自身的析构函数,逐层清理。
规则 5:自定义析构不会影响成员析构
哪怕我们手动实现了析构函数,类中的自定义类型成员依然会在当前析构函数执行完毕后,自动调用自身析构。
规则 6:局部对象析构顺序
在同一个局部作用域中,后定义的对象先析构(遵循栈 “后进先出” 的特性)。
规则 7:资源类必须手动实现析构
只要类中通过malloc/new/fopen等申请了外部资源,必须手写析构函数进行释放资源,否则会内存泄漏。
3. 无资源析构代码示例:
Date类成员均为内置int,无外部资源,使用默认析构即可:
#include <iostream>
using namespace std;
class Date
{
public:
// 全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 编译器自动生成默认析构,此处省略
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2026, 6, 8);
d1.Print();
// 函数结束,d1生命周期结束,自动调用默认析构
return 0;
}
三、析构函数的四大调用时机
不同存储类型的对象,析构函数触发时机完全不同。
场景 1:局部栈对象(最常用)
触发时机:对象所在局部作用域结束(函数结束、代码块{}结束),自动调用析构。
#include <iostream>
using namespace std;
class Test
{
public:
~Test()
{
cout << "Test 析构函数被调用" << endl;
}
};
void Func()
{
Test t1; // 定义局部对象
cout << "函数Func执行中" << endl;
} // 作用域结束,t1销毁 → 调用析构
int main()
{
Func();
cout << "main函数继续执行" << endl;
return 0;
}
运行结果:
场景 2:全局 / 静态对象
触发时机:整个程序正常退出时,才会调用析构。
全局 / 静态对象生命周期贯穿整个程序,函数结束不会触发析构。
#include <iostream>
using namespace std;
class Test
{
public:
~Test()
{
cout << "Test 析构函数被调用" << endl;
}
};
Test g_t; // 全局对象
void Func()
{
static Test s_t; // 静态局部对象
cout << "函数Func执行中" << endl;
}
int main()
{
Func();
cout << "main函数执行完毕" << endl;
return 0; // 程序退出,全局、静态对象依次析构
}
运行结果:
场景 3:堆对象(new 创建)
触发时机:必须手动使用delete释放对象,delete会先调用析构函数,再释放堆内存。
如果只new不delete:析构永远不执行,造成内存泄漏。
#include <iostream>
using namespace std;
class Test
{
public:
~Test()
{
cout << "Test 析构函数被调用" << endl;
}
};
int main()
{
Test* p = new Test; // new创建堆对象,仅调用构造
cout << "堆对象使用中" << endl;
delete p; // 先调用析构,再释放堆内存
p = nullptr;
return 0;
}
运行结果:
场景 4:临时对象
触发时机:创建临时对象的完整表达式执行完毕,立即调用析构。
#include <iostream>
using namespace std;
class Test
{
public:
Test() { cout << "构造" << endl; }
~Test() { cout << "析构" << endl; }
};
int main()
{
cout << "开始" << endl;
Test(); // 创建临时对象,表达式结束立即析构
cout << "结束" << endl;
return 0;
}
运行结果:
#include <iostream>
using namespace std;
class Test
{
public:
Test() { cout << "构造" << endl; }
~Test() { cout << "析构" << endl; }
};
int main()
{
cout << "开始" << endl;
Test(); // 创建临时对象,表达式结束立即析构
cout << "结束" << endl;
return 0;
}
四、默认析构函数实战:嵌套类成员
例子:用两个栈实现队列MyQueue类包含两个Stack类型的成员对象,用来理解自定义成员的析构规则。
1. 代码实现
#include <iostream>
#include <cstdlib>
using namespace std;
typedef int STDateType;
// 栈类:持有堆内存,手动实现析构
class Stack
{
public:
// 全缺省构造,默认容量4
Stack(int n = 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成员(无自定义析构,使用默认析构)
class MyQueue
{
private:
Stack pushst; // 入栈成员
Stack popst; // 出栈成员
};
int main()
{
MyQueue mq; // 创建队列对象
return 0; // 作用域结束,mq析构
}
2. 流程解析
MyQueue没有手动写析构,使用编译器默认析构;- 默认析构不会处理内置成员,但会依次调用所有自定义成员(pushst、popst)的析构函数;
- 最终两个
Stack对象的析构函数被执行,堆内存正常释放,无内存泄漏。
3. 结论
- 嵌套自定义成员的类,哪怕不写析构,成员的资源也会被自动清理;
- 只有当前类自身申请外部资源时,才需要手动实现析构。
五、有资源析构实战(Stack 栈类)
通过Stack栈类,对比C 语言手动销毁和C++ 析构自动销毁的差异。
1. C 语言版本
#include <stdio.h>
#include <stdlib.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
size_t _capacity;
size_t _top;
}ST;
// 初始化
void STInit(ST* st, int n)
{
st->_a = (STDataType*)malloc(sizeof(STDataType)*n);
st->_capacity = n;
st->_top = 0;
}
// 手动销毁(必须主动调用,容易遗忘)
void STDestroy(ST* st)
{
free(st->_a);
st->_a = NULL;
}
int main()
{
ST st;
STInit(&st, 4);
// 业务逻辑...
STDestroy(&st); // 忘记调用会内存泄漏
return 0;
}
2. C++ 版本
#include <iostream>
#include <cstdlib>
using namespace std;
typedef int STDateType;
class Stack
{
public:
Stack(int n = 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;
}
void Push(STDataType x)
{
_a[_top++] = x;
}
private:
STDateType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st; // 构造初始化
st.Push(10);
st.Push(20);
// 无需手动销毁,函数结束自动析构释放内存
return 0;
}
六、析构顺序(重点!!!)
1. 基础规则
同一局部作用域内的多个对象:
- 构造顺序:从上到下(代码书写顺序)
- 析构顺序:从下到上(后构造的对象先析构)
本质原因:局部对象存储在栈中,栈遵循 后进先出(LIFO) 规则。
2. 代码示例:
#include <iostream>
using namespace std;
class Test
{
public:
Test(int id)
: _id(id)
{
cout << "构造对象:" << _id << endl;
}
~Test()
{
cout << "析构对象:" << _id << endl;
}
private:
int _id;
};
int main()
{
Test t1(1); // 第一个构造
Test t2(2); // 第二个构造
Test t3(3); // 第三个构造
cout << "执行完毕" << endl;
return 0;
}
运行结果:
可以清晰看到:t3最后构造,最先析构;t1最先构造,最后析构。
3. 嵌套成员析构顺序
对于包含自定义成员的类:
- 构造顺序:先构造成员 → 再构造当前对象
- 析构顺序:先析构当前对象 → 再析构成员(逆序)
总结
下一篇讲拷贝构造函数。
更多推荐

所有评论(0)