37 / 80 · C++14 · 约 10 分钟
RAII:把清理责任交给对象
RAII 把资源的获取与释放封装进拥有型对象,用确定的析构时机处理正常返回和异常展开。关键不是“资源必须在栈上”,而是每份资源有明确拥有者,构造失败不泄漏,析构不把失败继续向外抛。
清理应该属于一个对象
内存、锁、文件句柄和事务都可能需要成对操作。把释放写在函数末尾会漏掉中途 return 和异常路径。RAII 让拥有型对象在建立有效状态时接管资源,在析构时释放;正常离开作用域和异常栈展开都能沿同一套规则完成清理。
RAII 对象本身也可以是成员或动态对象,不限于局部栈变量。关键是最终拥有者会被正确销毁。标准容器管理元素内存,unique_ptr 管理动态对象,lock_guard 管理锁的持有期;优先组合它们,而不是每个函数手写清理分支。
构造失败时谁负责回收
若非委托构造函数抛出,完整对象的析构函数不会执行,但已完成构造的成员和基类会清理。若委托构造的目标已成功完成,而委托构造函数体随后抛出,则会调用该对象的析构函数。因此把资源放进 RAII 成员比裸指针成员更可靠,后续成员初始化失败时,前面获取的资源仍能回收。
工厂函数应返回拥有型结果。示例先用 make_unique 建立一个 Ticket,再进行可能失败的校验。异常发生时局部 unique_ptr 自动删除 Ticket;成功时返回值把所有权交给调用者,任何时刻都没有需要别人猜测责任的裸资源。
析构负责收尾而非报告主错误
析构函数应尽量不失败,尤其不要在已有异常展开时再抛异常,否则程序可能终止。若关闭文件或提交事务必须向用户报告失败,应提供显式操作;析构只做不抛异常的兜底清理,不把关键业务结果隐藏在无法处理的阶段。
示例用存活计数观察失败与成功两条路径,计数只用于单线程教学,不是生产分配器。RAII 保证的是对象被正常销毁时的释放;强制终止进程、断电等并不遵循一般栈展开规则,持久化与崩溃恢复仍需独立设计。
容易答错的地方
- 构造函数中先获取裸资源再执行可抛操作,不能依赖本类析构回收未成功构造的对象。
- 调用 unique_ptr::release 只交出指针,不会释放资源;若没有新拥有者接管,就破坏了 RAII。
运行一个例子
最低标准 C++14 · 完整程序 · 下载 .cpp
#include <cassert>
#include <memory>
#include <stdexcept>
struct Ticket {
static int alive;
Ticket() { ++alive; }
~Ticket() noexcept { --alive; }
Ticket(const Ticket&) = delete;
Ticket& operator=(const Ticket&) = delete;
};
int Ticket::alive = 0;
std::unique_ptr<Ticket> make_ticket(bool valid) {
auto result = std::make_unique<Ticket>();
if (!valid) throw std::invalid_argument("invalid ticket");
return result;
}
int main() {
bool caught = false;
try { auto ticket = make_ticket(false); }
catch (const std::invalid_argument&) { caught = true; }
assert(caught && Ticket::alive == 0);
{
auto ticket = make_ticket(true);
assert(Ticket::alive == 1);
}
assert(Ticket::alive == 0);
}
在本地编译
g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread memory-raii.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
如果把 make_ticket 中的 make_unique 改成裸 new,校验失败前要补 delete 才安全。更好的修复是什么?
查看参考答案
保留或立即建立 unique_ptr 拥有者,让每条退出路径共享同一个析构清理机制。不要增加一个只覆盖当前异常的 delete 分支,因为后续增加新的可抛操作又会产生漏洞。成功时直接返回 unique_ptr,调用者得到清楚的独占所有权。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。