本节摘要:RAII(资源获取即初始化)把资源的生命周期焊死在栈对象的生命周期上——构造函数获取资源,析构函数释放资源,作用域一结束释放自动发生,异常也拦不住。五法则(析构、拷贝构造、拷贝赋值、移动构造、移动赋值)规定了资源拥有者必须实现的五个特殊成员函数;零法则则提醒你:多数类根本不该手写任何一个是。
阅读完本节,你应当能够:
3.1 节的泄漏代码,RAII 版只需要一个"资源外套":
#include <vector> #include <memory> void process() { auto data = std::make_unique<std::vector<int>>(1000000); if (!do_work(*data)) return; // 提前返回 析构照常执行 释放照常发生 } // 异常同理 栈展开也会析构 data
data 是栈上的智能指针对象,vector 活在堆上;无论函数从哪条路径离开,data 的析构函数都会执行,堆内存随之归还。资源的死期不再依赖程序员的记性,而依赖作用域的几何学——这就是 RAII 的全部,也是 C++ 整个资源管理体系的基石。标准库里的 std::string、容器、智能指针、锁守卫(std::lock_guard)、文件流全是 RAII 的实例。

一个类一旦拥有资源,编译器默认生成的逐成员拷贝就会出事(2.1 节的 BadBuffer 双重释放)。C++11 起的完整清单是五个特殊成员函数:析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值。逐个实现一个不依赖标准库的缓冲类,看它们各自的职责:
#include <algorithm> #include <cstddef> #include <utility> class Buffer { public: explicit Buffer(std::size_t n) : size_(n), data_(new int[n]{}) {} ~Buffer() { delete[] data_; } // 1 析构:释放 Buffer(const Buffer& other) // 2 拷贝构造:深拷贝 : size_(other.size_), data_(new int[other.size_]) { std::copy(other.data_, other.data_ + size_, data_); } Buffer& operator=(const Buffer& other) { // 3 拷贝赋值:先自检再深拷贝 if (this == &other) return *this; int* fresh = new int[other.size_]; // 先分配再释放 防异常中途半残 std::copy(other.data_, other.data_ + other.size_, fresh); delete[] data_; data_ = fresh; size_ = other.size_; return *this; } Buffer(Buffer&& other) noexcept // 4 移动构造:偷资源 : size_(other.size_), data_(other.data_) { other.data_ = nullptr; other.size_ = 0; // 掏空来源 防双释放 } Buffer& operator=(Buffer&& other) noexcept { // 5 移动赋值:换资源 if (this == &other) return *this; delete[] data_; data_ = other.data_; size_ = other.size_; other.data_ = nullptr; other.size_ = 0; return *this; } private: std::size_t size_; int* data_; };
五个函数各守一关:析构管死、拷贝两兄弟管"复制出独立新主"、移动两兄弟管"资源过户、原主清零"。注意移动版本标了 noexcept——这不是装饰,std::vector 扩容时若移动构造可能抛异常,它会退回拷贝以保证异常安全(3.6 节解释缘由),白白损失性能。
法则的用法其实是三条:
= default 与 = delete 是配套的开关:前者"显式要默认实现"(例如给移动构造补一个),后者"明确禁止"(Buffer(const Buffer&) = delete; 一行禁拷贝,比 private 化的老办法直白得多)。
RAII 的"资源"是广义的——凡"获取、使用、释放"三段式的东西都算。锁是最典型的例子:
#include <mutex> std::mutex g_mtx; void old_style() { g_mtx.lock(); if (!ready()) { g_mtx.unlock(); return; } // 每个出口都要记得解锁 do_work(); // do_work 抛异常 死锁 g_mtx.unlock(); } void raii_style() { std::lock_guard<std::mutex> lock(g_mtx); // 构造即加锁 if (!ready()) return; // 任意出口自动解锁 do_work(); // 异常也自动解锁 } // 析构即解锁
老写法每个出口手工配对,一个漏网就是死锁;RAII 写法把"解锁"焊在作用域上,出错免费。文件流(std::fstream 析构自动关闭)、事务对象(析构自动回滚未提交的改动)、作用域计时器,全是同一模式的变体。识别 RAII 机会的嗅觉:凡是代码评审时问出"这条路走得到释放吗"的地方,都值得一个 RAII 套子。
一个纪律问题:构造函数里尽量别做可能抛异常之外的"半途而废"。RAII 依赖"构造完成 = 资源到手"的约定;若构造函数中途抛异常,已构造的成员会正确析构,但对象本身从未活过。两阶段构造(先空对象再 init)是反模式——它打破 RAII 契约,把"是否有效"的检查负担推给每个使用者。
💡 关键直觉:五法则不是五个需要背诵的语法点,而是对"资源所有权"这个概念的完整法律:谁能复制主(拷贝)、谁能过户主(移动)、主死谁收尸(析构)。零法则则说:多数类只该当房客(用现成管理类),别去当房东。
下一节看标准库给你的现成答案:三种智能指针如何把所有权做成类型系统里的名词。