news 2026/5/21 1:09:05

【c++面向对象编程】第36篇:析构函数应永远不抛出异常——原因与最佳实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【c++面向对象编程】第36篇:析构函数应永远不抛出异常——原因与最佳实践

目录

一、一个崩溃的程序

二、为什么析构函数不能抛出异常?

核心原因:同时存在两个异常

标准的规定

三、正确的做法:吞掉所有异常

通用模板

四、标准库中的例子

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块根本没有机会执行。

发生了什么?

  1. main中抛出第一个异常

  2. 栈展开开始,d被析构

  3. 析构函数中抛出第二个异常

  4. 两个异常同时存在 → 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; } }

三、正确的做法:吞掉所有异常

如果析构函数中的操作可能失败(比如关闭文件、释放网络连接、写日志),正确的做法是:

  1. 在析构函数内部用try-catch捕获所有异常

  2. 记录日志(或采取其他补救措施)

  3. 绝不重新抛出

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 代码的基石。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/21 1:08:02

3分钟免费汉化Android Studio:社区中文语言包完整安装教程

3分钟免费汉化Android Studio&#xff1a;社区中文语言包完整安装教程 【免费下载链接】AndroidStudioChineseLanguagePack AndroidStudio中文插件(官方修改版本&#xff09; 项目地址: https://gitcode.com/gh_mirrors/an/AndroidStudioChineseLanguagePack 还在为Andr…

作者头像 李华
网站建设 2026/5/21 1:05:01

大模型如何推理:从分词到答案一秒之内的旅程

一、一秒之内发生了什么 你在输入框里敲下一行字&#xff0c;按下回车。一秒钟后&#xff0c;屏幕上出现了答案。 这一秒里&#xff0c;模型内部经历了什么&#xff1f; 前几篇文章讲的是"身体结构"——Token 怎么切、Embedding 怎么表示、Attention 怎么工作、FFN 怎…

作者头像 李华
网站建设 2026/5/21 1:04:08

Agent 的上下文窗口管理:一个被低估的工程难题

Agent 的上下文窗口管理&#xff1a;一个被低估的工程难题关键词 上下文窗口压缩、Agent记忆分层、提示工程、Transformer架构、LLM推理效率、多智能体上下文共享、上下文窗口安全摘要 上下文窗口&#xff08;Context Window&#xff09;是大型语言模型&#xff08;LLM&#xf…

作者头像 李华
网站建设 2026/5/21 1:03:17

Java 继承与高级特性精讲:继承实现、方法重写、类型转换与多态实战

前言 在Java面向对象三大特性中&#xff0c;继承是代码复用的核心基石&#xff0c;而多态是程序高扩展性的灵魂。熟练掌握继承语法、方法重写、父子类类型转换&#xff0c;再结合多态思想开发&#xff0c;能彻底告别冗余重复代码&#xff0c;写出结构清晰、易维护、易拓展的Jav…

作者头像 李华
网站建设 2026/5/21 1:00:10

Inter字体终极指南:从零开始掌握现代界面设计的免费开源字体方案

Inter字体终极指南&#xff1a;从零开始掌握现代界面设计的免费开源字体方案 【免费下载链接】inter The Inter font family 项目地址: https://gitcode.com/gh_mirrors/in/inter Inter字体是一款专为计算机屏幕精心设计的开源无衬线字体系统&#xff0c;凭借其卓越的可…

作者头像 李华