天天看點

淺談C++ 之RAII

一、何為RAII

RAII(Resource Acquisition is Initialization)——“資源取得時即初始化”。這是一種資源管理的觀念,一般通過資源管理類來實作。其精髓在于,在構造函數中獲得資源,在析構函數中釋放資源。這裡的資源包括記憶體、檔案句柄、網絡連接配接、互斥量等等。 

二、為什麼要用RAII

請看如下代碼:

void function()
{
Resource* pRc = createResource();
...
delete pRc;
}
           

pRc 是指向某資源的指針。我們本意是希望在function中初始化一個資源,利用該資源執行某些運算,最後釋放資源。然而"delete pRc;"并不是總能執行到,如果在操作時發生了什麼異常,或者别的什麼事,比如很愚蠢的使用了goto語句,或者也許這個資源在某個循環内,而這個循環在某種情況下執行了continue。總之隻要發生能讓"delete pRc"不執行的情況,這個資源就會得不到釋放,這絕不是我們所樂于見到的。

當然也許有人會考慮用下面的方法解決異常的問題。

void function()
{
try
{
Resource* pRc = createResource();
...
delete pRc;
}catch(...){  delete pRc; }
}
           

這也許不失為一種方法,然後考慮到日後維護時,維護程式員也許會在不完全了解這個函數對資源進行管理的用意下加入某些代碼。如果這些代碼引起了不在catch内聲明的異常,那麼資源仍舊會遇到沒有釋放的情況。為此我們引入用對象管理資源的思想。

三、資源管理類

資源管理類的基本構造并不複雜。一個private的原始資源字段,一個能夠初始化原始資源的構造函數以及一個釋放資源用的析構函數。基本思想上文已經提及,在構造函數中獲得資源,在析構函數中釋放資源。還是考慮上面的代碼,這次我們有一個資源管理類Manager。

class Manager
{
private:Resource* pRc ;
public:Manager(Resource* pRc1);
~Manager();
...
 }
public Manager();
Manager::Manager(Resource* pRc1)
{
pRc = pRc1;
}
Manager::~Manager()
{
    try
{
delete pRc ;
}catch(...)
{
delete pRc ;
}
}
void function()
{
Manager(createResource());
...
}
           

利用Manager管理Resource的好處在于不論function的生命周期是如何結束的,Manager的析構函數總是會被調用,資源會被釋放。當然如果遇到異常就不太好辦了,關于這點會在下面提及如何解決。

四、常用的RAII實作

對于指針的管理,實際上C++的庫已經為我們提供了不少智能智能。例如std::auto_ptr。

上面的function我們可以這麼改寫:

void function()
{
std::auto_ptr<Resource> pRc(createResource());
...
}
           

當function結束的時候,我們借由std::auto_ptr的析構函數将資源Resource釋放掉。這也是我們寫自己的資源管理類時依仗的思想——利用自動調用的析構函數來釋放資源。

然而std::auto_ptr的使用需要注意以下幾點:

1、别用多個std::auto_ptr指向同一個資源,這會導緻同一資源“多次”被 delete,會發生什麼你懂的。

2、不要對std::auto_ptr使用指派或者copy函數,否則原來的那個就會被置為null。這時候如果你再使用原來的std::auto_ptr,那麼恭喜你,中獎了。

(C++ 11中對auto_ptr做了一些修複)

為此推薦使用另一款智能指針——RCSP(referene-counting smart pointer),引用計數型智能指針。該指針的行為有點像Java對于reference的管理(與C++的reference不同,行為接近這裡提到的智能指針),它持續追蹤統計公有多少個對象在管理某筆資源,并在無人指向時,釋放該資源。學過Java或者.net托管的童鞋對于這種近似垃圾回收器(gargbage collection)的行為一定不陌生。

不過C++的世界從來就沒有最優方案,隻有最适用方案。RCSP同樣有其弊病,當遇到環狀引用(兩個實際沒有被使用卻彼此互相指着)時它就束手無策了。

