一、RAII定义
-
RAII(Resource Acquisition Is Initialization)资源获取即初始化,是C++中唯一且核心的资源管理范式。
-
RAII的设计思路是将资源的生命周期与栈上的对象的生命周期作用域进行强绑定,利用C++编译器保证确定性析构,让资源管理自动化
-
就是让栈对象配合资源释放,无论是正常返回,还是中途抛出异常,栈对象一定会销毁,对应资源也一定会释放。
二、RAII工作原理
-
1、在对象构造函数中获取资源(new内存,open文件,lock互斥量)
-
2、将资源句柄存储在对象的成员变量中。
-
3、在对象析构函数中释放资源。
关键核心:
-
保证一旦栈对象离开作用域,其析构函数必然会被调用,这种确定性是RAII威力的根基。作用域结束,调用析构函数,从而释放资源。
代码:
class TextureGuard {
private:
GLuint m_textureID; // 绑定:存储纹理 ID
public:
// 获取资源
TextureGuard() : m_textureID(0) {
glGenTextures(1, &m_textureID); // OpenGL 生成纹理 ID
// 绑定
}
// 释放资源
~TextureGuard() {
if (m_textureID != 0) {
glDeleteTextures(1, &m_textureID); // 归还显存
m_textureID = 0;
}
}
// 同样要删除拷贝构造,增加移动构造...
};
三、RAII强大的三大根基
1、异常安全
-
在没有RAII的C语言或手动管理内存的代码中,如果在new之后,delete之前发生了异常,执行流会直接跳转,delete被跳过,导致内存泄漏。
-
但RAII对象在栈上,异常发生时,C++的栈展开机制会保证所有已构造的栈对象被析构。(局部变量在栈内存中)
-
比如std::locak_guard<std::mutex>,即使临界区抛出异常,锁mutex也保证能被释放,杜绝死锁问题
2、确定性资源释放,实时性能的保障
-
与该有GC(垃圾回收)的语言不同,GC回收时机不可控(可能卡顿),而RAII的释放时机精确锁定在对象离开作用域的那一刻。
-
对于内存池、数据库连接池、文件句柄等紧缺资源,这种确定性能让系统资源占用可预测,是构建低延迟、高并发系统的基石。
3、极简的代码表现力
-
一旦业务代码采用RAII,基本看不到delete,close,unlock这些方法,资源清理操作被封装在析构函数中,业务逻辑干净纯粹,根除了因忘记释放导致的资源泄漏问题。
四、RAII是一种资源管理的通用范式
-
对资源的获取-释放操作制定了统一的处理思路,智能指针就是RAII思想在堆内存管理上的具象化工具。
五、RAII思想的实战使用
-
强制使用std::unique_ptr,shared_ptr,管理堆对象,确保内存零泄漏。
-
对于特殊系统资源(句柄,OpenGl纹理ID),绝不裸存,而是封装成RAII包装类,在构造时Create,析构时Release。
-
利用lock_guard保障锁安全,利用unique_lock配合条件变量,利用C11的unique_ptr自定义Deleter,写一次性回调,或者利用shared_ptr配合Deleter实现复杂的生命周期管理。
六、RAII的使用注意点
1、RAII的析构函数绝对不能抛出异常
-
如果析构函数抛出异常,且处于另一个异常导致的栈展开过程中,C++会调用std::terminate函数,直接终止进程。
-
所以RAII析构函数必须使用noexcept或内部catch所有异常,确保不往外抛异常。
2、unique_ptr如何使用RAII移动语义,而不引起双重释放
-
unique_ptr禁用了拷贝构造函数和拷贝赋值运算符,但提供了移动构造函数。
-
move移动时,源unique_ptr内部的裸指针会被置为nullptr,析构时只释放非空指针。
-
保证了资源的所有权唯一,且明确转移,不会出现两个对象同时管理同一资源导致双重释放问题。
代码:
#include <windows.h>
#include <iostream>
#include <stdexcept> // 用于异常
// ==============================================
// 白银级 RAII 封装:Windows 文件句柄守卫类
// ==============================================
class FileGuard {
private:
HANDLE m_handle; // 【绑定】资源句柄存储在成员变量中
public:
// ---------- 1. 获取资源(Acquisition) ----------
// 显式构造函数:在对象创建时立即打开文件
explicit FileGuard(const char* filePath)
: m_handle(INVALID_HANDLE_VALUE) // 先初始化为无效值
{
// 调用 Windows API 获取系统资源(文件句柄)
m_handle = CreateFileA(
filePath, // 文件名
GENERIC_READ, // 访问权限
FILE_SHARE_READ, // 共享模式
nullptr, // 安全属性
OPEN_EXISTING, // 必须存在
FILE_ATTRIBUTE_NORMAL, // 普通文件
nullptr // 模板文件
);
// 检查资源是否获取成功
if (m_handle == INVALID_HANDLE_VALUE) {
// 获取失败时,抛出异常(构造失败,对象不会被创建)
throw std::runtime_error("文件打开失败!");
}
// 【绑定】此时资源句柄已经成功存入成员变量 m_handle
std::cout << "[获取] 文件句柄已打开,句柄值: " << m_handle << std::endl;
}
// ---------- 2. 释放资源(Release) ----------
// 析构函数:对象离开作用域时,由编译器自动调用
~FileGuard() {
// 只有句柄有效时才释放
if (m_handle != INVALID_HANDLE_VALUE && m_handle != nullptr) {
CloseHandle(m_handle); // 调用 Windows API 归还资源
std::cout << "[释放] 文件句柄已安全关闭,句柄值: " << m_handle << std::endl;
m_handle = INVALID_HANDLE_VALUE; // 防御性置空
}
}
// ---------- 3. 禁止拷贝(防双重释放) ----------
// 拷贝构造和拷贝赋值必须删除!否则两个对象指向同一个句柄,析构两次会崩溃
FileGuard(const FileGuard&) = delete;
FileGuard& operator=(const FileGuard&) = delete;
// ---------- 4. 允许移动(转移所有权) ----------
// 移动构造:让资源所有权从一个对象转移到另一个对象
FileGuard(FileGuard&& other) noexcept
: m_handle(other.m_handle)
{
// 源对象交出所有权,置为无效,防止其析构时释放我们的资源
other.m_handle = INVALID_HANDLE_VALUE;
}
FileGuard& operator=(FileGuard&& other) noexcept {
if (this != &other) {
// 先释放自己手上的老资源(如果有)
if (m_handle != INVALID_HANDLE_VALUE && m_handle != nullptr) {
CloseHandle(m_handle);
}
// 接收对方资源,并置空对方
m_handle = other.m_handle;
other.m_handle = INVALID_HANDLE_VALUE;
}
return *this;
}
// 提供获取底层句柄的方法(方便调用 ReadFile 等)
HANDLE get() const { return m_handle; }
};


被折叠的 条评论
为什么被折叠?



