资源获取即初始化 (RAII) 是什么意思?
10 回答
对于一个非常强大的概念来说,这是一个非常糟糕的名字,也许是 C++ 开发人员在切换到其他语言时错过的第一件事。尝试将这个概念重命名为Scope-Bound Resource Management已经有了一些尝试,尽管它似乎还没有流行起来。
当我们说“资源”时,我们不仅仅指内存——它可以是文件句柄、网络套接字、数据库句柄、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();
在后一种情况下,当抛出异常并解开堆栈时,局部变量将被销毁,从而确保我们的资源被清理并且不会泄漏。
这是一个编程习语,它简要地表示你
- 将资源封装到一个类中(其构造函数通常 - 但不一定** - 获取资源,而其析构函数总是释放它)
- 通过类的本地实例使用资源*
- 当对象超出范围时资源会自动释放
这保证了资源在使用时无论发生什么,它最终都会被释放(无论是由于正常返回、包含对象的破坏还是抛出异常)。
这是 C++ 中广泛使用的良好实践,因为除了作为一种安全的资源处理方式之外,它还使您的代码更加简洁,因为您不需要将错误处理代码与主要功能混合在一起。
*
更新: “local”可能表示局部变量或类的非静态成员变量。在后一种情况下,成员变量将使用其所有者对象进行初始化和销毁。
**
更新2 :正如@sbi 指出的那样,资源 - 虽然经常在构造函数内部分配 - 也可以在外部分配并作为参数传入。
“RAII”代表“Resource Acquisition is Initialization”,实际上是用词不当,因为它关心的不是资源获取(和对象的初始化),而是释放资源(通过对象的销毁) )。
但 RAII 是我们得到的名字,它坚持下去。
从本质上讲,该惯用语的特点是在本地自动对象中封装资源(内存块、打开的文件、解锁的互斥锁、您的名字),并让该对象的析构函数在对象被销毁时释放资源它所属范围的结尾:
{
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
它实现了一种移动语义:对象仅由一个指针拥有,并且尝试复制对象将导致(通过语法黑客)将对象的所有权转移到复制操作的目标。
一个对象的生命周期是由它的作用域决定的。然而,有时我们需要,或者它是有用的,创建一个独立于创建它的范围的对象。在 C++ 中,运算符new
用于创建这样的对象。而要销毁对象,delete
可以使用操作符。运算符创建的对象new
是动态分配的,即分配在动态内存(也称为堆或空闲存储)中。因此,由 所创建的对象new
将继续存在,直到使用delete
.
new
使用和时可能出现的一些错误delete
是:
- 泄漏的对象(或内存):
new
用于分配对象并忘记delete
该对象。 - 过早删除(或悬空引用):持有另一个指向对象的指针,
delete
该对象,然后使用另一个指针。 - 双重删除:尝试
delete
两次删除一个对象。
通常,范围变量是首选。但是,RAII 可以用作替代对象new
并使delete
对象独立于其范围而存在。这种技术包括获取指向在堆上分配的对象的指针并将其放置在句柄/管理器对象中。后者有一个析构函数,负责销毁对象。这将保证该对象可供任何想要访问它的函数使用,并且该对象在句柄对象的生命周期结束时被销毁,而无需显式清理。
C++ 标准库中使用 RAII 的示例是std::string
和std::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_ptr
、std::unique_ptr
和std::lock_guard
.
这种技术的另一个名称是SBRM,是Scope-Bound Resource Management 的缩写。
C++ Programming with Design Patterns Revealed一书将 RAII 描述为:
- 获取所有资源
- 使用资源
- 释放资源
在哪里
资源被实现为类,并且所有指针都有围绕它们的类包装器(使它们成为智能指针)。
资源是通过调用它们的构造函数获取的,并通过调用它们的析构函数隐式释放(以获取的相反顺序)。
There are three parts to an RAII class:
- The resource is relinquished in the destructor
- Instances of the class are stack allocated
- The resource is acquired in the constructor. This part is optional, but common.
RAII stands for "Resource Acquisition is initialization." The "resource acquisition" part of RAII is where you begin something that must be ended later, such as:
- Opening a file
- Allocating some memory
- Acquiring a lock
The "is initialization" part means that the acquisition happens inside the constructor of a class.
自编译器发明以来,手动内存管理是程序员一直在发明方法来避免的噩梦。带有垃圾收集器的编程语言使生活更轻松,但以性能为代价。在这篇文章 -消除垃圾收集器:RAII 方式中,Toptal 工程师 Peter Goodspeed-Niklaus 向我们展示了垃圾收集器的历史,并解释了所有权和借用的概念如何帮助消除垃圾收集器而不损害其安全保证。
RAII 概念只是一个 C 堆栈变量的想法。最简单的解释方式。
许多人认为 RAII 用词不当,但实际上它是这个成语的正确名称,只是没有很好地解释。
维基百科详细解释行为:资源获取即初始化 (RAII) 是一种编程惯用语,用于几种面向对象、静态类型的编程语言中,用于描述特定语言的行为。在 RAII 中,持有资源是类不变量,并且与对象生命周期相关:资源分配(或获取)在对象创建(特别是初始化)期间由构造函数完成,而资源释放(释放)在对象销毁期间完成(具体完成),由析构函数。换句话说,资源获取必须成功,初始化才能成功。因此,资源被保证在初始化完成和终结开始之间被持有(持有资源是类不变量),并且只在对象处于活动状态时被持有。因此,如果没有对象泄漏,
而现在的名字,它只是意味着“资源获取”的动作是一个初始化动作,应该是资源类对象的初始化/构造函数的一部分。换句话说,使用这个成语,使用资源意味着创建一个资源类来保存资源并在构造类对象时初始化资源。隐含地,它表明资源的释放应该在资源类析构函数中对称地发生。
这有什么用? 您当然可以选择不使用此成语,但如果您想知道使用此成语会得到什么,请考虑
RAII 对于更大的 C++ 项目,在构造函数/析构函数对之外不包含对 new 或 delete(或 malloc/free)的单个调用是很常见的。或者根本没有,事实上。
你可以避免
exit: free_resouce() // 在退出函数之前清理资源
或使用RAII 锁,这样您就永远不会忘记解锁。
我已经多次回到这个问题并阅读了它,我认为投票最高的答案有点误导。
RAII 的关键是:
“这(主要)不是关于捕捉异常,它主要是关于管理资源的所有权。”
投票最高的答案夸大了exception-safe,这让我感到困惑。
事实是:
您仍然需要编写
try catch
来处理异常(检查下面的 2 个代码示例),除了您不必担心在您的catch
块中使用 RAII 为这些类释放资源。否则,您需要查找每个非 RAII 类的 API 来查找调用哪个函数,从而将获取的资源分catch
块释放。RAII 简单地保存了这些工作。与上面类似,使用 RAII 编码时,您只需编写更少的代码,无需调用释放资源函数。所有的清理工作都在析构函数中完成。
另外,请检查我在上面的评论中发现有用的这 2 个代码示例。
https://ideone.com/1Jjzuc,https://ideone.com/xm2GR9
PS One 可以与 pythonwith .. as
语句进行比较,您还需要捕获块内可能发生的异常with
。