目录
一、一个崩溃的程序
二、为什么析构函数不能抛出异常?
核心原因:同时存在两个异常
标准的规定
三、正确的做法:吞掉所有异常
通用模板
四、标准库中的例子
std::vector 的析构函数
std::unique_ptr 的自定义删除器
五、noexcept 关键字的作用
验证默认行为
六、完整例子:安全的资源管理类
七、常见误区
误区1:认为可以在析构函数中抛异常然后让调用者处理
误区2:认为只有在栈展开时抛异常才危险
误区3:认为可以用 noexcept(false) 绕开规则
八、最佳实践总结
九、这一篇的收获
一、一个崩溃的程序
先看这段代码,猜猜会发生什么:
cpp
#include <iostream> #include <stdexcept> using namespace std; class Dangerous { public: ~Dangerous() { cout << "析构函数开始" << endl; throw runtime_error("析构函数异常"); // ❌ 析构函数抛异常 cout << "析构函数结束" << endl; } }; int main() { try { Dangerous d; throw runtime_error("main 中的异常"); } catch (const exception& e) { cout << "捕获到: " << e.what() << endl; } return 0; }运行结果(典型输出):
text
析构函数开始 terminate called after throwing an instance of 'std::runtime_error' what(): 析构函数异常 Aborted (core dumped)
程序直接崩溃,catch块根本没有机会执行。
发生了什么?
main中抛出第一个异常栈展开开始,
d被析构析构函数中抛出第二个异常
两个异常同时存在 → C++ 调用
std::terminate()
二、为什么析构函数不能抛出异常?
核心原因:同时存在两个异常
C++ 异常处理机制不支持同时处理两个活跃异常。如果栈展开过程中析构函数又抛出异常,程序无法决定该处理哪个,唯一的选择就是终止。
cpp
// 场景1:栈展开中抛异常 → 崩溃 try { throw A(); // 异常1 } catch(...) { // 析构局部对象时,如果某个析构函数抛异常 B // 程序 terminate } // 场景2:析构函数在非栈展开时抛异常 // 理论上可以捕获,但风险极大,仍然不推荐标准的规定
C++ 标准明确:如果栈展开过程中析构函数抛出了异常,程序会调用std::terminate()。
这意味着:即使你写了catch(...),也救不了。
cpp
int main() { try { Dangerous d; // 析构函数会抛异常 } catch (...) { // 这个 catch 无法捕获析构函数在栈展开时抛的异常 cout << "不会执行到这里" << endl; } }三、正确的做法:吞掉所有异常
如果析构函数中的操作可能失败(比如关闭文件、释放网络连接、写日志),正确的做法是:
在析构函数内部用
try-catch捕获所有异常记录日志(或采取其他补救措施)
绝不重新抛出
cpp
class FileCloser { FILE* file; public: FileCloser(FILE* f) : file(f) {} ~FileCloser() { try { if (file && fclose(file) != 0) { // fclose 可能失败,但无法向调用者报告 // 只能记录日志 cerr << "警告: 关闭文件失败" << endl; } } catch (...) { // 捕获任何异常,确保不向外传播 cerr << "警告: 关闭文件时发生未知异常" << endl; } } };通用模板
cpp
class ResourceGuard { // 资源句柄 public: ~ResourceGuard() { try { // 可能失败的清理操作 doCleanup(); } catch (const std::exception& e) { // 记录异常信息,但不抛出 std::cerr << "析构函数异常: " << e.what() << std::endl; } catch (...) { std::cerr << "析构函数未知异常" << std::endl; } // 函数正常结束,不向外传播异常 } };四、标准库中的例子
std::vector 的析构函数
std::vector的析构函数会调用每个元素的析构函数。如果某个元素的析构函数抛异常,标准库的选择是:立即 terminate。
cpp
struct Bad { ~Bad() { throw std::runtime_error("Bad 析构异常"); } }; int main() { std::vector<Bad> v(10); // v 析构时 → terminate,程序崩溃 }这就是为什么自定义类型的析构函数必须遵守“不抛异常”规则。
std::unique_ptr 的自定义删除器
unique_ptr允许自定义删除器,但删除器也应该不抛异常:
cpp
auto deleter = [](FILE* f) { if (f) { try { fclose(f); } catch (...) { // 吞掉异常,不传播 } } }; unique_ptr<FILE, decltype(deleter)> file(fopen("test.txt", "r"), deleter);五、noexcept 关键字的作用
C++11 引入了noexcept,可以明确声明一个函数不会抛出异常。
cpp
class Safe { public: ~Safe() noexcept { // 如果这里抛异常,会直接 terminate // 所以必须确保内部不会抛出 } };如果析构函数被标记为noexcept(默认就是),抛出异常就会调用terminate。这更加强化了规则。
验证默认行为
cpp
class Test { public: ~Test() { throw 42; // 默认是 noexcept(true) 吗? } }; // C++11 起,析构函数默认是 noexcept static_assert(noexcept(std::declval<Test>().~Test()), "析构函数应该是 noexcept");C++11 开始,析构函数隐式是noexcept(除非基类或成员析构函数是noexcept(false))。
六、完整例子:安全的资源管理类
cpp
#include <iostream> #include <stdexcept> #include <cstdio> #include <memory> using namespace std; class DatabaseConnection { int conn_id; bool is_open; void doDisconnect() { // 模拟断开连接,可能失败 if (conn_id == 0) { throw runtime_error("连接句柄无效"); } cout << "断开连接: " << conn_id << endl; is_open = false; } public: DatabaseConnection(int id) : conn_id(id), is_open(true) { if (id == -1) { throw runtime_error("无效的连接ID"); } cout << "建立连接: " << conn_id << endl; } // 主动关闭(可能抛异常) void close() { if (is_open) { doDisconnect(); } } // 析构函数:保证不抛异常 ~DatabaseConnection() { try { if (is_open) { doDisconnect(); } } catch (const exception& e) { // 记录日志,但不向外传播 cerr << "析构函数警告: 关闭连接 " << conn_id << " 时发生异常: " << e.what() << endl; } catch (...) { cerr << "析构函数警告: 关闭连接 " << conn_id << " 时发生未知异常" << endl; } } void query(const string& sql) { if (!is_open) { throw runtime_error("连接已关闭"); } cout << "执行查询: " << sql << endl; } }; int main() { cout << "=== 正常场景 ===" << endl; { DatabaseConnection db(1); db.query("SELECT * FROM users"); db.close(); // 主动关闭,可以捕获异常 } cout << "\n=== 异常场景:连接无效 ===" << endl; try { DatabaseConnection db(-1); // 构造函数抛异常 } catch (const exception& e) { cout << "捕获: " << e.what() << endl; } cout << "\n=== 析构函数中的异常被吞掉 ===" << endl; { DatabaseConnection db(2); db.query("SELECT * FROM orders"); // 不调用 close,由析构函数关闭 // 即使 doDisconnect 抛异常,也会被捕获并记录,程序正常运行 } cout << "\n程序正常结束" << endl; return 0; }输出:
text
=== 正常场景 === 建立连接: 1 执行查询: SELECT * FROM users 断开连接: 1 === 异常场景:连接无效 === 捕获: 无效的连接ID === 析构函数中的异常被吞掉 === 建立连接: 2 执行查询: SELECT * FROM orders 断开连接: 2 程序正常结束
七、常见误区
误区1:认为可以在析构函数中抛异常然后让调用者处理
cpp
// ❌ 错误 ~MyClass() { if (error) throw MyError(); }无法保证调用者能捕获到,特别是在栈展开过程中。
误区2:认为只有在栈展开时抛异常才危险
即使不在栈展开过程中,析构函数抛异常也会导致:
cpp
int main() { MyClass* p = new MyClass(); delete p; // 如果 ~MyClass() 抛异常,程序可能终止或资源泄漏 }误区3:认为可以用 noexcept(false) 绕开规则
cpp
class Bad { public: ~Bad() noexcept(false) { throw 42; // 理论上可以,但实践中是灾难 } };即使这样声明,栈展开时抛异常仍然会terminate。
八、最佳实践总结
| 规则 | 说明 |
|---|---|
| 永远不要让异常离开析构函数 | 用try-catch捕获所有异常 |
| 记录失败信息 | 写入日志或cerr,便于调试 |
| 不要重新抛出 | 即使想重新抛出,也做不到安全传播 |
主动提供close()方法 | 给调用者一个可以处理错误的途径 |
| 使用 RAII 管理资源 | 让智能指针、容器管理资源,避免手写析构函数 |
标记析构函数为noexcept | 让编译器帮你检查 |
九、这一篇的收获
你现在应该理解:
析构函数抛异常是危险的:栈展开过程中会导致
terminate核心原因:无法同时处理两个活跃异常
正确做法:在析构函数内用
try-catch捕获所有异常,记录日志,但不向外传播主动提供
close()方法:让需要处理失败的调用者有机会捕获异常C++11 起析构函数默认
noexcept:强化了这条规则
💡 小作业:写一个
ScopedFile类,在析构函数中关闭文件。故意让fclose失败(如传入无效指针),确保析构函数不会崩溃。同时提供一个close()方法,让调用者可以主动关闭并处理错误。
下一篇预告:第37篇《面向对象设计原则(一):单一职责与开闭原则》——进入设计模式与设计原则章节。单一职责:一个类只做一件事;开闭原则:对扩展开放,对修改关闭。这些原则是写出可维护 OOP 代码的基石。