理解术语和概念的含义 – RAII(资源获取是初始化)

请问C ++开发人员,请给我们一个关于RAII是什么的好的描述,为什么它很重要,以及它是否与其他语言有关联?

确实知道一点点。 我相信它代表“资源获取是初始化”。 然而,这个名字并没有和我(可能是不正确的)理解RAII是什么关系:我得到的印象是,RAII是一种初始化堆栈上的对象的方法,当这些variables超出范围时,析构函数会自动被称为导致资源被清理。

那为什么不叫“使用栈来触发清理”(UTSTTC :)呢? 你怎么从那里到“RAII”?

而且你怎么能在堆栈上做些什么来清理堆上的东西呢? 另外,有没有不能使用RAII的情况? 你有没有发现自己希望收集垃圾? 至less有一个垃圾收集器可以用于某些对象,同时让其他人被pipe理?

谢谢。

那为什么不叫“使用栈来触发清理”(UTSTTC :)呢?

RAII告诉你该怎么做:在构造函数中获取你的资源! 我会添加:一个资源,一个构造函数。 UTSTTC只是其中的一个应用,RAII更多。

资源pipe理很糟糕。 这里资源就是使用后需要清理的东西。 对许多平台上的项目进行研究表明,大多数错误都与资源pipe理有关 – 在Windows上尤其糟糕(由于对象和分配器的types众多)。

在C ++中,由于exception和(C ++风格)模板的组合,资源pipe理特别复杂。 对于引擎盖下的窥视,请参阅GOTW8 )。


C ++保证当且仅当构造函数成功时调用析构函数。 依靠这个,RAII可以解决一般程序员可能不知道的许多令人讨厌的问题。 这里有一些超出“我的本地variables将被销毁,每当我返回”的例子。

