RAII惯用法:C++资源管理的利器
RAII是指C++語言中的一個慣用法(idiom),它是“Resource?Acquisition?Is?Initialization”的首字母縮寫。中文可將其翻譯為“資源獲取就是初始化”。雖然從某種程度上說這個名稱并沒有體現出該慣性法的本質精神,但是作為標準C++資源管理的關鍵技術,RAII早已在C++社群中深入人心。
我記得第一次學到RAII慣用法是在Bjarne Stroustrup的《C++程序設計語言(第3版)》一書中。當講述C++資源管理時,Bjarne這樣寫道:
使用局部對象管理資源的技術通常稱為“資源獲取就是初始化”。這種通用技術依賴于構造函數和析構函數的性質以及它們與異常處理的交互作用。
Bjarne這段話是什么意思呢?
首先讓我們來明確資源的概念,在計算機系統中,資源是數量有限且對系統正常運轉具有一定作用的元素。比如,內存,文件句柄,網絡套接字(network sockets),互斥鎖(mutex locks)等等,它們都屬于系統資源。由于資源的數量不是無限的,有的資源甚至在整個系統中僅有一份,因此我們在使用資源時必須嚴格遵循的步驟是:
1.?????????獲取資源
2.?????????使用資源
3.?????????釋放資源
例如在下面的UseFile函數中:
void UseFile(char const* fn){
????FILE* f = fopen(fn,?"r");????????//?獲取資源
????//?在此處使用文件句柄f...??????????//?使用資源
????fclose(f);???????????????????????//?釋放資源
}
調用fopen()打開文件就是獲取文件句柄資源,操作完成之后,調用fclose()關閉文件就是釋放該資源。資源的釋放工作至關重要,如果只獲取而不釋放,那么資源最終會被耗盡。上面的代碼是否能夠保證在任何情況下都調用fclose函數呢?請考慮如下情況:
void UseFile(char const* fn)
{
????FILE* f = fopen(fn, "r");????????//?獲取資源
????//?使用資源
????if (!g()) return;????????????????//?如果操作g失敗!
????// ...
????if (!h()) return;????????????????//?如果操作h失敗!
????// ...
????fclose(f);???????????????????????//?釋放資源
}
在使用文件f的過程中,因某些操作失敗而造成函數提前返回的現象經常出現。這時函數UseFile的執行流程將變為:
?
很明顯,這里忘記了一個重要的步驟:在操作g或h失敗之后,UseFile函數必須首先調用fclose()關閉文件,然后才能返回其調用者,否則會造成資源泄漏。因此,需要將UseFile函數修改為:
void UseFile(char const* fn)
{
????FILE* f = fopen(fn, "r");????????//?獲取資源
????//?使用資源
????if (!g()) {?fclose(f);?return; }
????// ...
????if (!h()) {?fclose(f);?return; }
????// ...
????fclose(f);???????????????????????//?釋放資源
}
現在的問題是:用于釋放資源的代碼fclose(f)需要在不同的位置重復書寫多次。如果再加入異常處理,情況會變得更加復雜。例如,在文件f的使用過程中,程序可能會拋出異常:
void UseFile(char const* fn)
{
????FILE* f = fopen(fn, "r");????????//?獲取資源
????//?使用資源
????try?{
????????if (!g()) { fclose(f); return; }
????????// ...
????????if (!h()) { fclose(f); return; }
????????// ...
????}
????catch?(...) {
????????fclose(f);???????????????????//?釋放資源
????????throw;
????}
????fclose(f);???????????????????????//?釋放資源
}
我們必須依靠catch(...)來捕獲所有的異常,關閉文件f,并重新拋出該異常。隨著控制流程復雜度的增加,需要添加資源釋放代碼的位置會越來越多。如果資源的數量還不止一個,那么程序員就更加難于招架了。可以想象這種做法的后果是:代碼臃腫,效率下降,更重要的是,程序的可理解性和可維護性明顯降低。是否存在一種方法可以實現資源管理的自動化呢?答案是肯定的。假設UseResources函數要用到n個資源,則進行資源管理的一般模式為:
void UseResources()
{
????//?獲取資源1
????// ...
????//?獲取資源n
????
????//?使用這些資源
????
????//?釋放資源n
????// ...
????//?釋放資源1
}
不難看出資源管理技術的關鍵在于:要保證資源的釋放順序與獲取順序嚴格相反。這自然使我們聯想到局部對象的創建和銷毀過程。在C++中,定義在棧空間上的局部對象稱為自動存儲(automatic memory)對象。管理局部對象的任務非常簡單,因為它們的創建和銷毀工作是由系統自動完成的。我們只需在某個作用域(scope)中定義局部對象(這時系統自動調用構造函數以創建對象),然后就可以放心大膽地使用之,而不必擔心有關善后工作;當控制流程超出這個作用域的范圍時,系統會自動調用析構函數,從而銷毀該對象。
讀者可能會說:如果系統中的資源也具有如同局部對象一樣的特性,自動獲取,自動釋放,那該有多么美妙啊!。事實上,您的想法已經與RAII不謀而合了。既然類是C++中的主要抽象工具,那么就將資源抽象為類,用局部對象來表示資源,把管理資源的任務轉化為管理局部對象的任務。這就是RAII慣用法的真諦!可以毫不夸張地說,RAII有效地實現了C++資源管理的自動化。例如,我們可以將文件句柄FILE抽象為FileHandle類:
class FileHandle {
public:
????FileHandle(char const* n, char const* a) { p = fopen(n, a); }
????~FileHandle() { fclose(p); }
private:
????//?禁止拷貝操作
????FileHandle(FileHandle const&);
????FileHandle& operator= (FileHandle const&);
????FILE *p;
};
FileHandle類的構造函數調用fopen()獲取資源;FileHandle類的析構函數調用fclose()釋放資源。請注意,考慮到FileHandle對象代表一種資源,它并不具有拷貝語義,因此我們將拷貝構造函數和賦值運算符聲明為私有成員。如果利用FileHandle類的局部對象表示文件句柄資源,那么前面的UseFile函數便可簡化為:
void UseFile(char const* fn)
{
????FileHandle file(fn,?"r");?
????//?在此處使用文件句柄f...
????//?超出此作用域時,系統會自動調用file的析構函數,從而釋放資源
}
現在我們就不必擔心隱藏在代碼之中的return語句了;不管函數是正常結束,還是提前返回,系統都必須“乖乖地”調用f的析構函數,資源一定能被釋放。Bjarne所謂“使用局部對象管理資源的技術……依賴于構造函數和析構函數的性質”,說的正是這種情形。
且慢!如若使用文件file的代碼中有異常拋出,難道析構函數還會被調用嗎?此時RAII還能如此奏效嗎?問得好。事實上,當一個異常拋出之后,系統沿著函數調用棧,向上尋找catch子句的過程,稱為棧輾轉開解(stack unwinding)。C++標準規定,在輾轉開解函數調用棧的過程中,系統必須確保調用所有已創建起來的局部對象的析構函數。例如:
void Foo()
{
????FileHandle file1("n1.txt",?"r");?
????FileHandle file2("n2.txt",?"w");
????Bar();???????//?可能拋出異常
????FileHandle file3("n3.txt",?"rw")
}
當Foo()調用Bar()時,局部對象file1和file2已經在Foo的函數調用棧中創建完畢,而file3卻尚未創建。如果Bar()拋出異常,那么file2和file1的析構函數會被先后調用(注意:析構函數的調用順序與構造函數相反);由于此時棧中尚不存在file3對象,因此它的析構函數不會被調用。只有當一個對象的構造函數執行完畢之后,我們才認為該對象的創建工作已經完成。棧輾轉開解過程僅調用那些業已創建的對象的析構函數。
?
RAII慣用法同樣適用于需要管理多個資源的復雜對象。例如,Widget類的構造函數要獲取兩個資源:文件myFile和互斥鎖myLock。每個資源的獲取都有可能失敗并且拋出異常。為了正常使用Widget對象,這里我們必須維護一個不變式(invariant):當調用構造函數時,要么兩個資源全都獲得,對象創建成功;要么兩個資源都沒得到,對象創建失敗。獲取了文件而沒有得到互斥鎖的情況永遠不能出現,也就是說,不允許建立Widget對象的“半成品”。如果將RAII慣用法應用于成員對象,那么我們就可以實現這個不變式:
class Widget {
public:
????Widget(char const* myFile, char const* myLock)
????: file_(myFile),?????//?獲取文件myFile
??????lock_(myLock)??????//?獲取互斥鎖myLock
????{}
????// ...
private:
????FileHandle file_;
????LockHandle lock_;
};
FileHandle和LockHandle類的對象作為Widget類的數據成員,分別表示需要獲取的文件和互斥鎖。資源的獲取過程就是兩個成員對象的初始化過程。在此系統會自動地為我們進行資源管理,程序員不必顯式地添加任何異常處理代碼。例如,當已經創建完file_,但尚未創建完lock_時,有一個異常被拋出,則系統會調用file_的析構函數,而不會調用lock_的析構函數。Bjarne所謂構造函數和析構函數“與異常處理的交互作用”,說的就是這種情形。
綜上所述,RAII的本質內容是用對象代表資源,把管理資源的任務轉化為管理對象的任務,將資源的獲取和釋放與對象的構造和析構對應起來,從而確保在對象的生存期內資源始終有效,對象銷毀時資源必被釋放。換句話說,擁有對象就等于擁有資源,對象存在則資源必定存在。由此可見,RAII慣用法是進行資源管理的有力武器。C++程序員依靠RAII寫出的代碼不僅簡潔優雅,而且做到了異常安全。難怪微軟的MSDN雜志在最近的一篇文章中承認:“若論資源管理,誰也比不過標準C++”。
http://www.cnblogs.com/hsinwang/articles/214663.html
總結
以上是生活随笔為你收集整理的RAII惯用法:C++资源管理的利器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《将博客搬至CSDN》_离水的鱼_新浪博
- 下一篇: PHP知识体系