跳轉到內容

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 塊銷燬日誌檔案。

華夏公益教科書