资源获取即初始化 (RAII) 是什么意思?
对于一个非常强大的概念来说,这是一个非常糟糕的名字,也许是 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”可能表示局部变量或类的非静态成员变量。在后一种情况下,成员变量将使用其所有者对象进行初始化和销毁。
**
Update2: 正如@sbi 指出的那样,资源(尽管通常在构造函数内部分配)也可以在外部分配并作为参数传入。
open()
/close()
方法来初始化和释放资源,只有构造函数和析构函数,因此资源的“持有”只是对象的生命周期,无论该生命周期是由上下文(堆栈)还是显式(动态分配)处理
“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
的特色,它实现了一种移动语义:一个对象是仅由一个指针拥有,并且尝试复制对象将导致(通过语法黑客)将对象的所有权转移到复制操作的目标。
std::auto_ptr
是 std::unique_ptr
的过时版本。 std::auto_ptr
在 C++98 中尽可能多地模拟移动语义,std::unique_ptr
使用 C++11 的新移动语义。创建新类是因为 C++11 的移动语义更加明确(需要 std::move
,除了临时的),而它默认用于 std::auto_ptr
中非常量的任何副本。
一个对象的生命周期是由它的作用域决定的。然而,有时我们需要,或者它是有用的,创建一个独立于创建它的范围的对象。在 C++ 中,运算符 new
用于创建这样的对象。而要销毁对象,可以使用运算符delete
。 new
运算符创建的对象是动态分配的,即分配在动态内存中(也称为heap 或free store)。因此,由 new
创建的对象将继续存在,直到使用 delete
明确销毁它。
使用 new
和 delete
时可能出现的一些错误是:
泄漏的对象(或内存):使用new分配一个对象而忘记删除该对象。
过早删除(或悬空引用):持有另一个指向对象的指针,删除该对象,然后使用另一个指针。
双重删除:试图删除一个对象两次。
通常,范围变量是首选。但是,RAII 可以用作 new
和 delete
的替代方案,以使对象独立于其范围而存在。这种技术包括获取指向在堆上分配的对象的指针并将其放置在句柄/管理器对象中。后者有一个析构函数,负责销毁对象。这将保证该对象可供任何想要访问它的函数使用,并且该对象在 句柄对象 的生命周期结束时被销毁,而无需显式清理。
使用 RAII 的 C++ 标准库中的示例是 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 描述为:
获取所有资源 使用资源 释放资源
在哪里
资源被实现为类,并且所有指针都有围绕它们的类包装器(使它们成为智能指针)。
资源通过调用它们的构造函数来获取,并通过调用它们的析构函数隐式释放(以获取的相反顺序)。
RAII 类包含三个部分:
资源在析构函数中被放弃类的实例在堆栈中分配资源在构造函数中获取。这部分是可选的,但很常见。
RAII 代表“资源获取是初始化”。 RAII 的“资源获取”部分是您开始一些必须稍后结束的事情的地方,例如:
打开文件分配一些内存获取锁
“正在初始化”部分意味着获取发生在类的构造函数内部。
自编译器发明以来,手动内存管理是程序员一直在发明方法来避免的噩梦。带有垃圾收集器的编程语言使生活更轻松,但以性能为代价。在这篇文章 - Eliminating the Garbage Collector: The RAII Way 中,Toptal 工程师 Peter Goodspeed-Niklaus 向我们介绍了垃圾收集器的历史,并解释了所有权和借用的概念如何帮助消除垃圾收集器而不损害其安全保证。
RAII 概念只是一个 C 堆栈变量的想法。最简单的解释方式。
许多人认为 RAII 用词不当,但实际上它是这个成语的正确名称,只是没有很好地解释。
Wikipedia 详细解释了行为:资源获取即初始化 (RAII) 是一种编程习惯用法,用于几种面向对象、静态类型的编程语言中,用于描述特定语言的行为。在 RAII 中,持有资源是类不变量,并且与对象生命周期相关联:资源分配(或获取)在对象创建(特别是初始化)期间由构造函数完成,而资源释放(释放)在对象销毁期间完成(具体完成),由析构函数。换句话说,资源获取必须成功,初始化才能成功。因此,资源被保证在初始化完成和终结开始之间被持有(持有资源是类不变量),并且只在对象处于活动状态时被持有。因此,如果没有对象泄漏,就没有资源泄漏。
而现在的名字,它只是意味着“资源获取”的动作是一个初始化动作,应该是资源类对象的初始化/构造函数的一部分。换句话说,使用这个成语,使用资源意味着创建一个资源类来保存资源并在构造类对象时初始化资源。隐含地,它表明资源的释放应该在资源类析构函数中对称地发生。
这有什么用?您当然可以选择不使用此成语,但如果您想知道使用此成语会得到什么,请考虑
RAII 对于更大的 C++ 项目,在构造函数/析构函数对之外不包含对 new 或 delete(或 malloc/free)的单个调用是很常见的。或者根本没有,事实上。
你可以避免
exit: free_resouce() // 在退出函数之前清理资源
或使用 RAII lock,这样您就不会忘记解锁。
我已经多次回到这个问题并阅读了它,我认为投票最高的答案有点误导。
RAII 的关键是:
“这(主要)不是关于捕捉异常,它主要是关于管理资源的所有权。”
投票最高的答案夸大了异常安全,这让我感到困惑。
事实是:
您仍然需要编写 try catch 来处理异常(查看下面的 2 个代码示例),除了您不必担心在 catch 块中使用 RAII 为这些类释放资源。否则,您需要查找每个非 RAII 类的 API 来查找调用哪个函数,以便在 catch 块中释放获取的资源。 RAII 简单地保存了这些工作。与上面类似,使用 RAII 编码时,您只需编写更少的代码,无需调用释放资源函数。所有的清理工作都在析构函数中完成。
另外,请检查我在上面的评论中发现有用的这 2 个代码示例。
https://ideone.com/1Jjzuc , https://ideone.com/xm2GR9
PS One 可以与 python with .. as
语句进行比较,您还需要捕获可能发生在 with
块内的异常。
int *p = new int[-1];
行不编译。
不定期副业成功案例分享
Scope-Bound
是否是此处的最佳名称选择,因为存储类说明符与范围一起确定实体的存储持续时间。将其缩小到范围限制可能是一个有用的简化,但它不是 100% 精确RA is initialization
?我对 RAII 的理解是,一旦超出范围,每个对象都有责任处理其删除。对我来说,它与is initialization
不匹配,因为一切都与析构函数有关。我仍然对那个成语名称感到困惑。