1. 程式人生 > >C++中RAII機制的介紹與簡單例項

C++中RAII機制的介紹與簡單例項

今天看陳碩的多執行緒書上提到了C++中RAII技術的使用,通過用C11裡面自帶的智慧指標來完成對資源的控制,但是一直不太清楚具體RAII是怎麼樣的,帶著這樣的疑問,特地去看了幾篇部落格,找了一個簡單的檔案控制代碼開啟關閉RAII管理的例項,瞬間就明白了,這裡分享出來。主要從兩個部分,首先是RAII技術的介紹,然後是RAII技術的簡單例項。

RAII技術的介紹

1、RAII定義

RAII,它是“Resource Acquisition Is Initialization”的首字母縮寫。也稱為“資源獲取就是初始化”,是c++等程式語言常用的管理資源、避免記憶體洩露的方法。它保證在任何情況下,使用物件時先構造物件,最後析構物件。

RAII的好處在於它提供了一種資源自動管理的方式,當產生異常、回滾等現象時,RAII可以正確地釋放掉資源。

當講述C++資源管理時,Bjarne這樣寫道:

使用區域性物件管理資源的技術通常稱為“資源獲取就是初始化”。這種通用技術依賴於建構函式和解構函式的性質以及它們與異常處理的互動作用。

2、RAII的理解

看到上面RAII的介紹,發現這不就是棧的資源回收過程中嗎??

沒錯,思想的確是一樣的。

我們知道在函式內部的一些成員是放置在棧空間上的,當函式返回時,這些棧上的區域性變數就會立即釋放空間,於是Bjarne Stroustrup就想到確保能執行資源釋放程式碼的地方就是在這個程式段(棧)中放置的物件的析構函數了,因為stack winding會保證它們的解構函式都會被執行。RAII就利用了棧裡面的變數的這一特點。

作業系統中棧的操作過程
Stack Winding & Unwinding
當程式執行時,每一個函式(包括資料、暫存器、程式計數器,等等)在呼叫時,都被對映到棧上。這就是 stack winding。
Unwinding 是以相反順序把函式從棧上移除的過程。
正常的 stack unwinding 發生在函式返回時;不正常的情況,比如引發異常,呼叫setjmp和longjmp,也會導致 stack unwinding。
可見 stack unwinding 的過程中,區域性物件的解構函式將逐一被呼叫。這也就是 RAII 工作的原理,它是由語言和編譯器來保證的。

3、RAII做法

RAII 的一般做法是這樣的:在物件構造時獲取資源,接著控制對資源的訪問使之在物件的生命週期內始終保持有效,最後在物件析構的時候釋放資源。藉此,我們實際上把管理一份資源的責任託管給了一個存放在棧空間上的區域性物件。
這種做法有兩大好處:
(1)不需要顯式地釋放資源。
(2)採用這種方式,物件所需的資源在其生命期內始終保持有效。

那麼給出了RAII的做法,問題就是什麼是資源,我們來明確資源的概念,在計算機系統中,資源是數量有限且對系統正常運轉具有一定作用的元素。

比如,記憶體,檔案控制代碼,網路套接字(network sockets),互斥鎖(mutex locks)等等,它們都屬於系統資源。

由於資源的數量不是無限的,有的資源甚至在整個系統中僅有一份,因此我們在使用資源時必須嚴格遵循的步驟是:

獲取資源
使用資源
釋放資源

總結
使用RAII,需要自己定義資源類,將自己業務的操作資源封裝起來,然後通過這個資源類來完成資源的構造、使用、釋放。這樣讓我們可以放心的去編寫邏輯功能的程式碼,而不用去關心會不會造成記憶體洩漏這樣的問題。

RAII技術的簡單例項

正如上面介紹的RAII技術,這裡我們給出一個簡單的檔案控制代碼開啟關閉RAII管理的例項。
一個簡單的控制代碼檔案開啟關閉的程式

void Func() 
{ 
  FILE *fp; 
  char* filename = "test.txt"; 
  if((fp=fopen(filename,"r"))==NULL) 
  { 
      printf("not open"); 
      exit(0); 
  } 
  ... // 如果 在使用fp指標時產生異常 並退出 
       // 那麼 fp檔案就沒有正常關閉 
       
  fclose(fp); 
}

在資源的獲取到釋放之間,我們往往需要使用資源,但常常一些不可預計的異常是在使用過程中產生,就會使資源的釋放環節沒有得到執行。

可能我們會在每個分支上進行關閉操作,來保證資源的正常釋放。

  FILE *fp; 
  char* filename = "test.txt"; 
  if((fp=fopen(filename,"r"))==NULL) 
  { 
      printf("not open"); 
      exit(0); 
  } 
  if (.....)
    {
        fclose(fp);			//在每個分支上都關閉fp
        return;
    }
    else if(.....)
    {
        fclose(fp);			//在每個分支上都關閉fp
        return;
    }
...
  fclose(fp); 

使用這種方法,每個分支你都要去釋放資源,不僅程式碼會很冗餘,同時可能在分支較多的情況下,你會忘記釋放,這是十分常見的現象。
此時,就可以讓RAII慣用法大顯身手了。

使用RAII去管理資源的建立釋放
RAII的實現原理很簡單,利用stack上的臨時物件生命期是程式自動管理的這一特點,將我們的資源釋放操作封裝在一個臨時物件中。
例如:

class Resource{}; 
class RAII{ 
public: 
    RAII(Resource* aResource):r_(aResource){} //獲取資源 
    ~RAII() {delete r_;} //釋放資源 
    Resource* get()    {return r_ ;} //訪問資源 
private: 
    Resource* r_; 
}; 

那麼對於上述的檔案控制代碼開啟關閉的例子,我們可以寫成如下的資源類:

class FileRAII{  
public:  
    FileRAII(FILE* aFile):file_(aFile){}  
    ~FileRAII() { fclose(file_); }//在解構函式中進行檔案關閉  
    FILE* get() {return file_;}  
private:  
    FILE* file_;  
}; 

則上面這個開啟檔案的例子就可以用RAII改寫為:

class FileRAII{  
public:  
    FileRAII(FILE* aFile):file_(aFile){}  
    ~FileRAII() { fclose(file_); }//在解構函式中進行檔案關閉  
    FILE* get() {return file_;}  
private:  
    FILE* file_;  
}; 

void Func()  
{  
  FILE *fp;  
  char* filename = "test.txt";  
  if((fp=fopen(filename,"r"))==NULL)  
  {  
      printf("not open");  
      exit(0);  
  }  
  FileRAII fileRAII(fp);  
  ... // 如果 在使用fp指標時產生異常 並退出  
       // 那麼 fileRAII在棧展開過程中會被自動釋放,解構函式也就會自動地將fp關閉  
 
  // 即使所有程式碼是都正確執行了,也無需手動釋放fp,fileRAII它的生命期在此結束時,它的解構函式會自動執行!      
 } 

這就是RAII的魅力,它免除了對需要謹慎使用資源時而產生的大量維護程式碼。在保證資源正確處理的情況下,還使得程式碼的可讀性也提高了不少。

參考部落格:
https://www.cnblogs.com/jiangbin/p/6986511.html
https://www.cnblogs.com/Allen-rg/p/6891971.html
https://blog.csdn.net/wozhengtao/article/details/52187484
https://blog.csdn.net/gettogetto/article/details/60878776

ps:這裡使用一個簡單的例子說明,沒有涉及文章最開始說到的C11裡面的智慧指標,具體怎麼將智慧指標加入到這個RAII機制中來,我也在學習,後續。。。。。