什么是资源获取初始化(RAII)?

什么是资源获取初始化(RAII)?

133778 次浏览

这是一个编程习语,简单地说就是您

  • 将资源封装到类中(类的构造函数通常——但不一定是**——获取资源,其析构函数总是释放它)
  • 通过类的本地实例*使用资源
  • 当对象超出作用域时,资源将自动释放

这保证了在使用资源时无论发生什么,它最终都将被释放(无论是由于正常返回、销毁包含的对象,还是抛出异常)。

这是c++中广泛使用的良好实践,因为除了是处理资源的安全方式外,它还使您的代码更加干净,因为您不需要将错误处理代码与主要功能混合在一起。

* 更新: "local"可以是一个局部变量,也可以是一个类的非静态成员变量。在后一种情况下,成员变量被初始化并与其所有者对象一起销毁。

** 更新2:正如@sbi指出的那样,尽管资源通常是在构造函数内部分配的,但也可以在构造函数外部分配并作为参数传入。

“RAII”代表“资源获取是初始化”,实际上是一个相当错误的称呼,因为它不是资源收购(和对象的初始化),而是资源释放(通过对象的破坏) 但RAII是我们得到的名字,并且一直沿用。< / p >

这个习语的核心特点是在本地,自动对象中封装资源(内存块、打开的文件、解锁的互斥对象、you-name-it),并让该对象的析构函数在对象所属作用域的末尾销毁时释放资源:

{
raii obj(acquire_resource());
// ...
} // obj's dtor will call release_resource()

当然,对象并不总是本地自动对象。他们也可以是一个阶级的成员:

class something {
private:
raii obj_;  // will live and die with instances of the class
// ...
};

如果这样的对象管理内存,它们通常被称为“智能指针”。

这个有很多变体。例如,在第一个代码片段中,问题出现了如果有人想复制obj会发生什么。最简单的方法就是不允许复制。std::unique_ptr<>是一个智能指针,将成为下一个c++标准的标准库的一部分,它执行此操作 另一个这样的智能指针std::shared_ptr具有它所持有的资源(动态分配的对象)的“共享所有权”。也就是说,它可以被自由复制,并且所有副本都指向同一个对象。智能指针会跟踪有多少副本引用同一个对象,并在最后一个副本被销毁时删除它 第三种变体是std::auto_ptr,它实现了一种移动语义:一个对象仅由一个指针拥有,试图复制一个对象将导致(通过语法黑客)将对象的所有权转移到复制操作的目标。< / p >

揭示设计模式的c++编程书将RAII描述为:

  1. 获取所有资源
  2. 利用资源
  3. 释放资源

在哪里

  • 资源被实现为类,所有指针都有类包装器(使它们成为智能指针)。

  • 通过调用构造函数获取资源,通过调用析构函数隐式释放资源(与获取资源的顺序相反)。

对于一个非常强大的概念来说,这是一个非常糟糕的名字,而且可能是c++开发人员在转向其他语言时最容易忽略的事情之一。有一些运动试图将这个概念重命名为范围绑定资源管理,尽管它似乎还没有流行起来。

当我们说“资源”时,我们不仅仅指内存——它可以是文件句柄、网络套接字、数据库句柄、GDI对象……简而言之,我们有有限的供应,所以我们需要能够控制他们的使用。“作用域绑定”方面意味着对象的生命周期绑定到变量的作用域,因此当变量超出作用域时,析构函数将释放资源。这种方法的一个非常有用的特性是,它有助于提高异常安全性。例如,比较一下:

RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation();  // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks

用RAII这个

class ManagedResourceHandle {
public:
ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
~ManagedResourceHandle() {delete rawHandle; }
... // omitted operator*, etc
private:
RawResourceHandle* rawHandle;
};


ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();

在后一种情况下,当抛出异常并展开堆栈时,局部变量将被销毁,这确保了我们的资源被清理并且不会泄漏。

手动内存管理是一个噩梦,自编译器发明以来,程序员一直在发明各种方法来避免。带有垃圾收集器的编程语言使生活变得更容易,但以性能为代价。在这篇文章- 消除垃圾收集器:RAII方法中,Toptal工程师Peter goodsped - niklaus向我们介绍了垃圾收集器的历史,并解释了所有权和借款的概念如何在不影响其安全保证的情况下帮助消除垃圾收集器。

RAII类有三个部分:

  1. 该资源在析构函数中被放弃
  2. 类的实例是堆栈分配的
  3. 在构造函数中获取资源。这部分是

