Skip to content

RAII:资源获取即初始化 ​

RAII 是 C++ 最核心的资源管理哲学,也是它与「带 GC 的语言」最本质的分野之一。理解 RAII 之后,智能指针、lock_guard、容器、fstream、jthread 这些看似零散的东西会瞬间串成一条线。

一、RAII 是什么 ​

RAII = Resource Acquisition Is Initialization(资源获取即初始化)。

一句话本质:把「资源」的生命周期绑定到一个栈对象的生命周期上——在构造函数里获取资源,在析构函数里释放资源,于是"资源随对象生、随对象亡"由编译器自动保证,程序员不再需要手写配对的开/关、加锁/解锁、new/delete。

这里的"资源"是广义的:堆内存、文件句柄、互斥锁、socket、数据库连接、GDI/窗口句柄、临时文件、事务锁……任何**"必须成对使用、用完要归还"**的东西都算。

更准确的叫法其实是 Scope-Bound Resource Management(作用域绑定的资源管理):名字里的 "Is Initialization" 强调的是"获取动作发生在初始化(构造)阶段",而真正保证安全的是作用域结束时自动析构。名字是历史包袱,不必抠字面。

核心机制只有两点:

  1. 构造函数获取资源,构造失败就抛异常(对象没建成,也就不会析构,不留半成品)。
  2. 析构函数释放资源;由于 C++ 保证栈对象在离开作用域时一定被析构,无论控制流怎么走,资源都会被回收。

二、为什么 RAII 能成立(语言层面的保证) ​

  • 栈对象的析构是确定的:离开作用域(正常结束、return、break/continue、goto 跳出)都会调用析构函数。
  • 异常也会触发析构(stack unwinding / 栈展开):抛出异常沿调用栈向上传播时,每一层已构造完成的栈对象都会依次析构 → 资源在异常路径上同样被释放。这是 RAII 相对手写 try/finally 的最大优势。
  • 确定性析构:与 GC 不同,C++ 析构时机可预测(作用域结束/对象销毁时),不依赖垃圾回收器"有空时"才跑。
cpp
void f() {
    std::ifstream in("data.txt");      // 构造:打开文件(获取资源)
    std::string line;
    while (std::getline(in, line)) {
        if (bad(line)) throw std::runtime_error("err"); // 抛异常
        // ...
    }
}   // 无论正常返回还是异常传播,in 都会析构 → 文件自动关闭

三、怎么写一个 RAII 类(设计要点) ​

  1. 构造即获取:资源在构造函数中拿到;拿不到就抛异常,绝不让对象处于"半初始化"状态。
  2. 析构即释放:析构函数负责释放,且不要抛异常(析构抛异常在栈展开中会触发 std::terminate),一般标 noexcept。
  3. 定所有权:要么禁止拷贝(独占),要么用移动语义转移所有权,要么用引用计数共享。避免"两个对象都以为自己拥有同一份资源"。
  4. 遵守三/五/零法则:一旦自定义了析构(说明要手动管理资源),通常也要处理拷贝/移动(Rule of Five);能用成员自带的 RAII 类型拼装、自己不写析构最好(Rule of Zero)。
cpp
class FileHandle {
public:
    explicit FileHandle(const char* path, const char* mode = "r")
        : fp_(std::fopen(path, mode)) {
        if (!fp_) throw std::runtime_error("open failed");   // 构造失败即抛
    }
    ~FileHandle() noexcept { if (fp_) std::fclose(fp_); }     // 析构释放,不抛

    FileHandle(const FileHandle&) = delete;                   // 禁拷贝(独占所有权)
    FileHandle& operator=(const FileHandle&) = delete;
    FileHandle(FileHandle&& o) noexcept : fp_(o.fp_) { o.fp_ = nullptr; } // 移动转移
    FileHandle& operator=(FileHandle&& o) noexcept {
        if (this != &o) { if (fp_) std::fclose(fp_); fp_ = o.fp_; o.fp_ = nullptr; }
        return *this;
    }

