raii类必须显式禁用拷贝,否则会导致双析构和重复释放资源;其核心是将资源生命周期绑定对象生命周期,构造获取资源、析构自动释放,且析构函数须声明为noexcept以保障异常安全。

RAII类必须显式禁用拷贝,否则资源会重复释放
RAII的核心是“资源生命周期绑定对象生命周期”,一旦允许拷贝,两个对象会持有同一份资源指针,析构时必然 double free。C++ 默认生成的拷贝构造函数和拷贝赋值运算符不会做深拷贝或引用计数,直接调用就是灾难。
实操建议:
- 在类定义中明确
delete拷贝相关函数:MyResource(const MyResource&) = delete;<br>MyResource& operator=(const MyResource&) = delete;
- 若需共享资源,改用
std::shared_ptr或手动实现引用计数——但这已不是基础 RAII,而是 RAII 的扩展模式 - 移动语义可选但推荐:加上
MyResource(MyResource&&)和operator=(MyResource&&),并把原对象的资源指针置为nullptr,避免移动后析构误释放
析构函数里不能抛异常,否则可能终止程序
如果 ~MyResource() 中调用的释放函数(比如 fclose()、delete[]、CloseHandle())内部抛出异常,而此时栈正在展开(例如另一个异常已发生),C++ 会直接调用 std::terminate()。
实操建议:
- 所有释放逻辑必须用
noexcept标记析构函数:~MyResource() noexcept { /* 释放代码 */ } - 避免在析构中做任何可能失败的 I/O 或系统调用;如有必要,用错误码代替异常,并记录日志而非抛出
- 像
std::vector、std::unique_ptr这些标准 RAII 类型,析构函数全是noexcept,这是有明确规范依据的
构造函数申请失败时,必须确保不留下半初始化对象
如果构造函数中资源申请失败(如 new 抛 std::bad_alloc、fopen() 返回 nullptr),对象不会被创建,但若你在构造函数里做了多步初始化(比如先 malloc 再 setsockopt),其中间状态可能已污染全局或静态资源。
实操建议:
- 资源申请尽量放在构造函数体最前,且只做**单一、原子性**操作;失败即 throw,不执行后续逻辑
- 避免在构造函数中调用用户可重载的虚函数——此时虚表还未完全建立,行为未定义
- 更安全的做法是把资源申请封装进私有 static 工厂函数,成功才 new 对象,例如:
static std::unique_ptr<myresource> create(const char* path) {<br> auto fp = std::fopen(path, "r");<br> if (!fp) throw std::runtime_error("open failed");<br> return std::unique_ptr<myresource>(new MyResource(fp));<br>}</myresource></myresource>
自定义 RAII 类与 std::unique_ptr 的分工边界在哪
不是所有资源都需要手写 RAII 类。std::unique_ptr 已覆盖绝大多数裸指针场景,但它的定制能力有限:只能换删除器(deleter),不能封装打开/关闭逻辑、状态检查、或多个关联资源的协同释放。
实操建议:
- 用
std::unique_ptr:管理单个动态内存块、简单句柄(如int fd配合 lambda deleter) - 手写 RAII 类:需要封装协议(如 socket + setsockopt + bind)、资源依赖(如 OpenGL texture + buffer)、或带状态的生命周期控制(如 lock_guard 只在作用域内加锁)
- 可组合使用:RAII 类内部用
std::unique_ptr管理子资源,比如一个数据库连接类里用std::unique_ptr<sqlite3_stmt></sqlite3_stmt>管理预编译语句
真正难的从来不是“写个析构函数”,而是判断哪些状态要归零、哪些错误要静默吞掉、哪些资源必须严格按逆序释放——这些细节不会报错,但会在某个高并发或低内存场景下突然崩掉。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











