errorman

不积跬步,无以至千里

导航

RAII

Posted on 2023-03-25 15:16  Clemens  阅读(30)  评论(0编辑  收藏  举报

RAII全称Resource Acquisition Is Initialization,即“资源获取即初始化”。这种技术的核心思想是,通过在对象的构造函数中获取资源,并在对象的析构函数中释放资源,来确保资源的正确管理。

它绑定了在使用前必须获取的资源的生命周期(分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、 磁盘空间、数据库连接——任何有限供应的东西)到对象的生命周期。

RAII保证资源对任何可能访问对象的函数都可用(资源的可用性是类不变量,消除了冗余的运行时测试)。它还保证在控制对象的生命周期结束时,以获取的相反顺序释放所有资源。同样,如果资源获取失败(构造函数因异常而退出),则释放每个完全构造的成员和基础子对象获取的所有资源,释放顺序与初始化顺序相反。这利用了核心语言特性(对象生命周期,作用域退出,初始化顺序和堆栈展开),消除了资源泄漏并保证异常安全。这种技术的另一个名称是范围绑定资源管理(Scope-Bound Resource Management,SBRM),基本用例是由于作用域退出而结束RAII对象的生命周期

RAII可以总结如下:

  • 将每个资源封装到一个类中,其中
    • 构造函数获取资源并建立所有类不变量,或者如果无法完成则抛出异常,
    • 析构函数释放资源并且从不抛出异常;
  • 总是通过 RAII 类的实例使用资源
    • 具有自动存储持续时间或临时生命周期本身,或者
    • 具有受自动或临时对象的生命周期限制的生命周期

Move语义使在对象之间、跨作用域以及线程内外安全地转移资源所有权成为可能,同时保持资源安全。

具有 open()/close()、lock()/unlock() 或 init()/copyFrom()/destroy() 成员函数的类是非 RAII 类的典型示例:

std::mutex m;
void bad() 
{
    m.lock();                    // 获取mutex
    f();                         // 如果f()抛出一个异常,那么mutex永远都不会被释放
    if(!everything_ok()) return; // 提前返回,mutex永远不会被释放
    m.unlock();                  // 如果执行到此语句,释放mutex
}
void good()
{
    std::lock_guard<std::mutex> lk(m); // RAII class: mutex获取即初始化
    f();                               // 如果 f() 抛出一个异常,那么释放mutex
    if(!everything_ok()) return;       // 提前返回,mutex被释放。
                                       // good()正常返回,mutex被释放
}  

 

当定义一个RAII类时,需要在类的构造函数中获取资源,并在析构函数中释放资源。当RAII对象被创建时,资源会被自动获取,当对象被销毁时,资源会被自动释放。这样,即使程序出现异常,也能够保证资源的正确释放。

下面看一个使用RAII技术管理内存的例子:

class MyMemory {
public:
    MyMemory(int size) : m_data(new int[size]) {}
    ~MyMemory() { delete[] m_data; }
private:
    int* m_data;
};
int main() {
    MyMemory mem(100);
    // 使用mem对象管理的内存
    return 0;
}

在这个例子中,我们定义了一个MyMemory类来管理一个动态分配的内存区域。在构造函数中,我们使用new运算符分配了一块内存,并将指针存储在m_data成员变量中。在析构函数中,我们使用delete[]运算符释放了这块内存。

在main函数中,我们创建了一个MyMemory对象,它会在作用域结束时自动被销毁。由于MyMemory对象的析构函数会释放内存,因此我们无需手动释放内存,RAII技术帮助我们避免了内存泄漏的可能性。

c++ standard library

管理自己资源的 C++ 库类遵循 RAII:std::string、std::vector、std::jthread(C++20 起),以及许多其他类在构造函数中获取它们的资源(错误时抛出异常), 在它们的析构函数(永远不会抛出)中释放它们,并且不需要显式清理。

此外,标准库提供了几个 RAII 包装器来管理用户提供的资源:

  • std::unique_ptr 和 std::shared_ptr 管理动态分配的内存,或者使用用户提供的删除器,管理由普通指针表示的任何资源;
  • std::lock_guard、std::unique_lock、std::shared_lock 来管理互斥锁。

 

Notes

RAII 不适用于对使用前未获取资源的管理:CPU 时间、内核和缓存容量、熵池容量、网络带宽、电力消耗、堆栈内存。

 

1.在析构函数中宏释放资源,如果发生异常,那么还会执行析构函数吗?

看一个例子:

class Obj
{
public:
    Obj() {std::cout << "Obj()" << std::endl;}
    ~Obj() { std::cout << "~Obj()" << std::endl; }
};

void foo(int n)
{
    Obj obj;
    if (n == 66) {
        throw "throw an exception";
    }
}

int main()
{
    try {
        foo(67);
        foo(66);
    } catch (const char *s) {
        std::cout << s << std::endl;
    }
    return 0;
}

我们可以看到输出如下:

Obj()
~Obj()
Obj()
~Obj()
throw an exception

也就是说,即使发生异常,析构函数也会被执行。

参考:

https://en.cppreference.com/w/cpp/language/raii