跳转到内容

C++ 编程/RAII

来自维基教科书,开放的书籍,开放的世界

资源获取即初始化 (RAII)

[编辑 | 编辑源代码]

RAII 技术通常用于在多线程应用程序中控制线程锁。另一个典型的 RAII 示例是文件操作,例如 C++ 标准库中的文件流。输入文件流在对象的构造函数中打开,并在对象销毁时关闭。由于 C++ 允许在 堆栈 上分配对象,因此 C++ 的作用域机制可用于控制文件访问。

使用 RAII,我们可以使用类析构函数来保证清理,类似于其他语言中的 finally 关键字。这样做可以自动执行任务,从而避免错误,但也提供了不使用它的自由。

RAII 也用于(如以下示例所示)确保异常安全。RAII 使得在没有大量使用 try/catch 块的情况下避免资源泄漏成为可能,并在软件行业中得到广泛使用。

动态分配内存(使用new)的拥有权可以使用 RAII 控制。为此,C++ 标准库定义了 auto ptr。此外,共享对象的生存期可以通过具有共享所有权语义的智能指针来管理,例如 C++ 中由 Boost 库 定义的 boost::shared_ptrLoki 库 中基于策略的 Loki::SmartPtr

以下 RAII 类是对 C 标准库文件系统调用的轻量级包装器。

 #include <cstdio>
 
 // exceptions
 class file_error { } ;
 class open_error : public file_error { } ;
 class close_error : public file_error { } ;
 class write_error : public file_error { } ;
   
 class file
 {
 public:
     file( const char* filename )
         :
         m_file_handle(std::fopen(filename, "w+"))
     {
         if( m_file_handle == NULL )
         {
             throw open_error() ;
         }
     }
   
     ~file()
     {
         std::fclose(m_file_handle) ;
     }
 
     void write( const char* str )
     {
         if( std::fputs(str, m_file_handle) == EOF )
         {
             throw write_error() ;
         }
     }
  
     void write( const char* buffer, std::size_t num_chars )
     {
         if( num_chars != 0
             &&
             std::fwrite(buffer, num_chars, 1, m_file_handle) == 0 )
         {
             throw write_error() ;
         }
     }
 
 private:
     std::FILE* m_file_handle ;
       
     // copy and assignment not implemented; prevent their use by
     // declaring private.
     file( const file & ) ;
     file & operator=( const file & ) ;
 } ;

此 RAII 类可以按如下方式使用 

void example_with_RAII()
{
  // open file (acquire resource)
  file logfile("logfile.txt") ;
   
  logfile.write("hello logfile!") ;
  // continue writing to logfile.txt ...
   
  // logfile.txt will automatically be closed because logfile's
  // destructor is always called when example_with_RAII() returns or
  // throws an exception.
}

如果不使用 RAII,每个使用输出日志的函数都必须显式管理文件。例如,不使用 RAII 的等效实现如下

void example_without_RAII()
{
  // open file
  std::FILE* file_handle = std::fopen("logfile.txt", "w+") ;
 
  if( file_handle == NULL )
  {
    throw open_error() ;
  }
  
  try
  {

    if( std::fputs("hello logfile!", file_handle) == EOF )
    {
      throw write_error() ;
    }
  
    // continue writing to logfile.txt ... do not return
    // prematurely, as cleanup happens at the end of this function
  }
  catch(...)
  {
    // manually close logfile.txt
    std::fclose(file_handle) ;
 
    // re-throw the exception we just caught
    throw ;
  }
  
  // manually close logfile.txt 
  std::fclose(file_handle) ;
}

如果 fopen()fclose() 可能抛出异常,则 fileexample_without_RAII() 的实现将变得更加复杂;但是,example_with_RAII() 不会受到影响。

RAII 习惯用法的主要思想是类 file 封装了任何有限资源的管理,例如 FILE* 文件句柄。它保证在函数退出时资源将被正确释放。此外,file 实例保证一个有效的日志文件可用(如果文件无法打开,则抛出异常)。

在存在异常的情况下,还有一个大问题:在 example_without_RAII() 中,如果分配了多个资源,但在它们的分配之间抛出异常,则没有普遍的方法可以知道在最终的 catch 块中需要释放哪些资源 - 并且释放未分配的资源通常是一件坏事。RAII 可以解决这个问题;自动变量以与它们构造相反的顺序销毁,并且只有在对象完全构造后(构造函数内部没有抛出异常)才销毁该对象。因此,example_without_RAII() 永远不可能像 example_with_RAII() 那样安全,除非对每种情况进行特殊编码,例如检查无效的默认值或嵌套 try-catch 块。事实上,应该注意的是,example_without_RAII() 在本文的早期版本中包含资源错误。

这使得 example_with_RAII() 不必像以前那样显式管理资源。当多个函数使用 file 时,这将简化和减少总体代码大小,并有助于确保程序正确性。

example_without_RAII() 类似于在非 RAII 语言(如 Java)中用于资源管理的习惯用法。虽然 Java 的 try-finally 块允许正确释放资源,但负担仍然落在程序员身上,以确保正确行为,因为每个使用 file 的函数都可能显式地要求使用 try-finally 块销毁日志文件。

华夏公益教科书