3.2 RAII与五法则


3.2 RAII 与五法则

本节摘要:RAII(资源获取即初始化)把资源的生命周期焊死在栈对象的生命周期上——构造函数获取资源,析构函数释放资源,作用域一结束释放自动发生,异常也拦不住。五法则(析构、拷贝构造、拷贝赋值、移动构造、移动赋值)规定了资源拥有者必须实现的五个特殊成员函数;零法则则提醒你:多数类根本不该手写任何一个是。

学习目标

阅读完本节,你应当能够:

  1. 用 RAII 模式封装任意资源(内存、文件、锁);
  2. 完整实现一个满足五法则的资源管理类;
  3. 说出"零、三、五"法则各自的适用边界;
  4. 解释 =default 与 = delete 的工程用途。

一、先看 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 的实例。

图:RAII 对象的一生:三进三出都指向同一个析构

图: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 节解释缘由),白白损失性能。

法则的用法其实是三条

  • 零法则(默认首选):类不直接拥有资源,全部成员用现成 RAII 类型(string、vector、智能指针),五个函数一个都不写,编译器生成的默认行为天然正确。业务代码里九成以上的类应该走这条。
  • 三法则:管理裸资源且不支持移动(或还在 C++98),写析构加拷贝两件套。
  • 五法则:真要写资源管理类,五个一起写,缺一个就留洞——比如只写移动不写拷贝,拷贝会被编译器删除(这是有意的:能移动的资源类通常不该隐式拷贝)。

= default= delete 是配套的开关:前者"显式要默认实现"(例如给移动构造补一个),后者"明确禁止"(Buffer(const Buffer&) = delete; 一行禁拷贝,比 private 化的老办法直白得多)。

三、RAII 不止管内存

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 契约,把"是否有效"的检查负担推给每个使用者。

💡 关键直觉:五法则不是五个需要背诵的语法点,而是对"资源所有权"这个概念的完整法律:谁能复制主(拷贝)、谁能过户主(移动)、主死谁收尸(析构)。零法则则说:多数类只该当房客(用现成管理类),别去当房东。

本节要点回顾

  • RAII 即几何学:资源释放绑定作用域结束,正常返回、提前返回、异常逃逸三路同归。
  • 五个特殊成员各司其职:析构收尸、拷贝复制、移动过户;移动构造记得 noexcept。
  • 零法则优先:成员用现成 RAII 类型,五个函数全免;真写管理类才升级到五法则。
  • 资源是广义的:锁、文件、事务同享一套模式;lock_guard 是 RAII 的第二经典案例。
  • =default 与 =delete:显式声明默认行为或明确禁止,取代 private 化等老技巧。

下一节看标准库给你的现成答案:三种智能指针如何把所有权做成类型系统里的名词。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U