
Effective C 条款08别让异常逃离析构函数在 C 中异常机制是处理错误的重要手段。但有一个地方绝对不应该让异常逃逸出来——那就是析构函数。今天我们来深入探讨这个看似严格实则必要的规则。一、问题的引入假设我们有一个数据库连接类在析构时需要关闭连接classDBConnection{public:staticDBConnectioncreate(){returnDBConnection();}voidclose(){// 如果关闭失败抛出异常if(!db_close(handle_)){throwstd::runtime_error(Close failed!);}closed_true;}~DBConnection(){if(!closed_){close();// 危险可能抛出异常}}private:DBConnection():handle_(db_open()),closed_(false){}db_handle handle_;boolclosed_;};这个设计有什么问题二、为什么异常不能逃离析构函数2.1 C 的异常处理机制在 C 中当异常被抛出时栈会开始栈展开stack unwinding。栈展开的过程中所有局部对象的析构函数都会被调用以确保资源被正确释放。voidsomeFunction(){DBConnection connDBConnection::create();// ... 其他代码 ...throwstd::exception(Something went wrong!);// conn 的析构函数会在这里被调用栈展开过程中}2.2 双重异常的灾难现在假设在栈展开过程中另一个异常被抛出比如conn的析构函数抛出了异常voidsomeFunction(){DBConnection conn1DBConnection::create();DBConnection conn2DBConnection::create();// ...throwstd::exception(First exception);// 栈展开开始// 1. 调用 conn2.~DBConnection() - 抛出异常// 2. 调用 conn1.~DBConnection() - 又抛出异常}C 标准明确规定如果在栈展开过程中析构函数抛出了异常且未被捕获程序会立即调用std::terminate()导致程序非正常终止。结果程序直接崩溃2.3 即使不在栈展开中也有问题即使在正常的执行流程中析构函数抛出异常也可能导致问题voidprocess(){DBConnection connDBConnection::create();// ... 正常执行完毕conn 离开作用域 ...// conn.~DBConnection() 抛出异常}intmain(){try{process();}catch(conststd::exceptione){// 希望能捕获并处理异常}// 但如果 process() 中还有其他局部对象需要析构呢// 异常会中断正常的析构流程}三、解决方案3.1 方案一吞下异常Swallow the exception在析构函数中捕获所有异常并记录日志classDBConnection{public:~DBConnection(){if(!closed_){try{close();}catch(conststd::exceptione){// 记录日志但不传播异常std::cerrError in destructor: e.what()std::endl;// 或者使用更专业的日志系统Logger::logError(DBConnection::~DBConnection() failed: %s,e.what());}catch(...){// 捕获所有异常确保什么都不抛出std::cerrUnknown error in destructorstd::endl;}}}};优点程序不会崩溃栈展开可以正常完成。缺点错误被静默吞掉了可能掩盖了真正的问题。3.2 方案二结束程序Terminate the program如果错误非常严重无法安全地继续运行classDBConnection{public:~DBConnection(){if(!closed_){try{close();}catch(...){// 记录错误Logger::logFatal(Critical error in DBConnection destructor!);// 结束程序std::abort();// 或者 std::terminate()}}}};适用场景数据一致性已经遭到破坏继续运行可能导致更严重的损失。3.3 方案三将责任转移给调用者最佳实践与其在析构函数中做可能失败的操作不如提供一个显式的关闭接口让用户有机会处理错误classDBConnection{public:staticDBConnectioncreate(){returnDBConnection();}// 显式关闭可能抛出异常voidclose(){if(closed_)return;if(!db_close(handle_)){throwstd::runtime_error(Failed to close database connection);}closed_true;}// 析构函数做兜底但不抛异常~DBConnection(){if(!closed_){try{db_close(handle_);// 直接调用不经过可能抛异常的 close()}catch(...){// 吞下异常记录日志Logger::logError(DBConnection::~DBConnection(): close failed);}}}private:DBConnection():handle_(db_open()),closed_(false){}db_handle handle_;boolclosed_;};使用方式voidprocessData(){DBConnection connDBConnection::create();// ... 使用 conn ...// 显式关闭处理可能的错误try{conn.close();}catch(conststd::exceptione){// 优雅地处理关闭失败std::cerrWarning: connection close failed: e.what()std::endl;}}// 如果 conn.close() 成功析构函数什么都不做// 如果忘记调用 close()析构函数会兜底但吞下异常四、实际应用场景4.1 RAII 资源管理类templatetypenameTclassSmartPointer{public:explicitSmartPointer(T*ptr):ptr_(ptr){}~SmartPointer(){// delete 不会抛出异常所以这里安全deleteptr_;}// 显式释放允许调用者处理异常voidreset(T*ptrnullptr){T*oldptr_;ptr_ptr;if(old){// 如果 T 的析构函数可能抛异常这里需要注意// 但通常 delete 本身不抛异常deleteold;}}private:T*ptr_;};4.2 事务管理类classTransaction{public:Transaction():committed_(false){begin_transaction();}// 显式提交可能失败voidcommit(){if(committed_)return;if(!do_commit()){throwTransactionException(Commit failed);}committed_true;}// 显式回滚voidrollback(){if(committed_)return;do_rollback();committed_true;// 标记为已处理}~Transaction(){if(!committed_){// 如果用户没有显式提交或回滚自动回滚try{do_rollback();}catch(...){// 记录日志不抛异常Logger::logError(Transaction rollback failed in destructor);}}}private:boolcommitted_;};使用方式voidtransferMoney(Accountfrom,Accountto,doubleamount){Transaction tx;from.debit(amount);to.credit(amount);// 显式提交如果失败可以处理try{tx.commit();}catch(constTransactionExceptione){tx.rollback();// 手动回滚throw;// 重新抛出让上层处理}}// 如果中途异常析构函数会自动回滚4.3 文件句柄管理classFileWriter{public:explicitFileWriter(conststd::stringpath):file_(std::fopen(path.c_str(),w)){}voidwrite(conststd::stringdata){if(std::fwrite(data.c_str(),1,data.size(),file_)!data.size()){throwIOError(Write failed);}}// 显式刷新可能失败voidflush(){if(std::fflush(file_)!0){throwIOError(Flush failed);}flushed_true;}~FileWriter(){if(file_){try{if(!flushed_){std::fflush(file_);// 最后一次尝试刷新}std::fclose(file_);}catch(...){// 绝对不能让异常逃离析构函数Logger::logError(FileWriter::~FileWriter() failed);}}}private:FILE*file_;boolflushed_false;};五、C11 及以后的补充5.1 noexcept 关键字C11 引入了noexcept关键字可以显式标记函数不会抛出异常classMyClass{public:~MyClass()noexcept{// 显式承诺不抛异常// ...}};如果noexcept函数实际上抛出了异常程序会立即调用std::terminate()。从 C11 开始析构函数默认就是noexcept的这意味着编译器会帮你强制执行不抛异常的约定。5.2 移动操作与异常在实现移动构造函数和移动赋值运算符时也需要考虑异常安全classMyClass{public:MyClass(MyClassother)noexcept{// 标记为 noexcept// 移动操作通常不抛异常data_other.data_;other.data_nullptr;}~MyClass()noexcept{deletedata_;}};如果移动构造函数标记为noexceptSTL 容器在重新分配内存时会优先使用它而不是拷贝构造函数。六、常见误区6.1 “我的析构函数很简单不会抛异常”即使你的析构函数看起来很简单它调用的其他函数也可能抛异常classWidget{public:~Widget(){cleanup();// 你确定 cleanup() 不会抛异常吗}private:voidcleanup(){resource_.release();// 这个呢}SomeResource resource_;};最佳实践在析构函数中对所有可能失败的操作都使用 try-catch 保护。6.2 “我用智能指针了不需要关心”智能指针的析构函数本身不会抛异常但它管理的对象如果在析构时抛异常问题依然存在classBadClass{public:~BadClass(){throwstd::exception(Oops!);// 千万别这样}};std::unique_ptrBadClassp(newBadClass());// p 销毁时BadClass 的析构函数抛异常 - 程序终止七、总结方案适用场景优缺点吞下异常大多数情况程序稳定运行但可能掩盖问题结束程序致命错误无法安全继续避免数据损坏但用户体验差显式接口 析构兜底最佳实践给用户处理错误的机会析构做安全兜底请记住析构函数绝对不要吐出异常。如果一个析构函数可能抛出异常析构函数应该捕捉任何异常然后吞下它们记录日志或结束程序。更好的设计是提供一个显式的关闭/清理接口让调用者有机会处理错误而析构函数只做安全的兜底操作。从 C11 开始析构函数默认是noexcept的。异常是 C 中强大的错误处理工具但在析构函数这个特殊场景中不抛异常不是限制而是保护。遵循这个规则你的代码将更加健壮和可靠。参考阅读《Effective C》第三版Scott Meyers《C Primer》第五版关于异常安全的章节C Core Guidelines: E.16