RAII是什么意思?

460
RAII是资源获取即初始化的意思是什么?

14
http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization - user151323
17
这就是让我深刻认识到它的原因。http://www.stroustrup.com/bs_faq2.html#finally - Hal Canary
4
微软提供的参考文献非常清晰,包含3个句子和2个例子! https://msdn.microsoft.com/zh-cn/library/hh438480.aspx - Gab是好人
10个回答

553

这是一个非常强大的概念,但却有一个非常糟糕的名称。也许是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();

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


3
@the_mandrill: 我尝试运行 ideone.com/1Jjzuc 这个程序,但没有调用析构函数。根据 tomdalling.com/blog/software-design/... 的说法,C++ 保证会在堆栈上的对象抛出异常时调用析构函数。那么为什么这里没有执行析构函数?我的资源泄漏了吗?还是永远不会被释放或释放? - Destructor
19
出现了一个异常,但您没有捕获它,因此应用程序终止。如果您使用try {} catch () {} 包装一下,那么它就像预期的那样工作:http://ideone.com/xm2GR9 - the_mandrill
3
不确定“作用域限定”的名称是否是最佳选择,因为存储类别说明符与作用域一起确定实体的存储期限。将其缩小到“作用域限定”可能是一种有用的简化,但它并不完全准确。 - clickMe
2
但是你如何解释原名中的RA是初始化呢?在我看来,RAII的意思是每个对象都要负责在超出作用域后处理其删除。对我来说,这与是初始化不符,因为一切都与析构函数有关。我仍然对这个习语名称感到困惑。 - nowox
2
“C++开发人员最想念的事情之一…”这不是和Java中的try-with-resources类似吗?它似乎解决了同样的问题,我看不出RAII相比Java的解决方案有什么优势或劣势。 - marc.guenther

157

这是一种编程习惯,简要来说,你需要:

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

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

这是C++中广泛使用的良好实践,因为它不仅是处理资源的安全方式,而且使您的代码更加简洁,因为您不需要将错误处理代码与主要功能混合在一起。

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

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


2
据我所知,该缩写并不意味着对象必须是本地(堆栈)变量。它可以是另一个对象的成员变量,因此当“持有”对象被销毁时,成员对象也会被销毁,并释放资源。实际上,我认为这个缩写特别指的是只有构造函数和析构函数,没有open()/close()方法来初始化和释放资源,因此资源的“持有”仅仅是对象的生命周期,无论这个生命周期是由上下文(堆栈)还是显式(动态分配)处理的。 - Javier
1
实际上,并没有规定资源必须在构造函数中获取。文件流、字符串和其他容器会这样做,但是资源也可以像智能指针一样传递给构造函数,这通常是情况。由于您的答案得到了最多的赞同票,您可能需要修复这个问题。 - sbi
它不是一个首字母缩写,而是一个缩写。据我所知,大多数人发音为“ar ey ay ay”,因此它并不真正符合像DARPA这样的首字母缩写,后者的发音是DARPA而不是拼写。此外,我认为RAII是一种范例,而不仅仅是一种习惯用法。 - dtech
1
@Peter Torok:我尝试了http://ideone.com/1Jjzuc这个程序。但是没有析构函数调用。http://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/说C++保证堆栈上的对象的析构函数将被调用,即使抛出异常。那么,为什么这里没有执行析构函数呢?我的资源泄漏了吗?还是永远不会被释放或释放? - Destructor
1
在这个例子中,异常没有被捕获,因此程序立即终止。如果你捕获了异常,那么析构函数会在堆栈展开时被调用。 - the_mandrill

60

"RAII"是"Resource Acquisition is Initialization"的缩写,实际上这个术语名字有些不当,因为它关心的并不是资源的获取(以及对象的初始化),而是通过对象的销毁来释放资源。但是,我们用习惯了这个名字,就将其保留下来。

本质上,这种编程技巧的核心在于使用局部自动对象来封装资源(比如内存块、打开的文件、未锁定的互斥量等等),让该对象的析构函数在其所属的作用域结束时释放其所持有的资源:

