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

时间:2009-04-03 05:21:53

标签: c++ garbage-collection raii resource-management

请问C ++开发人员能否详细说明RAII是什么,为什么重要,以及它是否与其他语言有任何关联?

知道一点。我相信它代表“资源获取是初始化”。但是,这个名称并不符合我对RAII的理解(可能不正确):我得到的印象是RAII是一种初始化堆栈上对象的方式,当这些变量超出范围时,析构函数会自动被称为导致资源被清理。

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

你怎么能在堆栈上创建一些东西来清除堆上的东西呢?此外,是否有不能使用RAII的情况?你有没有发现自己希望收集垃圾?至少有一个垃圾收集器可以用来管理某些对象,同时管理其他对象吗?

感谢。

11 个答案:

答案 0 :(得分:129)

  

那么为什么不是“使用堆栈触发清理”(UTSTTC :)?

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

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

在C ++中,由于异常和(C ++样式)模板的组合,资源管理特别复杂。如需了解情况,请参阅GOTW8)。


C ++保证析构函数被称为当且仅当构造函数成功时。依靠这一点,RAII可以解决普通程序员可能甚至不知道的许多令人讨厌的问题。除了“每当我返回时我的局部变量将被销毁”之外,还有一些例子。

让我们从使用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!";
    }
}

Logger的构造函数失败导致original的构造函数失败(因为filename1无法打开),duplex的构造函数失败(因为{{1}无法打开),或写入filename2的构造函数体内的文件失败。在任何这些情况下,Logger的析构函数都将被调用 - 因此我们不能依赖Logger的析构函数来释放文件。但是如果构造了Logger,那么在清除original构造函数时将调用它的析构函数。

RAII简化了部分构建后的清理工作。


否定点:

否定点?使用RAII和智能指针可以解决所有问题;-)

当您需要延迟获取,将聚合对象推送到堆上时,RAII有时难以处理 想象一下Logger需要一个Logger。在这种情况下,仍然需要成为SetTargetFile(const char* target)成员的句柄需要驻留在堆上(例如,在智能指针中,以适当地触发句柄的破坏。)

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

我有一个特别复杂的结构可能从GC中获益,其中“简单”智能指针会导致多个类的循环引用。我们通过仔细平衡强弱指针而陷入困境,但无论何时我们想要改变某些东西,我们都必须研究一个大关系图。 GC可能更好,但有些组件拥有应该尽快发布的资源。


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

答案 1 :(得分:43)

答案 2 :(得分:16)

答案 3 :(得分:10)

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

SmartPointer pointer( new ObjectClass() );

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

在某些情况下,RAII没有帮助。例如,如果使用引用计数智能指针(如boost :: shared_ptr)并创建具有循环的图形结构,则可能面临内存泄漏,因为循环中的对象将阻止彼此释放。垃圾收集有助于解决这个问题。

答案 4 :(得分:8)

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

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

RAII背后的理性是使资源获取异常安全。无论异常发生在何处,所有获得的资源都会被正确释放。然而,这确实依赖于获取资源的类的质量(这必须是异常安全的,这很难)。

答案 5 :(得分:8)

我想比之前的回复更加强烈。

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

答案 6 :(得分:7)

垃圾收集的问题在于你失去了对RAII至关重要的确定性破坏。一旦变量超出范围,就会在回收对象时由垃圾收集器决定。在调用析构函数之前,对象持有的资源将继续保留。

答案 7 :(得分:4)

RAII来自资源分配正在初始化。基本上,这意味着当构造函数完成执行时,构造的对象被完全初始化并准备使用。它还意味着析构函数将释放该对象拥有的任何资源(例如,内存,OS资源)。

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

答案 8 :(得分:3)

  

你怎么能在堆栈上创建一些东西来清除堆上的东西呢?

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的情况?

不,不是真的。

  

你有没有发现自己希望收集垃圾?至少有一个垃圾收集器可以用来管理某些对象,同时管理其他对象吗?

从不。垃圾收集只解决了动态资源管理的一小部分。

答案 9 :(得分:2)

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

答案 10 :(得分:1)

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

这种技术对C ++来说非常独特,因为它们支持构造函数和结构。 Destructors&amp;几乎自动地匹配传入的参数的构造函数或最坏情况下的默认构造函数被调用&amp;如果提供了显式提供的析构函数,否则如果没有为C ++类显式编写析构函数,则会调用由C ++编译器添加的默认析构函数。这仅适用于自动管理的C ++对象 - 这意味着不使用免费存储(使用new,new [] / delete,delete [] C ++运算符分配/释放内存)。

RAII技术利用这个自动管理对象功能来处理在堆/免费存储上创建的对象,通过使用new / new []来请求更多内存,应该通过调用delete /来明确销毁删除[]。自动管理对象的类将包装在堆/自由存储内存上创建的另一个对象。因此,当运行自动管理对象的构造函数时,将在堆/自由存储存储器&amp;存储器上创建包装对象。当自动管理对象的句柄超出范围时,将自动调用该自动管理对象的析构函数,其中使用delete销毁包装对象。使用OOP概念,如果将这些对象包装在私有范围内的另一个类中,则无法访问包装类成员&amp;方法和这就是智能指针(也就是句柄类)的设计原因。这些智能指针将包装对象作为打字对象暴露给外部世界&amp;允许调用暴露的内存对象组成的任何成员/方法。请注意,智能指针具有基于不同需求的各种风格。您应该参考Andrei Alexandrescu的现代C ++编程或boost库(www.boostorg)的shared_ptr.hpp实现/文档来了解它的更多信息。希望这有助于您了解RAII。