tr1:shared_ptr就是個RCSP。(注:TR1 是std的拓展,全稱為std::tr1,如果标準庫不能滿足你的話,推薦去boost上找找别人寫的庫,也許會有驚喜)。這裡我們再次改寫function:

void function()
{
tr1:shared_ptr<Resource> pRc(createResource());
...
}
           

五、RAII簡單分類

RAII根據資源擷取方式可以分為:外部初始化的RAII與内部初始化的RAII兩種。其中後者實作起來較為簡單,std::string就是一個内部初始化的RAII,它把底層的char數組封裝了,資源對于外部程式來說是不可見的。上文中提到的std::auto_ptr與tr1:shared_ptr都是外部初始化RAII,我們可以通過給其構造函數傳參的方式給予被管理資源。

六、使用RAII的注意事項

注意copy行為。Copy行為的發生通常是隐晦的,很難引起注意。這種行為可以通過預設提供的copy構造函數、copy assignment實作。然而對于一個用于實作RAII的資源管理類來說,copy行為往往是值得注意的。把一個資源管理類複制一份,意味着同一資源被多個資源管理類管理,甚至原始資源本身也被複制了一份。如果這并不是你想要的設計,那麼你必須防止這種行為的發生。以下列舉了四種常見的可能性:

1. 不希望資源管理類發生複制時,請将copy構造函數顯示的聲明為private。

2. 允許資源管理類發生複制卻不希望資源被複制時,應該采用“淺拷貝”并引入“引用計數法”,確定在最後一個引用銷毀時,釋放資源。

3. 允許資源管理類發生複制的同時複制資源時應該采用“深拷貝”。

4. 允許資源管理類發生複制卻不允許指向同一原始資源時,應該在複制的同時銷毀原資源管理類對象管理着的原始資源。

提供通路原始資源的途徑。這麼做的好處有二:

1. 如果你調用第三方的庫,比如某某API,那麼API函數所要的參數往往是原始資源,而非你自定義的資源管理類。是以提供通路原始資源的途徑就顯得相當必要了。

2. 關于析構函數釋放資源的問題。釋放資源時如果發生了異常那麼恭喜你,資源管理類往往沒有好的方法。例如下面的代碼:

class Manager
{
private:Resource* pRc ;
public:Manager(Resource* pRc1);
~Manager();
...
 }
public Manager();
Manager::Manager()
{
pRc = new Resource;//姑且用内部初始化的RAII形式
}
Manager::~Manager()
{
    try
{
delete pRc ;
}catch(...)
{
delete pRc ;
}
}
           

上面展示的這種try-catch的用法的思想事實上在電商系統中也經常用到。例如在海航的項目中,退票子產品對于void操作主機抛出異常的情況就會再次進行void,出票子產品對于第一次打票失敗抛出異常的情況會再次進行打票。然後日前退票也好出票也好都出現了失敗的情況。因為這種設計方式隻能防止一次失敗,卻無法防止第二次失敗。設計是建立在連續兩次操作失敗是小機率事件的基礎之上的。然而事實上,在現實中,真正異常所導緻的往往是大機率的二次操作失敗,因為兩次操作間隔很小,此時異常往往尚未解除。上面的設計隻能用來防止打票機偶爾被占用的情況。為了彌補這種設計,電商網站會引入自動程序。而此處我們所需要做的隻是向客戶提供原始資源的通路,使客戶代碼可以直接釋放資源,這樣,當發生異常時,可由客戶代碼決定如何處理(如再次釋放、記錄日志、通知客戶等等)。

本文到此便結束了,然而要寫出完全可靠的RAII對象幾乎是不可能完成的任務,因為當資源釋放發生異常時,也許最終在代碼級别we can do nothing。這也正式C++世界永遠都必須面對的問題(其實任何程式設計語言都是如此,總有代碼夠不到的地方)。是以,隻要合适,就請帶着RAII的思想設計你的資源管理類。

後序:本文内容如有失當或錯誤之處,還望看客們指出,共同進步!