{
  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展示,它实现了一种移动语义:一个对象仅由一个指针拥有,并且尝试复制对象将通过语法hackery将对象所有权转移到复制操作的目标。

4
std::auto_ptr 已经过时,它是 std::unique_ptr 的旧版本。std::auto_ptr 尽可能地模拟了 C++98 中的移动语义,而 std::unique_ptr 使用了 C++11 中的新移动语义。由于在 std::auto_ptr 中,从非 const 复制默认使用移动语义(不需要 std::move),因此创建了这个新类来更明确地表示 C++11 中的移动语义(需要 std::move,除非是临时对象)。 - Jan Hudec
@JiahaoCai:曾经,很多年前(在Usenet上),Stroustrup本人就这么说过。 - sbi

38
一个对象的生命周期由其作用域确定。但是,有时我们需要创建一个独立于创建它的作用域的对象,这是很有用的。在C ++中,使用运算符new来创建这样的对象。要销毁对象,可以使用运算符delete。由运算符new创建的对象是动态分配的,即在动态内存中分配(也称为堆或自由存储)。因此,通过delete显式地销毁对象之前,由new创建的对象将继续存在。
一些使用newdelete可能出现的错误包括:
- 泄漏对象(或内存):使用new分配对象并忘记delete对象。 - 过早删除(或悬空引用):持有指向对象的另一个指针,delete对象,然后使用另一个指针。 - 双重删除:尝试两次delete对象。
通常,首选作用域变量。但是,RAII可用作替代newdelete,使对象独立于其作用域存活。这种技术包括将指向在堆上分配的对象的指针放置在句柄/管理器对象中。后者具有将负责销毁对象的析构函数。这将保证任何想要访问它的函数都可以访问该对象,并且当句柄对象的生命周期结束时,对象将被销毁,而不需要显式清理。
使用RAII的C ++标准库示例包括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不会泄漏资源。在这种情况下,vector是其元素的句柄对象
标准库中使用RAII的其他示例包括std::shared_ptrstd::unique_ptrstd::lock_guard
这种技术的另一个名称是SBRM,即Scope-Bound Resource Management(范围绑定资源管理)。

3
"SBRM"对我来说更有意义。我来回答这个问题是因为我认为自己理解RAII,但名称让我感到困惑。听到它被描述为“范围绑定资源管理”,我立即意识到自己确实理解这个概念。 - JShorthouse
2
我不确定为什么这个回答没有被标记为问题的答案。这是一个非常详尽和写得很好的答案,感谢@elmiomar。 - Abdelrahman Shoman

17

这本书《揭秘设计模式的C++编程》将RAII描述为:

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

其中

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

  • 资源是通过调用它们的构造函数来获取的,并通过隐式方式释放(按照获取的相反顺序)通过调用它们的析构函数。


1
@Brandin 我已经编辑了我的帖子,让读者关注重点内容,而不是争论什么构成公平使用的版权法律灰色地带。 - Dennis

8
RAII类有三个部分:
  1. 在析构函数中释放资源
  2. 类的实例是栈分配的
  3. 在构造函数中获取资源。这一部分是可选的,但很常见。
RAII代表“Resource Acquisition is initialization(资源获取即初始化)”。RAII中的“资源获取”部分是指开始一个必须稍后结束的操作,例如:
  1. 打开文件
  2. 分配内存
  3. 获取锁
“初始化”部分意味着获取发生在类的构造函数中。
来源:https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/

5
手动内存管理是程序员自编译器发明以来一直在想方设法避免的噩梦。具有垃圾收集器的编程语言使生活更加轻松,但代价是性能损失。在这篇文章 - 消除垃圾收集器:RAII方式中,Toptal工程师Peter Goodspeed-Niklaus向我们介绍了垃圾收集器的历史,并解释了所有权和借用的概念如何帮助消除垃圾收集器而不影响其安全保证。

3
许多人认为RAII是一个误称,但实际上这个习语的名称是正确的,只是没有解释清楚。 维基百科详细解释了行为: 资源获取即初始化(RAII)是用于描述特定语言行为的几种面向对象、静态类型编程语言中使用的编程习惯。在RAII中,持有资源是类不变量,并且与对象的生命周期相关联:资源分配(或获取)在对象创建(具体地说是初始化)期间由构造函数完成,而资源释放则在对象销毁(具体地说是最终化)期间由析构函数完成。换句话说,资源获取必须成功才能使初始化成功。因此,在初始化完成和最终化开始之间保证持有资源(持有资源是类不变量),并且仅在对象存活时持有资源。因此,如果没有对象泄漏,就不会有资源泄漏。
现在来讲讲这个名字,它的意思是“资源获取”的操作是初始化的一部分,应该成为资源类对象的初始化/构造函数的一部分。换句话说,使用这个习惯用法,处理资源意味着需要创建一个资源类来持有该资源,并在构造类对象时初始化资源。隐含地,它建议在资源类析构函数中对资源进行对称释放。 这有什么用处? 当然,您可以选择不使用这个习惯用法,但如果您想知道使用这个习惯用法会得到什么,考虑一下

RAII 事实上,即使在较大的C++项目中也很常见,在构造函数/析构函数对之外没有使用new或delete(或malloc/free)的调用。或者根本不使用。

这样就能避免:

exit: free_resouce() // 在退出函数之前清理资源

或者使用 RAII lock,这样您就永远不会忘记解锁。

2
我已经多次回到这个问题并阅读了它,我认为得票最高的答案有点误导性。
RAII 的关键是:
“它(大多数情况下)与捕获异常无关,主要是管理资源的所有权。”
得票最高的答案夸大了“异常安全”的作用,这让我感到困惑。
事实上:
1. 你仍然需要编写 try catch 来处理异常(请查看下面的两个代码示例),只是使用 RAII 的类在 catch 块中不需要担心释放资源。否则,您需要查找每个非-RAII 类的 API,以找到要调用以释放在 catch 块中获取的资源的函数。RAII 可以简化这些工作。
2. 使用 RAII 编码时,您只需编写较少的代码,无需调用释放资源函数。所有清理工作都在析构函数中完成。
此外,请查看我在上面的评论中发现有用的这两个代码示例。

https://ideone.com/1Jjzuchttps://ideone.com/xm2GR9

P.S. 可以将其与Python中的with .. as语句进行比较,需要捕获可能在with块内发生的异常。


这些示例有点模糊,因为它们不是有效的C++代码。行int *p = new int[-1];无法编译通过。 - David C. Rankin

2

RAII概念仅仅是一个C语言栈变量的想法,这是最简单的解释。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接