析构函数仅在类持有需显式释放的外部资源时才必须编写,如裸指针指向堆内存、打开文件、系统句柄等;raii类型(如std::unique_ptr)已内置释放逻辑,无需额外定义析构函数。

析构函数不是“一定要写”,而是“资源谁申请、谁释放”这条铁律在 C++ 对象生命周期里的落地点。没手动 new、没打开文件、没调用 malloc,通常不用写析构函数。
什么时候必须写析构函数
只有当类内部持有需要显式释放的**外部资源**时,才需要定义析构函数。C++ 不会自动帮你关文件、释放堆内存、卸载 DLL、解除锁或关闭 socket。
- 类里有裸指针成员(比如
int* data),且指向的是new出来的内存 - 类里调用了
fopen、open、CreateFile等打开资源,但没在其他地方统一关闭 - 类里持有系统句柄(如 Windows 的
HANDLE、Linux 的 fd)、线程 ID 或互斥锁对象 - 使用了 C 风格 API(如
curl_easy_init、sqlite3_open)并保存了返回句柄
注意:std::vector、std::string、std::unique_ptr 这类 RAII 类型已自带析构逻辑,它们的成员不构成你写析构函数的理由。
析构函数里不能做哪些事
析构函数是对象“临终前最后能干的事”,环境已经不稳定,很多操作不仅无效,还会引发未定义行为。
立即学习“C++免费学习笔记(深入)”;
- 不能再抛异常——C++ 标准规定,析构函数中抛出未捕获异常会直接调用
std::terminate - 不能调用虚函数(尤其是通过
this调用),因为虚表指针可能已被销毁,行为不可靠 - 不能访问已析构的基类或成员子对象(比如在派生类析构函数里还去读基类的某个
std::string成员,它可能已清空) - 避免调用可能阻塞的操作(如
std::cout 、网络 I/O、等待线程 join),析构时机不可控,容易卡死或死锁
常见错误现象:double free、segmentation fault in ~MyClass()、程序静默退出(其实是 std::terminate 了)。
如何安全释放资源:RAII 是默认解法
与其在析构函数里手动 delete、fclose、CloseHandle,不如从设计上把资源封装进 RAII 类型里。这是现代 C++ 的标准做法,也是最不容易出错的方式。
- 用
std::unique_ptr<t></t>替代裸T*;它会在析构时自动调用delete或自定义 deleter - 用
std::fstream替代FILE*;它的析构函数保证调用close() - 对系统句柄,可包装成类似
ScopedHandle的类,构造时接管句柄,析构时调用CloseHandle或close - 如果必须写析构函数,确保它只做一件事:释放本类直接持有的资源;不依赖其他成员是否还有效
示例(危险 vs 安全):
// 危险:裸指针 + 手动 delete,易漏、易重删
class Bad {
int* p;
public:
Bad() : p(new int[100]) {}
~Bad() { delete[] p; } // 忘写 = default 拷贝/移动,就崩
};
<p>// 安全:交给 unique_ptr 管理
class Good {
std::unique_ptr<int[]> p;
public:
Good() : p(std::make_unique<int[]>(100)) {}
// ~Good() 自动生成,自动释放,无需手写
};</p>析构函数被跳过的几种真实场景
很多人以为“对象出了作用域就一定调用析构函数”,其实有多个例外,容易导致资源泄漏却查不到原因。
-
std::longjmp或信号处理函数中跳转,会绕过栈展开(stack unwinding),析构函数不执行 - 调用
std::exit或_Exit,直接终止进程,不析构任何局部对象 - 对象在
main之前构造(全局/静态对象),若main没运行完就崩溃,其析构函数可能没机会运行 - 用
placement new在指定内存构造对象,但忘了手动调用析构函数(obj->~MyClass())
这些情况虽不常见,但在嵌入式、游戏引擎、服务守护进程中容易踩到。真正关键的资源(如日志文件句柄、硬件寄存器锁),最好加一层主动清理钩子(比如 atexit 或信号 handler 中补救),不能只信析构函数。









