析构函数
更新时间:2026-08-27。本文是
languages/cpp/主题入门层第 19 篇。有生就有灭:对象销毁的那一刻,析构函数自动接管清理。它最朴素的价值是"不用记得清理"——栈对象离开作用域自动析构,这也是后面 RAII 和智能指针的地基。
本文要回答的问题
- 析构函数什么时候调用?和构造函数是什么对偶关系?
- 为什么说"不用记得清理"是析构函数的礼物?
- 栈对象和堆对象的析构时机有什么差别?
一、析构函数:对象销毁时自动调用
class Logger {
FILE* file_;
public:
Logger(const char* path) : file_(fopen(path, "w")) {}
~Logger() { // 析构函数
fclose(file_); // 销毁时自动关文件
}
};析构函数:~ + 类名,无参数、无返回类型。它在对象销毁时自动执行,干的是"出生时干了什么、这里就还回去"的活——关文件、释放内存、断连接。
构造和析构是一对:
| 构造函数 | 析构函数 | |
|---|---|---|
| 语法 | 类名(...) | ~类名() |
| 时机 | 对象创建 | 对象销毁 |
| 次数 | 每个对象一次 | 每个对象一次 |
| 可以重载 | 可以 | 不可以(无参数) |
| 职责 | 获取资源、初始化 | 释放资源、清理 |
二、栈对象:离开作用域自动析构
void process() {
Logger log("run.log"); // 构造:打开文件
do_something();
if (error) return; // 提前返回?
do_more();
} // 无论怎么离开,log 在此析构,文件被关重点看 if (error) return; 这行:函数提前返回,析构函数照样执行。这就是"不用记得清理"的全部含义——你不需要在每一个 return 前面补 fclose,编译器在对象离开作用域时自动补。
for (int i = 0; i < 3; ++i) {
Logger log("x.log"); // 每次循环都构造
// ...
} // 每次循环结束都析构栈对象的生死跟着作用域走:进作用域构造、出作用域析构,天然成对。
三、堆对象:要手动 delete
new 出来的对象住在堆上,不会自动析构,必须 delete:
Logger* p = new Logger("run.log"); // 构造
// ...
delete p; // 析构 + 释放内存忘了 delete 就泄漏:析构不执行、资源不释放。这比 C 的 free 更隐蔽——free 只是不释放内存,忘了 delete 是"析构函数整个不跑",文件、连接全漏。
所以现代 C++ 的态度是:优先栈对象,堆对象交给智能指针(std::unique_ptr 在析构时自动 delete),手工 new/delete 只在万不得已时用。这正是 RAII 与智能指针 篇的核心,这里先记住结论。
四、析构函数里别抛异常
~Logger() {
fclose(file_); // 正常清理
// throw ...; // 禁止!析构里抛异常 = 灾难
}析构函数默认是 noexcept 的(C++11 起),在里面抛异常会导致 std::terminate 直接终止程序——因为析构往往发生在栈展开等"已经在处理异常"的路径上,再抛一个会炸掉异常机制。规则:析构函数只做不抛异常的清理。
五、和 C 对比
| 对比项 | C | C++ |
|---|---|---|
| 资源释放 | 手动 free/fclose,漏了是常态 | 析构自动执行 |
| 提前返回 | 每个 return 都要记得清理 | 作用域结束自动清理 |
| 泄漏的代价 | 内存泄漏 | 析构不跑,资源全漏(更隐蔽) |
| 资源管理范式 | 无标准答案 | RAII:资源随对象生命周期 |
| 忘记清理 | 编译不报错 | 编译器无法报错,但 RAII 把机会降到零 |
C 里 free/fclose 漏一次是内存泄漏,但至少编译器不吭声、运行期还能看出症状。C++ 手工管理时"忘了 delete"是同样的坑,但 RAII 给了你一条彻底绕开的路:让对象的生命周期去管资源的生命周期。
六、与本站主线衔接
- RAII 与智能指针是析构函数的最高级应用,见 RAII 与智能指针;
- 拷贝构造、拷贝赋值和析构经常一起出现(Rule of Three),见 拷贝构造与拷贝赋值;
- 堆上对象的释放与内存管理,见 new/delete 与动态内存 和 L5 内存布局。
一句话总结
析构函数在对象销毁时自动执行,栈对象离开作用域必被调用(包括提前 return 的路径),堆对象要靠 delete 或智能指针触发;它把"记得清理"从人的负担变成编译器的安排,是 RAII 的地基;析构函数默认 noexcept,里面别抛异常,只做不抛异常的清理。
上一篇:构造函数与初始化列表 下一篇:拷贝构造与拷贝赋值