目录
[std::vector 的析构函数](#std::vector 的析构函数)
[std::unique_ptr 的自定义删除器](#std::unique_ptr 的自定义删除器)
[五、noexcept 关键字的作用](#五、noexcept 关键字的作用)
[误区3:认为可以用 noexcept(false) 绕开规则](#误区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 代码的基石。