16. 智能指针与异常安全:异常发生时如何保证资源不泄漏?
异常安全,说白了就是你的代码在抛出异常时,资源不会「人间蒸发」。
我见过太多项目,平时跑得稳稳当当,一遇到异常就崩得稀里哗啦。原因很简单——资源没处理好。今天我们就来聊聊,智能指针怎么帮我们解决这个老大难问题。
异常安全的三个级别
在深入智能指针之前,我们先明确一下异常安全的三个级别。我个人习惯把它们分成这样:
| 级别 | 含义 | 实际表现 |
|---|---|---|
| 基本保证 | 异常发生后,资源不泄漏,对象处于有效但不确定的状态 | 程序还能继续跑,但数据可能变了 |
| 强保证 | 操作要么完全成功,要么回滚到操作前的状态 | 像数据库事务一样,失败就当没发生过 |
| 不抛异常 | 保证函数永远不会抛出异常 | 析构函数、swap 操作等 |
嗯,这里要注意:智能指针主要帮我们实现「基本保证」——确保异常发生时,堆内存能被正确释放。
裸指针的噩梦
先看一个反面教材。我在代码审查时经常看到这种写法:
void processData() {
Widget* p = new Widget();
// ... 一些操作,可能抛出异常
delete p; // 如果上面抛异常,这行永远不会执行
}
为什么会这样?因为 C++ 的异常处理机制不会自动帮你调用 delete。一旦 new 和 delete 之间出现异常,内存就泄漏了。
你想想看,如果这个函数被调用一万次,每次泄漏一点点内存……嗯,后果不用我多说。
智能指针的救赎
用 std::unique_ptr 改写一下:
void processData() {
auto p = std::make_unique<Widget>();
// ... 一些操作,可能抛出异常
// 离开作用域时,p 的析构函数自动 delete
}
就这么简单。智能指针的析构函数是自动调用的,而析构函数默认是 noexcept。所以无论函数正常返回还是异常退出,unique_ptr 都会释放它管理的资源。
shared_ptr 的异常安全陷阱
我曾经踩过一个坑,分享出来给大家避避雷:
// 危险写法
void process(std::shared_ptr<Widget> p1, std::shared_ptr<Widget> p2);
process(std::shared_ptr<Widget>(new Widget()),
std::shared_ptr<Widget>(new Widget()));
这段代码有什么问题?C++ 标准允许编译器按任意顺序求值。可能的情况是:
new Widget()第一次调用new Widget()第二次调用- 第一次构造
shared_ptr - 第二次构造
shared_ptr
如果第二步抛异常,第一步分配的内存就泄漏了——因为还没绑定到 shared_ptr 上。
std::make_shared 而不是直接 new。
// 安全写法
process(std::make_shared<Widget>(),
std::make_shared<Widget>());
make_shared 把内存分配和对象构造打包成一个原子操作,异常发生时不会出现「半成品」资源。
自定义删除器与异常
有时候我们需要管理非内存资源,比如文件句柄、数据库连接。自定义删除器就派上用场了:
auto fileDeleter = [](FILE* fp) {
if (fp) fclose(fp);
};
void writeData() {
std::unique_ptr<FILE, decltype(fileDeleter)>
fp(fopen("data.txt", "w"), fileDeleter);
// 写入操作,可能抛出异常
if (fprintf(fp.get(), "hello") < 0) {
throw std::runtime_error("write failed");
}
// 离开作用域时自动 fclose
}
你看,不管中间怎么抛异常,文件句柄都会被正确关闭。这就是 RAII 的威力。
异常安全的核心知识体系
下面这张图总结了智能指针与异常安全的核心逻辑:
实战中的避坑指南
我总结了几条实战经验,希望能帮你少走弯路:
- 优先使用
std::make_unique和std::make_shared——不仅异常安全,还少打几个字。 - 不要在构造函数中抛异常——如果构造函数抛异常,对象的析构函数不会被调用。但智能指针管理的资源已经分配了怎么办?用
make_shared就能避免这个问题。 - 注意循环引用——
shared_ptr循环引用会导致内存泄漏。我建议能用unique_ptr就别用shared_ptr。 - 自定义删除器要 noexcept——如果删除器抛异常,而它又在析构函数中被调用,程序会直接 terminate。
总结
异常安全不是锦上添花,而是底线要求。智能指针让我们用很小的代价,就能获得可靠的资源管理能力。
说白了,就是一句话:用 RAII 管理所有资源,用智能指针管理所有堆内存。做到这一点,异常安全的基本保证就有了。
我在项目中见过太多因为裸指针泄漏导致的内存问题,修复起来极其痛苦。养成用智能指针的习惯,能让你省下大量调试时间。
公众号:蓝海资料掘金营,微信deep3321