让我们从一个过于简单化的使用RAII的FileHandle类开始:

 class FileHandle { FILE* file; public: explicit FileHandle(const char* name) { file = fopen(name); if (!file) { throw "MAYDAY! MAYDAY"; } } ~FileHandle() { // The only reason we are checking the file pointer for validity // is because it might have been moved (see below). // It is NOT needed to check against a failed constructor, // because the destructor is NEVER executed when the constructor fails! if (file) { fclose(file); } } // The following technicalities can be skipped on the first read. // They are not crucial to understanding the basic idea of RAII. // However, if you plan to implement your own RAII classes, // it is absolutely essential that you read on :) // It does not make sense to copy a file handle, // hence we disallow the otherwise implicitly generated copy operations. FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; // The following operations enable transfer of ownership // and require compiler support for rvalue references, a C++0x feature. // Essentially, a resource is "moved" from one object to another. FileHandle(FileHandle&& that) { file = that.file; that.file = 0; } FileHandle& operator=(FileHandle&& that) { file = that.file; that.file = 0; return *this; } } 

如果构造失败(例外),则不会调用其他成员函数 – 甚至不是析构函数。

RAII避免使用处于无效状态的对象。 它甚至已经使我们甚至在使用这个对象之前变得更容易

现在,让我们看看临时对象:

 void CopyFileData(FileHandle source, FileHandle dest); void Foo() { CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest")); } 

有三种错误情况需要处理:无法打开文件,只能打开一个文件,可以打开两个文件,但复制文件失败。 在非RAII实施中, Foo将不得不明确处理所有三个案例。

即使在一个声明中获取多个资源,RAII也会释放所获取的资源。

现在,让我们汇总一些对象:

 class Logger { FileHandle original, duplex; // this logger can write to two files at once! public: Logger(const char* filename1, const char* filename2) : original(filename1), duplex(filename2) { if (!filewrite_duplex(original, duplex, "New Session")) throw "Ugh damn!"; } } 

如果original的构造函数失败(因为filename1无法打开), duplex的构造函数失败(因为filename2无法打开),或者Logger的构造函数体内的文件写入失败, Logger的构造函数将失败。 在这些情况下, Logger的析构函数都不会被调用 – 所以我们不能依赖Logger的析构函数来释放文件。 但是如果构build了original则在清理Logger构造函数时将调用其析构函数。

RAII简化了局部施工后的清理工作。


负面点:

消极点? 所有的问题都可以用RAII和智能指针来解决;-)

当你需要延迟采集时,RAII有时是笨拙的,把聚集的物体推到堆上。
想象一下,Logger需要一个SetTargetFile(const char* target) 。 在这种情况下,仍然需要成为Logger成员的句柄需要驻留在堆上(例如在一个智能指针中,以适当地触发句柄的销毁)。

我从来没有真正希望垃圾收集。 当我做C#的时候,我有时会感到一阵幸福,我只是不需要关心,但是我更想念所有可以通过确定性的破坏创造出来的酷玩具。 (使用IDisposable只是不削减它。)

我有一个特别复杂的结构,可能受益于GC,其中“简单”的智能指针会导致多个类的循环引用。 我们通过仔细平衡强弱指针来混淆视听,但是任何时候我们想要改变一些东西,我们都必须研究一个很大的关系图。 GC可能会更好,但是其中一些组件拥有应尽快发布的资源。


关于FileHandle示例的说明:它不是完整的,只是一个示例 – 但结果是不正确的。 感谢Johannes Schaub指出,并将FredOverflow转化为正确的C ++ 0x解决scheme。 随着时间的推移,我已经解决了这里logging的方法。

那里有优秀的答案,所以我只是添加一些遗忘的东西。

0. RAII是关于范围

RAII是关于两个:

  1. 获取构造函数中的资源(不pipe是什么资源),并在析构函数中取消它。
  2. 在声明variables时执行构造函数,当variables超出作用域时析构函数会自动执行。

其他人已经回答了,所以我不会详细说明。

1.在Java或C#编码时,您已经使用RAII …

MONSIEUR JOURDAIN:什么! 当我说:“妮可,把我的拖鞋拿来,给我睡帽,”那是散文?

哲学大师:是的,先生。

MONSIEUR JOURDAIN:四十多年来,我一直在讲散文,却对此毫不知情,所以我非常感谢你教给我。

– Molière:中产阶级绅士,第2幕,第4场

正如Jourdain先生散文一样,C#甚至是Java人都已经使用了RAII,但隐藏起来了。 例如,下面的Java代码(在C#中用相同的方式replacesynchronizedlock ):

 void foo() { // etc. synchronized(someObject) { // if something throws here, the lock on someObject will // be unlocked } // etc. } 

…已经在使用RAII:互斥量采集是在关键字( synchronizedlock )中完成的,并且在退出范围时将完成未获取。

即使对于从来没有听说过RAII的人来说,它也是很自然的。

C ++对Java和C#的优势在于,可以使用RAII来做任何事情。 例如,在C ++中没有直接的内build等价的synchronizedlock ,但我们仍然可以拥有它们。

在C ++中,将会写成:

 void foo() { // etc. { Lock lock(someObject) ; // lock is an object of type Lock whose // constructor acquires a mutex on // someObject and whose destructor will // un-acquire it // if something throws here, the lock on someObject will // be unlocked } // etc. } 

可以用Java / C#方式轻松编写(使用C ++macros):

 void foo() { // etc. LOCK(someObject) { // if something throws here, the lock on someObject will // be unlocked } // etc. } 

2. RAII有其他用途

白兔子:[唱歌]我迟到了/我迟到了/非常重要的一天。 /没时间说“你好。” / 再见。 /我迟到了,我迟到了,我迟到了。

– 爱丽丝梦游仙境(迪士尼版,1951)

你知道什么时候构造函数将被调用(在对象声明处),并且知道何时会调用其相应的析构函数(在作用域的出口处),所以你可以用一行代码来编写几乎神奇的代码。 欢迎来到C ++仙境(至less从C ++开发者的angular度来看)。

例如,你可以写一个计数器对象(我把它作为一个练习),并通过声明它的variables来使用它,就像使用上面的锁对象一样:

 void foo() { double timeElapsed = 0 ; { Counter counter(timeElapsed) ; // do something lengthy } // now, the timeElapsed variable contain the time elapsed // from the Counter's declaration till the scope exit } 

这当然也可以用Java / C#编写的方式使用macros:

 void foo() { double timeElapsed = 0 ; COUNTER(timeElapsed) { // do something lengthy } // now, the timeElapsed variable contain the time elapsed // from the Counter's declaration till the scope exit } 

3.为什么C ++ finally缺乏?

[SHOUTING]这是最后的倒计时!

– 欧洲:最后的倒计时(对不起,我是引号,这里… 🙂

finally子句在C#/ Java中用于在范围退出的情况下处理资源处理(通过return或抛出的exception)。

精明的规范读者会注意到C ++没有最后的条款。 这不是一个错误,因为C ++不需要它,因为RAII已经处理资源处置。 (相信我,编写C ++析构函数比编写正确的Java finally子句,甚至是C#的正确Dispose方法都容易得多。

不过,有时候, finally条款会很酷。 我们可以用C ++来完成吗? 我们可以! 再次使用RAII。

结论:在C ++中,RAII不仅仅是一个哲学:它是C ++

RAII? 这是C ++!

– C ++开发者愤怒的评论,被无名的斯巴达国王和他的300个朋友无耻复制

当你达到C ++的某个级别的经验时,你从RAII的angular度出发,就构造者和析构者自动执行而言

您开始考虑范围{}字符成为您的代码中最重要的字符。

几乎所有的事情都符合RAII的要求:exception安全,互斥锁,数据库连接,数据库请求,服务器连接,时钟,操作系统句柄等,以及最后但并非最不重要的内存。

数据库部分是不可忽略的,因为如果你接受支付价格,你甚至可以用“ 事务编程 ”风格编写 ,执行代码行和行,直到决定,最后,如果你想提交所有的变化,或者,如果不可能的话,将所有的变更恢复(只要每条线至less满足“powershell例外保证”)。 (请参阅Herb's Sutter文章的第二部分,了解事务性编程)。

而像一个难题,一切都合适。

RAII是C ++的一部分,如果没有它,C ++就不能成为C ++。

这就解释了为什么有经验的C ++开发人员对RAII如此痴迷,为什么RAII是他们尝试使用其他语言时首先要search的东西。

这也解释了为什么垃圾收集器本身就是一个巨大的技术,从C ++开发人员的angular度来看并不是那么令人印象深刻:

  • RAII已经处理了GC处理的大部分案件
  • GC比纯粹的托pipe对象的循环引用更好地处理(通过智能使用弱指针来减轻)
  • 仍然GC只限于内存,而RAII可以处理任何types的资源。
  • 如上所述,RAII可以做很多,更多…

请参见:

除了C ++之外,其他语言的程序员是否使用,了解或理解RAII?

RAII和C ++中的智能指针

C ++是否支持“终于”阻止? (我听说过这个“RAII”是什么?)

RAII与例外

等等..

RAII使用C ++析构函数语义来pipe理资源。 例如,考虑一个智能指针。 你有一个指针的参数化构造函数,用这个对象的地址初始化这个指针。 你在栈上分配一个指针:

 SmartPointer pointer( new ObjectClass() ); 

当智能指针超出范围时,指针类的析构函数将删除连接的对象。 指针是堆栈分配和对象堆分配。

有些情况下RAII不起作用。 例如,如果使用引用计数的智能指针(如boost :: shared_ptr)并创build一个具有周期的graphics结构,则面临内存泄漏的风险,因为周期中的对象将阻止彼此被释放。 垃圾收集将有助于此。

我同意cpitis。 但是想补充一下,资源可以是任何不只是内存。 资源可以是文件,关键部分,线程或数据库连接。

它被称为资源获取是初始化,因为当控制资源的对象被构​​build时获取资源,如果构造器失败(即由于例外),资源不被获取。 然后,一旦对象超出范围,资源被释放。 c ++保证已经成功构build的堆栈上的所有对象都将被破坏(即使超类构造函数失败,这也包括基类和成员的构造函数)。

RAII背后的理性是使资源获取exception安全。 无论发生什么exception,所获得的所有资源都能正确释放。 然而,这确实取决于获取资源的类的质量(这必须是exception安全的,而且很难)。

之前的回复,我想强调一下。

RAII, 资源获取是初始化意味着所有获得的资源都应该在对象初始化的情况下获得。 这禁止“裸”资源获取。 理由是C ++中的清理工作是基于对象的,而不是函数调用的基础。 因此,所有清理应该由对象完成,而不是函数调用。 从这个意义上说,C ++更多是面向对象的,例如Java。 Java清理是基于finally子句中的函数调用。

垃圾收集的问题在于你失去了对RAII至关重要的确定性破坏。 一旦variables超出范围,当对象被回收时,取决于垃圾收集器。 对象所持有的资源将继续保留,直到析构函数被调用。

RAII来自资源分配是初始化。 基本上,这意味着当构造函数完成执行时,构造的对象将被完全初始化并可以使用。 这也意味着析构函数将释放对象拥有的所有资源(如内存,操作系统资源)。

与垃圾收集语言/技术(例如Java,.NET)相比,C ++允许完全控制对象的生命周期。 对于一个堆栈分配的对象,你会知道什么时候该对象的析构函数将被调用(当执行超出范围时),在垃圾收集的情况下是不真正控制的东西。 即使在C ++中使用智能指针(例如boost :: shared_ptr),您也会知道,当没有对指向对象的引用时,该对象的析构函数将被调用。

而且你怎么能在堆栈上做些什么来清理堆上的东西呢?

 class int_buffer { size_t m_size; int * m_buf; public: int_buffer( size_t size ) : m_size( size ), m_buf( 0 ) { if( m_size > 0 ) m_buf = new int[m_size]; // will throw on failure by default } ~int_buffer() { delete[] m_buf; } /* ...rest of class implementation...*/ }; void foo() { int_buffer ib(20); // creates a buffer of 20 bytes std::cout << ib.size() << std::endl; } // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed. 

当int_buffer的一个实例出现时,它必须有一个大小,它将分配必要的内存。 当它超出范围,它的析构函数被调用。 这对于同步对象等事物非常有用。 考虑

 class mutex { // ... take(); release(); class mutex::sentry { mutex & mm; public: sentry( mutex & m ) : mm(m) { mm.take(); } ~sentry() { mm.release(); } }; // mutex::sentry; }; mutex m; int getSomeValue() { mutex::sentry ms( m ); // blocks here until the mutex is taken return 0; } // the mutex is released in the destructor call here. 

另外,有没有不能使用RAII的情况?

不,不是。

你有没有发现自己希望收集垃圾? 至less有一个垃圾收集器可以用于某些对象,同时让其他人被pipe理?

决不。 垃圾收集只能解决dynamic资源pipe理的一小部分。

这里已经有很多很好的答案,但是我只想补充一下:
对RAII的一个简单的解释是,在C ++中,当堆栈超出范围时,堆栈上分配的对象将被销毁。 这意味着,一个对象析构函数将被调用,并可以做所有必要的清理。
这意味着,如果创build的对象没有“新build”,则不需要“删除”。 这也是“智能指针”背后的思想 – 它们驻留在堆栈上,基本上包装了一个堆对象。

RAII是资源获取初始化的首字母缩略词。

这种技术对于C ++来说是非常独特的,因为它们支持构造函数和析构函数,并且几乎自动地支持与传入的参数相匹配的构造函数,在最坏的情况下,默认的构造函数被称为&析构函数。如果没有为C ++类显式地编写析构函数,则由C ++编译器添加的函数被调用。 这种情况只发生在自动pipe理的C ++对象上 – 意味着没有使用自由存储(使用new,new [] / delete,delete [] C ++运算符分配/释放内存)。

RAII技术利用这个自动pipe理的对象特性,通过使用new / new []显式地请求更多的内存来处理在堆/ free-store上创build的对象,这应该通过调用delete / delete [] 。 自动pipe理的对象的类将包装在堆/自由存储内存上创build的另一个对象。 因此,当自动pipe理对象的构造函数运行时,在堆/自由存储内存上创build包装对象,当自动pipe理对象的句柄超出范围时,自动调用该自动pipe理对象的析构函数,对象被删除使用删除。 使用OOP的概念,如果你把这些对象包装在私人范围内的另一个类中,你将无法访问被包装的类的成员和方法,这就是智能指针(aka句柄类)的devise原因。 这些智能指针通过允许调用由外部存储对象组成的任何成员/方法,将包装的对象作为types对象公开给外部世界。 请注意,智能指针根据不同的需求有不同的风格。 您应该参考Andrei Alexandrescu编写的Modern C ++编程或者增强库(www.boostorg)shared_ptr.hpp实现/文档以了解更多信息。 希望这有助于你理解RAII。