    std::FILE* get() const noexcept { return fp_; }
private:
    std::FILE* fp_ = nullptr;
};

四、体现了 RAII 思想的东西(清单) ​

判断标准:一个类型若「构造时拿到某种需要归还的资源、析构时自动归还、且把这件事交给对象生命周期」,它就是 RAII。下面这些几乎覆盖了标准库的全部常用 RAII 设施。

类别典型类型管理的资源
内存std::unique_ptr / shared_ptr / weak_ptr堆对象(new 出来的内存)
互斥/同步std::lock_guard / unique_lock / scoped_lock / shared_lockmutex / shared_mutex 的持有权
容器std::vector / string / map / unordered_map / deque …内部动态内存
文件 / 流std::fstream / ifstream / ofstream / stringstream文件描述符 / 缓冲
线程std::jthread(C++20)线程的 join 与停止
作用域守卫scope_exit / scope_fail / scope_success(C++23 及 gsl::finally)任意"离开作用域要做的清理"
分配器/内存资源std::allocator、std::pmr::memory_resource、pmr::monotonic_buffer_resource内存池 / 分配策略
智能指针 + 自定义删除器unique_ptr<T, D> 配 D任意句柄(FILE*、socket、dlopen 句柄、C 库资源)

逐个展开几个关键点:

  • 智能指针:unique_ptr 独占、shared_ptr 引用计数、weak_ptr 打破循环引用。它们把"手动 delete"变成"析构自动 delete",是 RAII 最广为人知的形态。
  • std::lock_guard / scoped_lock:构造即加锁、析构即解锁,彻底消灭"异常路径忘记 unlock"这类死锁/泄漏。unique_lock 更灵活(可延迟加锁、手动解锁、配合条件变量),但所有权思想一致。C++17 的 std::scoped_lock 还能一次锁多个 mutex 并避免死锁。
  • STL 容器:vector/string 等自己管理内部堆内存,拷贝/析构自动处理,是"隐形的 RAII"。
  • std::fstream:构造关联文件、析构关闭文件;不需要手写 fclose。
  • std::jthread(C++20)才是真 RAII:析构时自动 join(并请求 stop_token 停止)。注意 std::thread 不是 RAII——它析构时若仍可 join 会直接 std::terminate,必须手动 join/detach。
  • 自定义删除器的 unique_ptr:可以用统一的所有权语义去管任意 C 风格资源,是"给非 RAII 资源套一层 RAII"的通用套路。
cpp
// 给任意 C 资源套 RAII
struct FcloseDeleter { void operator()(std::FILE* f) const noexcept { if (f) std::fclose(f); } };
using FilePtr = std::unique_ptr<std::FILE, FcloseDeleter>;
FilePtr fp(std::fopen("a.txt", "r"));   // 离开作用域自动 fclose

// 作用域守卫:离开作用域执行任意清理(C++23 / gsl::finally)
auto guard = gsl::finally([]{ /* cleanup */ });

五、RAII 与异常安全 ​

RAII 是异常安全(exception safety)的基石:只有资源被对象托管,异常在栈展开时才不会漏释放。三个保证等级:

  • 基本保证:不泄漏资源、对象处于有效(可能被改过)状态。
  • 强保证:操作要么成功,要么回滚到操作前状态(常用 copy-and-swap:先在副本上改,成功后 swap)。
  • 不抛保证(nothrow):如 swap、移动操作,绝不失败。

lock_guard、智能指针正是靠 RAII 让"基本保证"几乎免费获得。

六、RAII + 移动语义 = 所有权转移 ​

C++11 之后,RAII 类型通过移动构造/移动赋值表达"所有权转移",于是资源能在对象间安全搬家而不复制:

  • unique_ptr 只可移动不可拷贝;
  • 把 unique_ptr 放进 vector、从函数返回、push_back(std::move(p)),底层资源指针被"搬运"而非复制。

