您能否请C++开发人员详细介绍RAII是什么,为什么重要,以及它是否与其他语言有任何关联?
我做知道一点点.我相信它代表"资源获取是初始化".但是,这个名称并不符合我对RAII的理解(可能不正确):我得到的印象是RAII是一种初始化堆栈中对象的方式,当这些变量超出范围时,析构函数会自动被称为导致资源被清理.
那么为什么不称为"使用堆栈触发清理"(UTSTTC :)?你怎么从那里到"RAII"?
你怎么能在堆栈上创建一些东西来清理堆上的东西呢?此外,是否有不能使用RAII的情况?你有没有发现自己希望收集垃圾?至少一个垃圾收集器,你可以使用一些对象,同时让其他人管理?
谢谢.
那么为什么不称为"使用堆栈触发清理"(UTSTTC :)?
RAII告诉你该怎么做:在构造函数中获取你的资源!我会添加:一个资源,一个构造函数.UTSTTC只是其中的一个应用,RAII更多.
资源管理很糟糕.在这里,资源是在使用后需要清理的任何东西.对许多平台上的项目进行的研究表明,大多数错误都与资源管理有关 - 而且在Windows上尤其糟糕(由于有许多类型的对象和分配器).
在C++中,由于异常和(C++样式)模板的组合,资源管理特别复杂.如需了解引擎盖,请参阅GOTW8).
C++保证当且仅当构造函数成功时才调用析构函数.依靠这一点,RAII可以解决普通程序员可能甚至不知道的许多令人讨厌的问题.除了"每当我返回时我的局部变量将被销毁"之外,还有一些例子.
让我们从FileHandle
使用RAII 的过于简单化的课程开始:
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!"; } }
的构造Logger
将失败original
的构造失败(因为filename1
无法打开)duplex
的构造失败(因为filename2
无法打开),或内写入文件Logger
的构造体失败.在任何这些情况下,Logger
都不会调用析构函数- 所以我们不能依赖Logger
析构函数来释放文件.但是如果original
被构造,它的析构函数将在Logger
构造函数的清理期间被调用.
RAII简化了部分施工后的清理工作.
否定点:
否定点?使用RAII和智能指针可以解决所有问题;-)
当您需要延迟获取时,RAII有时会变得难以处理,将聚合对象推送到堆上.
想象一下Logger需要一个SetTargetFile(const char* target)
.在这种情况下,仍然需要成为其成员的句柄Logger
需要驻留在堆上(例如,在智能指针中,以适当地触发句柄的破坏.)
我真的不希望收集垃圾.当我做C#时,我有时会感到一阵幸福,我不需要关心,但更多的是我想念所有可以通过确定性破坏创造的酷玩具.(使用IDisposable
只是不削减它.)
我有一个特别复杂的结构可能从GC中获益,其中"简单"智能指针会导致多个类的循环引用.我们通过仔细平衡强弱指针而陷入困境,但无论何时我们想要改变某些东西,我们都必须研究一个大关系图.GC可能会更好,但是一些组件拥有应该尽快发布的资源.
关于FileHandle示例的注释:它不是完整的,只是一个示例 - 但结果不正确.感谢Johannes Schaub指出并将FredOverflow转变为正确的C++ 0x解决方案.随着时间的推移,我已经解决了这里记录的方法.
那里有很好的答案,所以我只是添加了一些被遗忘的东西.
RAII是关于两者:
获取构造函数中的资源(无论什么资源),并在析构函数中取消它.
在声明变量时执行构造函数,并在变量超出范围时自动执行析构函数.
其他人已经回答了这个问题,所以我不会详细说明.
MONSIEUR JOURDAIN:什么!当我说,"妮可,把我的拖鞋带给我,给我睡帽,"这是散文?
哲学硕士:是的,先生.
MONSIEUR JOURDAIN:四十多年来,我一直在讲述散文而不知道任何事情,我非常感谢你教我这个.
- 莫里哀:中产阶级绅士,第2幕,场景4
正如Jourdain先生用散文所做的那样,C#甚至Java人已经使用RAII,但却是隐藏的方式.例如,下面的Java代码(这是通过替换在C#编写的相同方式synchronized
用lock
):
void foo() { // etc. synchronized(someObject) { // if something throws here, the lock on someObject will // be unlocked } // etc. }
...已经在使用RAII:互斥锁获取在关键字(synchronized
或lock
)中完成,取消将在退出范围时完成.
它的符号非常自然,即使是从未听说过RAII的人也几乎不需要解释.
C++在Java和C#方面的优势在于可以使用RAII进行任何操作.例如,有没有直接内建等效的synchronized
,也没有lock
在C++中,但我们仍然可以拥有它们.
在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++宏):
void foo() { // etc. LOCK(someObject) { // if something throws here, the lock on someObject will // be unlocked } // etc. }
白兔:[唱歌]我迟到/我迟到/非常重要的约会./没时间说"你好."/再见./我迟到了,我迟到了,我迟到了.
- 爱丽丝梦游仙境(迪士尼版,1951年)
你知道什么时候会调用构造函数(在对象声明中),并且你知道何时会调用它相应的析构函数(在作用域的出口处),所以你可以用一行来编写几乎神奇的代码.欢迎来到C++仙境(至少从C++开发人员的角度来看).
例如,您可以编写一个计数器对象(我将其作为练习)并仅通过声明其变量来使用它,就像上面使用的锁对象一样:
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#方式:
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 }
finally
?[SHOUTING]这是最后的倒计时!
- 欧洲:最后的倒计时(抱歉,我没有引号,这里...... :-)
该finally
子句在C#/ Java中用于处理范围退出时的资源处理(通过return
抛出异常或抛出异常).
精明的规范读者会注意到C++没有finally子句.这不是错误,因为C++不需要它,因为RAII已经处理了资源处理.(相信我,编写C++析构函数比编写正确的Java finally子句,甚至是C#的正确Dispose方法更容易).
不过,有时,一个finally
条款会很酷.我们可以用C++做吗?我们可以!再次使用RAII.
RAII?这是C++ !!!
- C++开发人员愤怒的评论,被一位不起眼的斯巴达国王和他的300个朋友无耻地复制
当您在C++中达到某种程度的经验时,就开发人员和析构函数自动执行而言,您开始考虑RAII.
你开始在思维范围,以及{
和}
人物成为在代码中最重要的人.
几乎所有东西都适合RAII:异常安全,互斥,数据库连接,数据库请求,服务器连接,时钟,操作系统句柄等,以及最后但并非最不重要的内存.
数据库部分是不可忽略的,因为,如果您接受支付价格,您甚至可以用" 事务编程 "方式编写,执行代码行和代码行,直到最终确定是否要提交所有更改,或者,如果不可能,将所有更改都还原(只要每行至少满足强异常保证).(参见Herb的Sutter关于事务编程的文章的第二部分).
就像拼图一样,一切都很合适.
RAII是C++的重要组成部分,如果没有它,C++就不可能是C++.
这解释了为什么有经验的C++开发人员如此迷恋RAII,以及为什么RAII是他们在尝试使用其他语言时首先搜索的内容.
它解释了为什么垃圾收集器本身就是一项非常出色的技术,从C++开发人员的角度来看并不那么令人印象深刻:
RAII已经处理了GC处理的大多数案例
GC在纯托管对象上使用循环引用比RAII更好(通过智能使用弱指针缓解)
GC仍然限于内存,而RAII可以处理任何类型的资源.
如上所述,RAII可以做很多事情......
请参阅:
除了C++之外,其他语言的程序员是否使用,了解或了解RAII?
RAII和C++中的智能指针
C++是否支持'finally'块?(我听到的'RAII'是什么?)
RAII与例外
等等..
RAII正在使用C++析构函数语义来管理资源.例如,考虑智能指针.你有一个参数化的指针构造函数,它使用object的地址初始化这个指针.您在堆栈上分配一个指针:
SmartPointer pointer( new ObjectClass() );
当智能指针超出范围时,指针类的析构函数将删除连接的对象.指针是堆栈分配的,对象是堆分配的.
在某些情况下,RAII没有帮助.例如,如果使用引用计数智能指针(如boost :: shared_ptr)并创建具有循环的图形结构,则可能面临内存泄漏,因为循环中的对象将阻止彼此释放.垃圾收集有助于解决这个问题.
我同意cpitis.但是想补充一点,资源可以是任何东西,而不仅仅是内存.资源可以是文件,关键部分,线程或数据库连接.
它被称为资源获取是初始化,因为在构造控制资源的对象时获取资源,如果构造函数失败(即由于异常),则不获取资源.然后,一旦对象超出范围,资源就会被释放.c ++保证堆栈中已成功构造的所有对象都将被破坏(这包括基类和成员的构造函数,即使超类构造函数失败).
RAII背后的理性是使资源获取异常安全.无论异常发生在何处,所有获得的资源都会被正确释放.然而,这确实依赖于获取资源的类的质量(这必须是异常安全的,这很难).
我想比以前的回复更强烈一点.
RAII,资源获取是初始化意味着应该在对象初始化的上下文中获取所有获取的资源.这禁止"裸"资源获取.理由是C++中的清理工作基于对象,而不是函数调用.因此,所有清理都应该由对象完成,而不是函数调用.从这个意义上讲,C++更面向对象,例如Java.Java清理基于finally
子句中的函数调用.
垃圾收集的问题在于你失去了对RAII至关重要的确定性破坏.一旦变量超出范围,就会在回收对象时由垃圾收集器决定.在调用析构函数之前,对象持有的资源将继续保留.