C++ / a working model

37 / 80   ·   C++14   ·   约 10 分钟

RAII:把清理责任交给对象

先记住这句话

RAII 把资源的获取与释放封装进拥有型对象,用确定的析构时机处理正常返回和异常展开。关键不是“资源必须在栈上”,而是每份资源有明确拥有者,构造失败不泄漏,析构不把失败继续向外抛。

本篇内容
  1. 清理应该属于一个对象
  2. 构造失败时谁负责回收
  3. 析构负责收尾而非报告主错误
  4. 运行示例
  5. 动手练习

清理应该属于一个对象

内存、锁、文件句柄和事务都可能需要成对操作。把释放写在函数末尾会漏掉中途 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,调用者得到清楚的独占所有权。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录