所謂資源就是,一旦用了它,將來必須還給系統(tǒng)。如果不這樣,糟糕的事情就會發(fā)生。
C++程序中最常使用的資源就是動態(tài)分配內(nèi)存(如果你分配內(nèi)存卻從來不曾歸還它,會導致內(nèi)存泄露),
但內(nèi)存只是你必須管理的眾多資源之一。
其他常見的資源還包括文件描述符(file description),互斥鎖(mutex lock),圖形界面中的字型和筆刷,數(shù)據(jù)庫連接,以及網(wǎng)絡socket。
不論哪一種資源,最重要的是,當你不再使用它時,必須將它還給系統(tǒng)。
嘗試在任何運用情況下都確保以上所言,是件困難的事,但當你考慮到異常,函數(shù)內(nèi)多重回傳路徑,程序維護員改動軟件卻沒能充分理解隨之而來的沖擊,
態(tài)勢就很明顯了,資源管理的特殊手段還不很充分夠用。
1. delete
假設我們使用一個用來塑模投資行為(例如,股票,債券等等)的程序庫,
其中各式各樣的投資類型繼承自一個root class Investment:
// “投資類型”集成體系中的root class
class Investment { ... };
進一步假設,這個程序庫通過一個工廠函數(shù)供應我們特定的Investment對象:
// 返回指針,指向Investment繼承體系內(nèi)的動態(tài)分配對象
// 調(diào)用者有責任刪除它
Investment* createInvestment();
createInvestment的調(diào)用端,使用了函數(shù)返回的對象后,有責任刪除之。
現(xiàn)在考慮有個f函數(shù)履行了這個責任。
void f(){
// 調(diào)用factory函數(shù)
Investment* pInv = createInvestment();
...
// 釋放pInv所指對象
delete pInv;
}
這看起來妥當,但若干情況下,f可能無法刪除它得自createInvestment的投資對象。
或許因為“...”區(qū)域內(nèi)的一個過早的return語句,如果這樣一個return被執(zhí)行起來,控制流就絕不會觸及delete語句。
類似情況發(fā)生在對createInvestment的使用及delete動作位于某循環(huán)內(nèi),而該循環(huán)由于某個continue或goto語句過早退出。
最后一種可能是“...”區(qū)域內(nèi)的語句拋出異常,果真如此控制流將再次不會幸臨delete。
無論delete如何被略過去,我們泄漏的不只是內(nèi)含投資對象的那塊內(nèi)存,
還包括那些投資對象所保存的任何資源。
當然啦,謹慎的編寫程序可以防止這一類錯誤,但你必須想想,
代碼可能會在時間漸漸過去后被修改,一旦軟件開始接受維護,可能會有某些人添加return語句或continue語句而未能全然領悟它對函數(shù)的資源管理策略造成的后果。
更糟的是f的“...”區(qū)域有可能調(diào)用一個“過去從未拋出異常,卻在被‘改善’之后開始那么做”的函數(shù),
因此單純倚賴“f總是會執(zhí)行其delete語句”是行不通的。
2. 資源管理對象
為確保createInvestment返回的資源總是被釋放,我們需要將資源放進對象內(nèi),
當控制流離開f,該對象的析構(gòu)函數(shù)會自動釋放那些資源。
把資源放進對象內(nèi),我們便可倚賴C++的“析構(gòu)函數(shù)自動調(diào)用機制”確保資源被釋放。
許多資源被動態(tài)分配與heap內(nèi)而后被用于單一區(qū)塊或函數(shù)內(nèi),它們應該在控制流離開那個區(qū)塊或函數(shù)時被釋放。
標準程序庫提供的auto_ptr正是針對這種形勢而設計的特制產(chǎn)品。
auto_ptr是個“類指針(pointer-like)對象”,也就是所謂“智能指針”,其析構(gòu)函數(shù)自動對其所指對象調(diào)用delete。
下面示范如何使用auto_ptr以避免f函數(shù)潛在的資源泄漏可能性:
void f(){
// 調(diào)用factory函數(shù)
std::auto_ptr<Investment> pInv(createInvestment());
// 一如既往的使用pInv
...
// 經(jīng)由auto_ptr的析構(gòu)函數(shù)自動刪除pInv
}
實際上“以對象管理資源”的觀念常被稱為“資源取得時機便是初始化時機”
(Resource Acquisition Is Initialization,RAII),
因為我們幾乎總是在獲得一筆資源后于同一語句內(nèi)以它初始化某個管理對象。
有時候獲得的資源被拿來賦值(而非初始化)某個管理對象,
但不論哪一種做法,每一筆資源都在獲得的同時立刻被放進管理對象中。
不論控制流如何離開區(qū)塊,一旦對象被銷毀(例如當對象離開作用域),其析構(gòu)函數(shù)自然會被自動調(diào)用,與時資源被釋放。
如果資源釋放動作可能導致拋出異常,事情變得有點棘手。(見條款8 P44
3. auto_ptr和shared_ptr
由于auto_ptr被銷毀時會自動刪除它所指之物,所以一定要注意別讓多個auto_ptr同時指向同一對象。
如果真是這樣,對象會被刪除一次以上,而那會使你的程序搭上“未定義行為”的快速列車上。
為了預防這個問題,auto_ptr有一個不尋常的性質(zhì),
若通過copy構(gòu)造函數(shù)或copy assignment操作符復制它們,它們會變成null,而復制所得的指針將取得資源的唯一擁有權(quán)。
// pInv1指向createInvestment返回物
std::auto_ptr<Investment> pInv1(createInvestment());
// copy構(gòu)造函數(shù),現(xiàn)在pInv2指向?qū)ο?,pInv1被設為null
std::auto_ptr<Investment> pInv2(pInv1);
// copy assignment操作符,現(xiàn)在pInv1指向?qū)ο?,pInv2被設為null
pInv1 = pInv2 ;
這一詭異的復制行為,附加上其底層條件:“受auto_ptr管理的資源必須絕對沒有一個以上的auto_ptr同時指向它”,
意味著auto_ptr并非管理動態(tài)分配資源的神兵利器。
舉個例子,STL容器要求其元素發(fā)揮“正常的”復制行為,因此這些容器容不得auto_ptr。
auto_ptr的替代方案是“引用計數(shù)型智慧指針”(reference-counting smart pointer,RCSP),
所謂RCSP也是個智能指針,持續(xù)追蹤共有多少對象指向某筆資源,并在無人指向它時自動刪除該資源。
RCSP提供的行為類似垃圾回收,不同的是RCSP無法打破環(huán)狀引用(cycles of reference),
例如,兩個其實已經(jīng)沒被使用的對象彼此互指,因此好像還處在“被使用”狀態(tài)。
TR1的tr1::shared_ptr就是個RCSP,所以你可以這么寫f:
void f(){
// 調(diào)用factory函數(shù)
std::tr1::shared_ptr<Investment> pInv(createInvestment());
// 使用pInv一如既往
...
// 經(jīng)由shared_ptr析構(gòu)函數(shù)自動刪除pInv
}
這段代碼看起來幾乎和使用auto_ptr的那個版本相同,但shared_ptr的復制行為正常多了。
4. delete[]
auto_ptr和tr1::shared_ptr兩者都在其析構(gòu)函數(shù)內(nèi)做delete而不是delete[]動作。
那意味著在動態(tài)分配而得的array身上使用auto_ptr或tr1::shared_ptr是個餿主意。
盡管如此,可嘆的是,那么做仍能通過編譯。
// 餿主意,會用上錯誤的delete形式
std::auto_ptr<std::string> aps(new std::string[10]);
// 相同問題
std::tr1::shared_ptr<int> spi(new int[1024]);
你或許會驚訝的發(fā)現(xiàn),并沒有特別針對“C++動態(tài)分配數(shù)組”而設計的類似auto_ptr或tr1::shared_ptr那樣的東西,甚至TR1中也沒有。
那是因為vector和string幾乎總是可以取代動態(tài)分配而得的數(shù)組。
如果你還是認為擁有針對數(shù)組而設計,類似auto_ptr和tr1::shared_ptr那樣的class較好,看看Boost吧。
在那你會很高興的發(fā)現(xiàn)boost::scoped_array和boost::shared_array class,它們都提供你要的行為。