目录一、一个崩溃的程序二、为什么析构函数不能抛出异常核心原因同时存在两个异常标准的规定三、正确的做法吞掉所有异常通用模板四、标准库中的例子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(...)也救不了。cppint main() { try { Dangerous d; // 析构函数会抛异常 } catch (...) { // 这个 catch 无法捕获析构函数在栈展开时抛的异常 cout 不会执行到这里 endl; } }三、正确的做法吞掉所有异常如果析构函数中的操作可能失败比如关闭文件、释放网络连接、写日志正确的做法是在析构函数内部用try-catch捕获所有异常记录日志或采取其他补救措施绝不重新抛出cppclass FileCloser { FILE* file; public: FileCloser(FILE* f) : file(f) {} ~FileCloser() { try { if (file fclose(file) ! 0) { // fclose 可能失败但无法向调用者报告 // 只能记录日志 cerr 警告: 关闭文件失败 endl; } } catch (...) { // 捕获任何异常确保不向外传播 cerr 警告: 关闭文件时发生未知异常 endl; } } };通用模板cppclass 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。cppstruct Bad { ~Bad() { throw std::runtime_error(Bad 析构异常); } }; int main() { std::vectorBad v(10); // v 析构时 → terminate程序崩溃 }这就是为什么自定义类型的析构函数必须遵守“不抛异常”规则。std::unique_ptr 的自定义删除器unique_ptr允许自定义删除器但删除器也应该不抛异常cppauto deleter [](FILE* f) { if (f) { try { fclose(f); } catch (...) { // 吞掉异常不传播 } } }; unique_ptrFILE, decltype(deleter) file(fopen(test.txt, r), deleter);五、noexcept 关键字的作用C11 引入了noexcept可以明确声明一个函数不会抛出异常。cppclass Safe { public: ~Safe() noexcept { // 如果这里抛异常会直接 terminate // 所以必须确保内部不会抛出 } };如果析构函数被标记为noexcept默认就是抛出异常就会调用terminate。这更加强化了规则。验证默认行为cppclass Test { public: ~Test() { throw 42; // 默认是 noexcept(true) 吗 } }; // C11 起析构函数默认是 noexcept static_assert(noexcept(std::declvalTest().~Test()), 析构函数应该是 noexcept);C11 开始析构函数隐式是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认为只有在栈展开时抛异常才危险即使不在栈展开过程中析构函数抛异常也会导致cppint main() { MyClass* p new MyClass(); delete p; // 如果 ~MyClass() 抛异常程序可能终止或资源泄漏 }误区3认为可以用 noexcept(false) 绕开规则cppclass Bad { public: ~Bad() noexcept(false) { throw 42; // 理论上可以但实践中是灾难 } };即使这样声明栈展开时抛异常仍然会terminate。八、最佳实践总结规则说明永远不要让异常离开析构函数用try-catch捕获所有异常记录失败信息写入日志或cerr便于调试不要重新抛出即使想重新抛出也做不到安全传播主动提供close()方法给调用者一个可以处理错误的途径使用 RAII 管理资源让智能指针、容器管理资源避免手写析构函数标记析构函数为noexcept让编译器帮你检查九、这一篇的收获你现在应该理解析构函数抛异常是危险的栈展开过程中会导致terminate核心原因无法同时处理两个活跃异常正确做法在析构函数内用try-catch捕获所有异常记录日志但不向外传播主动提供close()方法让需要处理失败的调用者有机会捕获异常C11 起析构函数默认noexcept强化了这条规则 小作业写一个ScopedFile类在析构函数中关闭文件。故意让fclose失败如传入无效指针确保析构函数不会崩溃。同时提供一个close()方法让调用者可以主动关闭并处理错误。下一篇预告第37篇《面向对象设计原则一单一职责与开闭原则》——进入设计模式与设计原则章节。单一职责一个类只做一件事开闭原则对扩展开放对修改关闭。这些原则是写出可维护 OOP 代码的基石。