假設我們和一個投資(例如,股票,債券等)模型庫一起工作,各種各樣的投資形式從一個根類 Investment 派生出來:
class Investment { ... }; // root class of hierarchy of
// investment types
進一步假設這個庫使用了通過一個 factory 函數為我們提供特定 Investment 對象的方法:
Investment* createInvestment(); // return ptr to dynamically allocated
// object in the Investment hierarchy;
// the caller must delete it
// (parameters omitted for simplicity)
通過注釋指出,當 createInvestment 函數返回的對象不再使用時,由 createInvestment 的調用者負責刪除它。那麼,請考慮,寫一個函數 f 來履行以下職責:
void f()
{
Investment *pInv = createInvestment(); // call factory function
... // use pInv
delete pInv; // release object
}
這個看上去沒問題,但是有幾種情形會造成 f 在刪除它從 createInvestment 得到的 investment 對象時失敗。有可能在這個函數的 "..." 部分的某處有一個提前出現的 return 語句。假如這樣一個 return 執行了,控制流程就再也無法到達 delete 語句。還可能發生的一個類似情況是假如 createInvestment 的使用和刪除在一個循環裡,而這個循環以一個 continue 或 goto 語句提前退出。還有,"..." 中的一些語句可能拋出一個異常。假如這樣,控制流程不會再到達那個 delete。無論那個 delete 被如何跳過,我們洩漏的不僅僅是容納 investment 對象的內存,還包括那個對象持有的任何資源。
當然,小心謹慎地編程能防止這各種錯誤,但考慮到這些代碼可能會隨著時間的流逝而發生變化。為了對軟件進行維護,一些人可能會在沒有完全把握對這個函數的資源治理策略的其它部分的影響的情況下增加一個 return 或 continue 語句。尤有甚者,f 的 "..." 部分可能調用了一個從不慣於拋出異常的函數,但是在它被“改良”後忽然這樣做了。依靠於 f 總能到達它的 delete 語句根本靠不住。
為了確保 createInvestment 返回的資源總能被釋放,我們需要將那些資源放入一個類中,這個類的析構函數在控制流程離開 f 的時候會自動釋放資源。實際上,這只是本文介紹的觀念的一半:將資源放到一個對象的內部,我們可以依靠 C++ 的自動地調用析構函數來確保資源被釋放。(過一會兒我們還要介紹本文觀念的另一半。)
許多資源都是動態分配到堆上的,並在一個單獨的塊或函數內使用,而且應該在控制流程離開那個塊或函數的時候釋放。標准庫的 auto_ptr 正是為這種情形量體裁衣的。auto_ptr 是一個類似指針的對象(一個智能指針),它的析構函數自動在它指向的東西上調用 delete。下面就是如何使用 auto_ptr 來預防 f 的潛在的資源洩漏:
void f()
{
std::auto_ptr<Investment> pInv(createInvestment()); // call factory
// function
... // use pInv as
// before
} // automatically
// delete pInv via
// auto_ptr’s dtor
這個簡單的例子示范了使用對象治理資源的兩個重要的方面:
獲得資源後應該立即移交給資源治理對象。如上,createInvestment 返回的資源被用來初始化即將用來治理它的 auto_ptr。實際上,因為獲取一個資源並在同一個語句中初始化資源治理對象是如此常見,所以使用對象治理資源的觀念也經常被稱為 Resource Acquisition Is Initialization (RAII)。有時被獲取的資源是被賦值給資源治理對象的,而不是初始化它們,但這兩種方法都是在獲取資源的同時就立即將它移交給資源治理對象。
資源治理對象使用它們的析構函數確保資源被釋放。因為當一個對象被銷毀時(例如,當一個對象離開其活動范圍)會自動調用析構函數,無論控制流程是怎樣離開一個塊的,資源都會被正確釋放。假如釋放資源的動作會引起異常拋出,事情就會變得棘手,不過,關於那些問題以後我將專題講解,所以不必擔心它。
因為當一個 auto_ptr 被銷毀的時候,會自動刪除它所指向的東西,所以不要讓超過一個的 auto_ptr 指向同一個對象非常重要。假如發生了這種事情,那個對象就會被刪除超過一次,而且會讓你的程序通過捷徑進入未定義行為。為了防止這個問題,auto_ptrs 具有不同平常的特性:拷貝它們(通過拷貝構造函數或者拷貝賦值運算符)就是將它們置為空,拷貝的指針被設想為資源的唯一所有權。
std::auto_ptr<Investment> // pInv1 points to the
pInv1(createInvestment()); // object returned from
// createInvestment
std::auto_ptr<Investment> pInv2(pInv1); // pInv2 now points to the
// object; pInv1 is now null
pInv1 = pInv2; // now pInv1 points to the
// object, and pInv2 is null
這個希奇的拷貝行為,增加了潛在的需求,就是通過 auto_ptrs 治理的資源必須絕對沒有超過一個 auto_ptr 指向它們,這也就意味著 auto_ptrs 不是治理所有動態分配資源的最好方法。例如,STL 容器要求其內含物能表現出“正常的”拷貝行為,所以 auto_ptrs 的容器是不被答應的。
相對於 auto_ptrs,另一個可選方案是一個引用計數智能指針(reference-counting smart pointer, RCSP)。一個 RCSP 是一個智能指針,它能持續跟蹤有多少對象指向一個特定的資源,並能夠在不再有任何東西指向那個資源的時候刪除它。就這一點而論,RCSP 提供的行為類似於垃圾收集(garbage collection)。與垃圾收集不同的是,無論如何,RCSP 不能打破循環引用(例如,兩個沒有其它使用者的對象互相指向對方)。
TR1 的 tr1::shared_ptr是一個 RCSP,所以你可以這樣寫 f:
void f()
{
...
std::tr1::shared_ptr<Investment>
pInv(createInvestment()); // call factory function
... // use pInv as before
} // automatically delete
// pInv via shared_ptr’s dtor
這裡的代碼看上去和使用 auto_ptr 的幾乎相同,但是拷貝 shared_ptrs 的行為卻自然得多:
void f()
{
...
std::tr1::shared_ptr<Investment> // pInv1 points to the
pInv1(createInvestment()); // object returned from
// createInvestment
std::tr1::shared_ptr<Investment> // both
pInv1 and pInv2 now
pInv2(pInv1); // point to the object
pInv1 = pInv2; // ditto - nothing has
// changed
...
} // pInv1 and pInv2 are
// destroyed, and the
// object they point to is
// automatically deleted
因為拷貝 tr1::shared_ptrs 的工作“符合預期”,它們能被用於 STL 容器以及其它和 auto_ptr 的非正統的拷貝行為不相容的環境中。
不要搞錯,本文不是關於 about auto_ptr,tr1::shared_ptr 或任何其它種類的智能指針。而是關於使用對象治理資源的重要性的。about auto_ptr 和 tr1::shared_ptr 僅僅是做這些事的對象的例子。(關於 tr1::shared_ptr 的更多信息,請參考 Item 14,18 和 54。)
about auto_ptr 和 tr1::shared_ptr 都在它們的析構函數中使用 delete,而不是 delete []。這就意味著將 about auto_ptr 或 tr1::shared_ptr 用於動態分配的數組是個馊主意,可是,可悲的是,那居然可以編譯:
std::auto_ptr<std::string> // bad idea! the wrong
aps(new std::string[10]); // delete form will be used
std::tr1::shared_ptr<int> spi(new int[1024]); // same problem
你可能會吃驚地發現 C++ 中沒有可用於動態分配數組的類似 auto_ptr 或 tr1::shared_ptr 這樣的東西,甚至在 TR1 中也沒有。那是因為 vector 和 string 幾乎總是能代替動態分配數組。假如你依然覺得有可用於數組的類似 auto_ptr 和類似 tr1::shared_ptr 的類更好一些的話,可以去看看 Boost。在那裡,你將興奮地找到 boost::scoped_array 和 boost::shared_array 兩個類提供你在尋找的行為。 本 Item 的關於使用對象治理資源的指導間接表明:假如你手動釋放資源(例如,使用 delete,而不使用資源治理類),你就是在自找麻煩。像 auto_ptr 和 tr1::shared_ptr 這樣的預制的資源治理類通常會使本文的建議變得輕易,但有時,你使用了一個資源,而這些預加工的類不能如你所願地做事。假如碰上這種情況,你就需要精心打造你自己的資源治理類。那也並非困難得可怕,但它包含一些需要你細心考慮的微妙之處。
作為最後的意見,我必須指出 createInvestment 的未加工指針的返回形式就是資源洩漏的請帖,因為調用者忘記在他們取回來的指針上調用 delete 實在是太輕易了。(即使他們使用一個 auto_ptr 或 tr1::shared_ptr 來完成 delete,他們仍然必須記住將 createInvestment 的返回值存儲到智能指針對象中。)對付這個問題需要改變 createInvestment 的接口。
Things to Remember
·為了防止資源洩漏,使用 RAII 對象,在 RAII 對象的構造函數中獲得資源並在析構函數中釋放它們。
·兩個通用的 RAII 是 tr1::shared_ptr 和 auto_ptr。tr1::shared_ptr 通常是更好的選擇,因為它的拷貝時的行為是符合直覺的。拷貝一個 auto_ptr 是將它置為空。