为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

C++ C
慕桂英546537 2019-07-29 15:36:46
理解术语和概念的含义 - RAII(资源获取是初始化)您能否请C ++开发人员详细介绍RAII是什么,为什么重要,以及它是否与其他语言有任何关联?我做知道一点点。我相信它代表“资源获取是初始化”。但是,这个名称并不符合我对RAII的理解(可能不正确):我得到的印象是RAII是一种初始化堆栈上对象的方式,当这些变量超出范围时,析构函数会自动被称为导致资源被清理。那么为什么不称为“使用堆栈触发清理”(UTSTTC :)?你怎么从那里到“RAII”?你怎么能在堆栈上创建一些东西来清理堆上的东西呢?此外,是否有不能使用RAII的情况?你有没有发现自己希望收集垃圾?至少一个垃圾收集器,你可以使用一些对象,同时让其他人管理?谢谢。
查看完整描述

3 回答

?
慕桂英3389331

TA贡献2036条经验 获得超8个赞

那么为什么不称为“使用堆栈触发清理”(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解决方案。随着时间的推移,我已经解决了这里记录的方法。


查看完整回答
反对 回复 2019-07-29
?
慕容708150

TA贡献1831条经验 获得超4个赞

那里有很好的答案,所以我只是添加了一些被遗忘的东西。

0. RAII是关于范围的

RAII是关于两者:

  1. 获取构造函数中的资源(无论什么资源),并在析构函数中取消它。

  2. 在声明变量时执行构造函数,并在变量超出范围时自动执行析构函数。

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

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

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

哲学硕士:是的,先生。

MONSIEUR JOURDAIN:四十多年来,我一直在讲述散文而不知道任何事情,我非常感谢你教我这个。

- 莫里哀:中产阶级绅士,第2幕,场景4

正如Jourdain先生用散文所做的那样,C#甚至Java人已经使用RAII,但却是隐藏的方式。例如,下面的Java代码(这是通过替换在C#编写的相同方式synchronizedlock):

void foo(){
   // etc.

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

   // etc.}

...已经在使用RAII:互斥锁获取在关键字(synchronizedlock)中完成,取消将在退出范围时完成。

它的符号非常自然,即使是从未听说过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.}

2. RAII有其他用途

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

- 爱丽丝梦游仙境(迪士尼版,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}

3.为什么C ++缺乏finally

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

- 欧洲:最后的倒计时(抱歉,我没有引号,这里...... :-)

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

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

不过,有时,一个finally条款会很酷。我们可以用C ++做吗?我们可以!再次使用RAII。

结论:RAII不仅仅是C ++中的哲学:它是C ++

RAII?这是C ++ !!!

- C ++开发人员愤怒的评论,被一位不起眼的斯巴达国王和他的300个朋友无耻地复制

当您在C ++中达到某种程度的经验时,就开发人员和析构函数自动执行而言,您开始考虑RAII

你开始在思维范围,以及{}人物成为在代码中最重要的人。

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

数据库部分是不可忽略的,因为,如果您接受支付价格,您甚至可以用“ 事务编程 ”方式编写,执行代码行和代码行,直到最终确定是否要提交所有更改,或者,如果不可能,将所有更改都还原(只要每行至少满足强异常保证)。(参见Herb的Sutter关于事务编程的文章的第二部分)。

就像拼图一样,一切都很合适。

RAII是C ++的重要组成部分,如果没有它,C ++就不可能是C ++。

这解释了为什么有经验的C ++开发人员如此迷恋RAII,以及为什么RAII是他们在尝试使用其他语言时首先搜索的内容。

它解释了为什么垃圾收集器本身就是一项非常出色的技术,从C ++开发人员的角度来看并不那么令人印象深刻:

  • RAII已经处理了GC处理的大多数案例

  • GC在纯托管对象上使用循环引用比RAII更好(通过智能使用弱指针缓解)

  • GC仍然限于内存,而RAII可以处理任何类型的资源。

  • 如上所述,RAII可以做很多事情......


查看完整回答
反对 回复 2019-07-29
  • 3 回答
  • 0 关注
  • 587 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信