Rule of Zero:优先用成员自带的 RAII 类型(智能指针、容器),这样你既不用写析构,也不用写拷贝/移动——编译器默认行为就是正确的。

七、RAII vs 垃圾回收(GC) ​

维度RAII(C++)GC(Java/Go/…)
回收时机确定(作用域结束/析构)不确定(GC 触发时)
管什么资源内存 + 文件/锁/socket/句柄等一切资源主要只管内存
开销析构调用,通常极低需要 GC 运行时,可能有 STW
异常安全栈展开自动释放依赖 try/finally / defer
循环引用shared_ptr 需 weak_ptr 打破GC 可自动回收

一句话:RAII 管的是"资源",GC 管的是"内存"——这是 C++ 不需要 GC(也不需要 try/finally 清理)的根本原因。

八、反面教材与常见坑 ​

  • 裸 new/delete:异常/提前 return 路径容易漏 delete;应改用智能指针。
  • 忘记 unlock / fclose:手写配对操作,在异常路径必然出问题。
  • 析构抛异常:栈展开途中析构抛异常 → std::terminate。析构应 noexcept 并吞掉/记录错误。
  • 误以为 std::thread 是 RAII:它析构不 join 就 terminate,真正的 RAII 是 std::jthread。
  • shared_ptr 循环引用:互相持有导致引用计数永不归零、内存泄漏,需用 weak_ptr 破环。
  • 自定义类型拥有资源却不处理拷贝:默认浅拷贝会让两个对象都去释放同一资源 → 双重释放。

九、高频问答 ​

Q1. 什么是 RAII?为什么 C++ 需要它?

答案

RAII(资源获取即初始化)用栈对象的生命周期管理资源:构造获取、析构释放。因为 C++ 保证栈对象离开作用域(含异常栈展开)一定析构,所以资源确定性地、在异常路径也能被释放,从而无需 try/finally,也无需 GC 来回收非内存资源。

Q2. RAII 里"资源"包括哪些?

答案

一切"用完必须归还"的东西:堆内存、文件描述符、互斥锁、socket、数据库连接、图形句柄、临时文件、事务等。关键词是成对操作 + 需要释放。

Q3. 有哪些东西体现了 RAII 思想?

答案

unique_ptr/shared_ptr(内存)、lock_guard/unique_lock/scoped_lock/shared_lock(锁)、vector/string/map 等容器(堆内存)、fstream(文件)、jthread(线程 join)、scope_exit(作用域守卫)、allocator/pmr(内存池)、以及"unique_ptr + 自定义删除器"包装任意 C 资源。反例:std::thread 不是 RAII。

Q4. 写一个 RAII 类要注意什么?

答案

① 构造获取、失败即抛;② 析构释放且不抛异常(noexcept);③ 明确所有权(禁拷贝或用移动语义);④ 遵守三/五/零法则;⑤ 优先用已有的 RAII 成员组合(Rule of Zero),能不自写析构就不写。

Q5. RAII 和异常安全什么关系?

答案

RAII 是异常安全的基础:异常栈展开时逐层析构栈对象,资源自动释放,满足"基本保证"。要"强保证"常用 copy-and-swap(先改副本、成功再 swap),要"不抛保证"则让移动/swap 为 noexcept。

Q6. std::thread 是 RAII 吗?jthread 呢?

答案

std::thread 不是:析构时若线程仍可 join 会直接 std::terminate,必须手动 join/detach。std::jthread(C++20)是:析构时自动 join,并先对 stop_token 请求停止,是名正言顺的 RAII 线程封装。

Q7. RAII 和 GC 的区别?为什么 C++ 不用 GC 也能安全?

答案

RAII 提供确定性析构,且能管理内存之外的一切资源;GC 回收时机不确定且通常只管内存。C++ 靠"栈对象一定析构 + 智能指针/容器"就实现了自动且确定的资源回收,因此对绝大多数场景无需 GC。

相关 ​

糌粑的面经笔记 · VitePress 版