raii是c++中资源管理的硬约束,要求资源生命周期严格绑定对象生命周期,通过构造函数获取、析构函数释放,确保异常安全;应优先使用std::unique_ptr、std::shared_ptr、std::scoped_lock等标准raii设施,禁用裸指针和手动配对操作。

RAII不是语法糖,是对象生命周期和资源绑定的硬约束
RAII(Resource Acquisition Is Initialization)在C++里不是可选技巧,而是唯一靠谱的资源管理方式。它靠的是构造函数获取资源、析构函数释放资源,把“资源生存期”死死绑在“对象生存期”上——只要对象被销毁(无论是正常离开作用域、异常退出,还是delete),资源就一定被清理。
常见错误现象:new配delete但漏写、fopen后忘记fclose、智能指针没用或乱用裸指针传参导致提前释放。这些都不是“写得不够小心”,而是违背了RAII原则的必然结果。
- 所有需要手动释放的资源(文件句柄、内存、互斥锁、socket、数据库连接)都该封装成类,且禁止提供公开的释放接口
- 构造函数必须成功获取资源,失败就抛异常(不能返回错误码);否则对象处于“半构建”状态,析构函数无法安全运行
- 禁止在类内保存裸指针指向外部分配的资源——那等于把RAII的链子剪断了
std::unique_ptr和std::shared_ptr已经覆盖90%场景
别自己写RAII包装类,除非有特殊需求(比如需要自定义释放逻辑或非堆内存管理)。std::unique_ptr和std::shared_ptr是标准库提供的成熟方案,它们的析构函数已确保调用delete或自定义deleter。
使用场景:动态内存几乎全用std::unique_ptr;跨作用域共享所有权才考虑std::shared_ptr;绝不用std::auto_ptr(已废弃)。
立即学习“C++免费学习笔记(深入)”;
-
std::unique_ptr移动语义安全,不可拷贝,避免误传导致悬空指针 - 用
std::make_unique<t>()</t>构造,不直接写new T()——前者异常安全,后者可能内存泄漏 - 对C风格资源(如
FILE*),用自定义deleter:std::unique_ptr<file int> fp(fopen("x.txt", "r"), &fclose);</file>
std::lock_guard和std::scoped_lock解决锁资源管理
互斥锁是最典型的易出错RAII场景:忘了unlock()会导致死锁,异常路径下尤其危险。C++17前用std::lock_guard,C++17起优先用std::scoped_lock(支持多锁、更安全)。
错误现象:mutex.lock(); do_work(); mutex.unlock();——一旦do_work()抛异常,unlock()永远不执行。
-
std::lock_guard<:mutex> lock(mtx);</:mutex>:构造即加锁,析构即解锁,无条件保证 -
std::scoped_lock<:mutex std::mutex> lock(mtx1, mtx2);</:mutex>:自动按地址顺序加锁,避免死锁,且支持多个互斥量 - 不要用
std::unique_lock除非真需要延迟加锁、手动解锁或转移所有权——它比scoped_lock重,也更容易误用
自定义RAII类最容易踩的三个坑
写自己的RAII类时,问题往往不出在逻辑,而出在细节:拷贝/移动语义、异常安全、资源唯一性。
典型错误:MyFile f1("a.txt"); MyFile f2 = f1; 导致两个对象析构时都试图close()同一个fd——崩溃。
- 禁用拷贝(
MyFile(const MyFile&) = delete;),只允许移动(实现MyFile(MyFile&&)并置空原对象的资源句柄) - 构造函数中任何可能抛异常的操作(如
open()失败)必须在资源真正获得前发生;否则析构函数会尝试释放无效句柄 - 如果资源本身支持“重复关闭不报错”(如
fclose(nullptr)安全),不代表你可以省略置空操作——它掩盖了逻辑错误,且在其他平台可能不成立
RAII真正的复杂点不在写法,而在“资源边界”的识别:一个std::vector<int></int>是不是RAII?是,但它只管堆内存;它不管里面int是否代表文件描述符。这种隐式资源最容易被忽略。