RAII代表“资源获取是初始化”。RAII的“资源获取”部分是你开始某些必须在之后结束的内容,例如:

  1. 打开文件
  2. 分配内存
  3. 获取锁

“is initialization”部分意味着获取发生在类的构造函数内部。

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/

对象的生命周期由其作用域决定。然而,有时我们需要(或者这很有用)创建一个独立于创建它的作用域的对象。在c++中,操作符new用于创建这样的对象。要销毁对象,可以使用操作符delete。由操作符new创建的对象是动态分配的,即分配在动态内存(也称为免费存储)中。因此,由new创建的对象将继续存在,直到使用delete显式销毁它。

使用newdelete时可能出现的一些错误是:

  • 泄露的对象(或内存):使用new分配对象,而忘记delete对象。
  • 不成熟的删除(或晃来晃去的参考):持有指向对象的另一个指针,delete对象,然后使用另一个指针。
  • 双删除:尝试delete一个对象两次。

一般来说,首选有作用域的变量。然而,RAII可以作为newdelete的替代方法,使对象独立于其作用域而存在。这种技术包括将指针指向分配在堆上的对象,并将其放置在处理/管理器对象中。后者有一个析构函数负责销毁对象。这将保证该对象对任何想要访问它的函数可用,并且在处理对象的生命周期结束时销毁该对象,而不需要显式的清理。

c++标准库中使用RAII的例子有std::stringstd::vector

考虑下面这段代码:

void fn(const std::string& str)
{
std::vector<char> vec;
for (auto c : str)
vec.push_back(c);
// do something
}

当你创建一个向量并将元素推入它时,你不关心这些元素的分配和释放。vector对象使用new为堆上的元素分配空间,使用delete释放空间。作为vector的用户,您不关心实现细节,并且相信vector不会泄漏。在这种情况下,向量是其元素的处理对象

标准库中使用RAII的其他示例有std::shared_ptrstd::unique_ptrstd::lock_guard

这种技术的另一个名字是SBRM,是范围绑定资源管理的缩写。

RAII概念只是一个C堆栈变量的想法。最简单的解释。

许多人认为RAII是用词不当,但实际上它是这个习语的正确名称,只是没有很好地解释它。

维基百科详细解释了行为: 资源获取即初始化(RAII)是一些面向对象的静态类型编程语言中使用的编程习语,用于描述特定的语言行为。在RAII中,持有资源是一个类不变量,并且与对象的生命周期相关联:资源分配(或获取)是在对象创建(特别是初始化)期间由构造函数完成的,而资源释放(释放)是在对象销毁(特别是终结)期间由析构函数完成的。换句话说,资源获取必须成功,初始化才能成功。因此,资源被保证在初始化结束和终结开始之间被持有(持有资源是一个类不变量),并且仅在对象处于活动状态时被持有。因此,如果没有对象泄漏,就没有资源泄漏

至于这个名字,它仅仅是指“获取资源”的行为。是初始化操作,应该是资源类对象的初始化/构造函数的一部分。换句话说,使用这种习惯用法,使用资源意味着创建一个资源类来保存资源,并在构造类对象时初始化资源。它隐式地建议资源的释放应该在资源类析构函数中对称地发生。

这个怎么有用? 当然,您可以选择不使用这个成语,但是如果您想知道使用这个成语会得到什么,请考虑

< a href = " https://berthub。eu/articles/posts/ cpp2 /" rel="nofollow noreferrer">RAII 对于更大的c++项目来说,在构造函数/析构函数对之外不包含对new或delete(或malloc/free)的单个调用是很常见的。

你可以避免

Exit: free_resource() //在退出函数前清除资源

或者使用RAII锁,这样你就不会忘记解锁。

我已经多次回到这个问题并阅读了它,我认为投票最多的答案有点误导。

RAII的关键是:

“它(主要)不是关于捕捉异常,主要是关于管理资源的所有权。”

得票最高的答案夸大了异常安全,这让我很困惑。

事实是:

  1. 你仍然需要编写try catch来处理异常(检查下面的2个代码示例),除了你不需要担心在你的catch块中使用RAII为那些类释放资源。否则,你需要查找每个非raii类的API,以找到调用哪个函数,以便在catch块中释放获得的资源。RAII只是保存这些工作。

  2. 与上面类似,当使用RAII编码时,你只需写更少的代码,不需要调用释放资源函数。所有的清理都在析构函数中完成。

另外,请查看我在上面的评论中发现的两个有用的代码示例。

__abc0, __abc1

附:可以与python with .. as语句进行比较,你需要捕获可能发生在with块内部的异常。