这是一个非常强大的概念,但却有一个非常糟糕的名称。也许是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();
在后一种情况下,当异常被抛出并且堆栈被解开时,局部变量将被销毁,这确保了我们的资源被清理并且不会泄漏。
RA是初始化
呢?在我看来,RAII的意思是每个对象都要负责在超出作用域后处理其删除。对我来说,这与是初始化
不符,因为一切都与析构函数有关。我仍然对这个习语名称感到困惑。 - nowox这是一种编程习惯,简要来说,你需要:
这确保了在资源被使用时无论发生什么,最终都会被释放(无论是正常返回、包含对象的销毁还是抛出异常)。
这是C++中广泛使用的良好实践,因为它不仅是处理资源的安全方式,而且使您的代码更加简洁,因为您不需要将错误处理代码与主要功能混合在一起。
*
更新:“本地”可能意味着本地变量或类的非静态成员变量。在后一种情况下,成员变量将随其所有者对象初始化和销毁。
**
更新2:正如@sbi指出的那样,资源虽然通常在构造函数内分配,但也可以在外部分配并作为参数传递。
open()
/close()
方法来初始化和释放资源,因此资源的“持有”仅仅是对象的生命周期,无论这个生命周期是由上下文(堆栈)还是显式(动态分配)处理的。 - Javier"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将对象所有权转移到复制操作的目标。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 Hudecnew
来创建这样的对象。要销毁对象,可以使用运算符delete
。由运算符new
创建的对象是动态分配的,即在动态内存中分配(也称为堆或自由存储)。因此,通过delete
显式地销毁对象之前,由new
创建的对象将继续存在。new
和delete
可能出现的错误包括:new
分配对象并忘记delete
对象。
- 过早删除(或悬空引用):持有指向对象的另一个指针,delete
对象,然后使用另一个指针。
- 双重删除:尝试两次delete
对象。new
和delete
,使对象独立于其作用域存活。这种技术包括将指向在堆上分配的对象的指针放置在句柄/管理器对象中。后者具有将负责销毁对象的析构函数。这将保证任何想要访问它的函数都可以访问该对象,并且当句柄对象的生命周期结束时,对象将被销毁,而不需要显式清理。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不会泄漏资源。在这种情况下,vector是其元素的句柄对象。std::shared_ptr
、std::unique_ptr
和std::lock_guard
。这本书《揭秘设计模式的C++编程》将RAII描述为:
其中
资源被实现为类,并且所有指针都有类包装器(使它们成为智能指针)。
资源是通过调用它们的构造函数来获取的,并通过隐式方式释放(按照获取的相反顺序)通过调用它们的析构函数。
这样就能避免:RAII 事实上,即使在较大的C++项目中也很常见,在构造函数/析构函数对之外没有使用new或delete(或malloc/free)的调用。或者根本不使用。
或者使用 RAII lock,这样您就永远不会忘记解锁。exit: free_resouce() // 在退出函数之前清理资源
https://ideone.com/1Jjzuc,https://ideone.com/xm2GR9
P.S. 可以将其与Python中的with .. as
语句进行比较,需要捕获可能在with
块内发生的异常。
int *p = new int[-1];
无法编译通过。 - David C. RankinRAII概念仅仅是一个C语言栈变量的想法,这是最简单的解释。