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


Answers:


374

这是一个令人难以置信的强大概念的名字,真是一个可怕的名字,也许是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();

在后一种情况下,当引发异常并取消堆栈堆栈时,局部变量将被销毁,以确保我们的资源被清理并且不会泄漏。


2
@the_mandrill:我尝试了ideone.com/1Jjzuc这个程序。但是没有析构函数调用。tomdalling.com/blog/software-design/…表示C ++保证即使抛出异常也将调用堆栈上对象的析构函数。那么,为什么析构函数不在这里执行?我的资源是否泄漏了,或者将永远不会被释放或释放?
毁灭者

8
引发了异常,但是您没有捕获到异常,因此应用程序终止。如果您尝试使用{} catch(){}进行包装,那么它会按预期工作:ideone.com/xm2GR9
the_mandrill

2
Scope-Bound由于存储类说明符范围一起确定了实体的存储持续时间,因此不太确定此处是否是最佳名称选择。将其范围缩小到范围限制可能是一个有用的简化方法,但是它不是100%精确的
-SebNag

125

这是一个编程习语,简要表示您

  • 将资源封装到一个类中(通常(但不一定是**的构造函数)获取资源,并且其析构函数始终释放该资源)
  • 通过类的本地实例使用资源*
  • 当对象超出范围时,资源将自动释放

这样可以保证无论使用资源时发生什么,它最终都会被释放(无论是由于正常返回,包含对象的破坏还是引发的异常)。

这是C ++中广泛使用的良好实践,因为除了安全地处理资源之外,它还使您的代码更加简洁,因为您无需将错误处理代码与主要功能混在一起。

* 更新: “ local”可能意味着局部变量或类的非静态成员变量。在后一种情况下,成员变量将使用其所有者对象初始化和销毁​​。

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


1
AFAIK,首字母缩写词并不表示对象必须位于局部(堆栈)变量上。它可能是另一个对象的成员变量,所以当“持有”对象被破坏时,该成员对象也被破坏,并且资源被释放。实际上,我认为首字母缩写仅意味着没有open()/ close()方法来初始化和释放资源,只有构造函数和析构函数,因此资源的“持有”只是对象的生存期,无论该生存期是否为由上下文(堆栈)或显式(动态分配)处理
哈维尔

1
实际上,没有什么说必须在构造函数中获取资源。文件流,字符串和其他容器可以做到这一点,但是资源也可以传递给构造函数,就像智能​​指针通常那样。由于您的答案是最受好评的,因此您可能需要解决此问题。
2012年

它不是首字母缩写,而是缩写。IIRC大多数人都将其发音为“ ar ey ay ay”,因此它实际上并没有资格使用诸如DARPA之类的首字母缩写词,它的发音是DARPA而不是拼写。另外,我想说RAII是一种范式,而不仅仅是一个成语。
dtech

@Peter Torok:我尝试了ideone.com/1Jjzuc这个程序。但是没有析构函数调用。该tomdalling.com/blog/software-design/...说,C ++保证在堆栈上的对象的析构函数将被调用,即使抛出异常。那么,为什么析构函数不在这里执行?我的资源是否泄漏了,或者将永远不会被释放或释放?
毁灭者

50

“ RAII”代表“资源获取是初始化”,实际上用词不当,因为它不是它所关注的资源获取(和对象的初始化),而是释放资源(通过销毁对象) )。
但是RAII是我们获得的名称,并且坚持下去。

从本质上讲,该惯用语具有将资源(内存块,打开的文件,解锁的互斥锁,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 它实现了一种移动语义:一个对象仅由一个指针拥有,并且尝试复制一个对象(通过语法黑客)将导致将该对象的所有权转移到复制操作的目标。


4
std::auto_ptr是的过时版本std::unique_ptrstd::auto_ptrC ++ 98中尽可能多的一种模拟移动语义,std::unique_ptr使用了C ++ 11的新移动语义。之所以创建新类,是因为C ++ 11的move语义更加明确(需要std::move从临时目录中删除),而默认情况下,它用于来自非const的任何副本std::auto_ptr
2013年

@JiahaoCai:很多年前(在Usenet上),Stroustrup自己是这样说的。
sbi

21

对象的生存期由其范围决定。但是,有时我们需要创建一个对象,该对象与创建对象的作用域无关,这是有用的,或者很有用。在C ++中,运算符new用于创建这样的对象。并销毁对象,delete可以使用运算符。由操作员创建的对象new是动态分配的,即在动态内存(也称为空闲存储)中分配。因此,由创建的对象new将继续存在,直到使用明确销毁为止delete

使用new和时可能发生的一些错误delete是:

  • 泄漏的对象(或内存):new用于分配对象并忘记delete该对象。
  • 过早删除(或悬挂参考):持有另一个指向对象的指针delete,然后再使用另一个指针。
  • 双重删除:尝试delete两次对象。

通常,首选范围变量。然而,RAII可以用作替代newdelete使对象活独立地是对其范围的。这种技术包括将指针分配到在堆上分配的对象,并将其放置在handle / manager对象中。后者具有一个析构函数,将负责销毁对象。这将确保该对象可用于任何想要访问它的函数,并且该对象在句柄对象的生存期结束时将被销毁,而无需显式清理。

来自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
}

当创建向量并将元素推入向量时,您不必担心分配和取消分配此类元素。向量用于new在堆上为其元素分配空间,并delete释放该空间。作为vector的用户,您无需关心实现细节,并且会相信vector不会泄漏。在这种情况下,向量是其元素的句柄对象

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

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


1
“ SBRM”对我来说意义更大。我之所以提出这个问题,是因为我以为我理解RAII,但是这个名字让我很吃惊,听到它被形容为“范围绑定资源管理”,使我立即意识到我确实理解了这个概念。
JShorthouse,

我不确定为什么这没有将其标记为问题的答案。这是一个非常彻底且写得很好的答案,谢谢@elmiomar
Abdelrahman Shoman

13

揭示了具有设计模式的C ++编程这本书将RAII描述为:

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

哪里

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

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


1
@Brandin我已经编辑了我的帖子,以便读者将重点放在重要的内容上,而不是就构成合理使用的版权法的灰色区域进行辩论。
丹尼斯

7

RAII类分为三个部分:

  1. 资源被释放在析构函数中
  2. 该类的实例是堆栈分配的
  3. 资源是在构造函数中获取的。这部分是可选的,但很常见。

RAII代表“资源获取是初始化”。RAII的“资源获取”部分是您开始一些必须在以后结束的事情的地方,例如:

  1. 开启档案
  2. 分配一些内存
  3. 取得锁

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

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


5

自从编译器发明以来,手动内存管理是程序员一直在想办法避免的噩梦。使用垃圾回收器进行编程的语言可以使生活更轻松,但是会降低性能。在本文“ 消除垃圾收集器:RAII方式”中,Toptal工程师Peter Goodspeed-Niklaus向我们介绍了垃圾收集器的历史,并解释了所有权和借用的概念如何在不损害其安全保证的前提下消除垃圾收集器。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.