谈一谈 RAII 技术

RAII--对象生命周期结束自动释放资源

RAII 是一种编程技术或编程思想,它把资源的生命周期绑定到一个对象上,这里的资源指的是分配的堆内存,执行线程,打开的套接字,打开的文件,锁定的互斥锁,磁盘空间,数据库连接等有限供应的一些东西,这些资源必须是申请获得后才能使用。资源请求即初始化,可以这么理解,在对象的初始化的时候请求资源,从具体实现的角度来讲就是指在对象构造的时候去申请资源。从编码的角度来讲,一种可行的实现就是在类的构造函数中去申请资源。

RAII 除了要保证对象能够被访问的时候资源必须是可以使用的,还必须保证所有资源在其控制对象的生存期结束时释放资源,资源释放的顺序是和资源获取的顺序是相反的。如果资源获取失败(比如构造函数异常退出),则每个完全构造的成员和基础子对象获取的所有资源将以和初始化顺序相反的顺序释放。和初始化顺序相反的顺序释放资源,这个和析构函数的析构过程是一样的。RAI I利用了 C++ 核心语言功能(对象生存期,范围退出,初始化顺序和堆栈展开)来消除资源泄漏并确保异常安全。从实现角度来看,标准库的 RAII 类通常是在其析构函数中释放资源。

C++保 证了所有栈对象在生命周期结束时会被销毁(即调用析构函数),所以该代码是异常安全的。

也就是说我们通过 RAII 封装的类,如果用这个类创建局部对象(切记不是堆对象),当它离开作用域之后(不管是正常结束离开作用域,还是发生异常导致的离开作用域,皆可),会自动调用析构函数。但如果你是用堆内存创建的话,你就得手动 delete(暂时不考虑指针),也就无法体现“对象生命周期结束自动释放资源”。为什么我们通常选择在析构函数释放资源,因为局部对象会在离开作用域之后自动调用析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Student {
public:
Student() = default;
~Student() {
std::cout << "Student destructor" << std::endl;
}
};

void Test() {
Student s1;
{
Student s2;
}
}

/*
调用 Test

Student destructor
Student destructor
*/

当一个函数需要通过多个局部变量来管理资源时,RAII就显得非常好用。因为只有被构造成功(构造函数没有抛出异常)的对象才会在返回时调用析构函数,同时析构函数的调用顺序恰好是它们构造顺序的反序,这样既可以保证多个资源(对象)的正确释放,又能满足多个资源之间的依赖关系。

1
2
3
4
5
6
7
8
9
10
11
template <class Mutex> class lock_guard {
private:
Mutex& mutex_;

public:
lock_guard(Mutex& mutex) : mutex_(mutex) { mutex_.lock(); }
~lock_guard() { mutex_.unlock(); }

lock_guard(lock_guard const&) = delete;
lock_guard& operator=(lock_guard const&) = delete;
};

四条性质

  1. 资源获取即初始化:资源(如内存、文件句柄、数据库连接等)在对象的构造函数中获得,而资源的释放则在对象的析构函数中进行。这意味着当一个对象被创建时,它就获取了某种资源,当对象销毁时,资源被自动释放。
  2. 自动管理资源生命周期:RAII 保证了对象的生命周期与资源的生命周期严格绑定。即当对象离开作用域时,析构函数会自动释放其所持有的资源,无需显式地调用释放函数,避免了资源泄漏的问题。
  3. 异常安全性:RAII 使得资源的管理具备异常安全性。如果在对象的构造过程中抛出异常,则对象的析构函数不会被调用,资源会自动回收,从而防止了资源泄漏。
  4. 明确的资源所有权:RAII 模式通过对象的生命周期明确资源的所有权,避免了多方竞争资源或资源的重复释放等问题。每个资源通常由一个对象独立管理,该对象是资源的唯一所有者。

不足

RAII 不是万能的技术,在某些资源需要非常精细控制的情况下,依然需要手动管理。必须提早(对象生命周期结束前)释放的东西你就别用 RAII。一定要在用完之后立即释放资源,不想等作用域结束,那只有纯手工操作。

RAII的作用是帮助处理发生异常时的资源释放问题以及避免程序员忘记释放资源

参考链接

【RAII】RAII 技术(内存安全解决技术/自动化解锁技术)

RAII

C++ RAII特性及其应用