假設(shè)我們和一個投資(例如,股票,債券等)模型庫一起工作,各種各樣的投資形式從一個根類 Investment 派生出來:
class Investment { ... }; // root class of hierarchy of
// investment types
進一步假設(shè)這個庫使用了通過一個 factory 函數(shù)為我們提供特定 Investment 對象的方法:
Investment* createInvestment(); // return ptr to dynamically allocated
// object in the Investment hierarchy;
// the caller must delete it
// (parameters omitted for simplicity)
通過注釋指出,當(dāng) createInvestment 函數(shù)返回的對象不再使用時,由 createInvestment 的調(diào)用者負責(zé)刪除它。那么,請考慮,寫一個函數(shù) f 來履行以下職責(zé):
void f()
{
Investment *pInv = createInvestment(); // call factory function
... // use pInv
delete pInv; // release object
}
這個看上去沒問題,但是有幾種情形會造成 f 在刪除它從 createInvestment 得到的 investment 對象時失敗。有可能在這個函數(shù)的 "..." 部分的某處有一個提前出現(xiàn)的 return 語句。如果這樣一個 return 執(zhí)行了,控制流程就再也無法到達 delete 語句。還可能發(fā)生的一個類似情況是如果 createInvestment 的使用和刪除在一個循環(huán)里,而這個循環(huán)以一個 continue 或 goto 語句提前退出。還有,"..." 中的一些語句可能拋出一個異常。如果這樣,控制流程不會再到達那個 delete。無論那個 delete 被如何跳過,我們泄漏的不僅僅是容納 investment 對象的內(nèi)存,還包括那個對象持有的任何資源。
當(dāng)然,小心謹(jǐn)慎地編程能防止這各種錯誤,但考慮到這些代碼可能會隨著時間的流逝而發(fā)生變化。為了對軟件進行維護,一些人可能會在沒有完全把握對這個函數(shù)的資源管理策略的其它部分的影響的情況下增加一個 return 或 continue 語句。尤有甚者,f 的 "..." 部分可能調(diào)用了一個從不慣于拋出異常的函數(shù),但是在它被“改良”后突然這樣做了。依賴于 f 總能到達它的 delete 語句根本靠不住。
為了確保 createInvestment 返回的資源總能被釋放,我們需要將那些資源放入一個類中,這個類的析構(gòu)函數(shù)在控制流程離開 f 的時候會自動釋放資源。實際上,這只是本文介紹的觀念的一半:將資源放到一個對象的內(nèi)部,我們可以依賴 C++ 的自動地調(diào)用析構(gòu)函數(shù)來確保資源被釋放。(過一會兒我們還要介紹本文觀念的另一半。)
許多資源都是動態(tài)分配到堆上的,并在一個單獨的塊或函數(shù)內(nèi)使用,而且應(yīng)該在控制流程離開那個塊或函數(shù)的時候釋放。標(biāo)準(zhǔn)庫的 auto_ptr 正是為這種情形量體裁衣的。auto_ptr 是一個類似指針的對象(一個智能指針),它的析構(gòu)函數(shù)自動在它指向的東西上調(diào)用 delete。下面就是如何使用 auto_ptr 來預(yù)防 f 的潛在的資源泄漏:
void f()
{
std::auto_ptr<Investment> pInv(createInvestment()); // call factory
// function
... // use pInv as
// before
} // automatically
// delete pInv via
// auto_ptr’s dtor
這個簡單的例子示范了使用對象管理資源的兩個重要的方面:
獲得資源后應(yīng)該立即移交給資源管理對象。如上,createInvestment 返回的資源被用來初始化即將用來管理它的 auto_ptr。實際上,因為獲取一個資源并在同一個語句中初始化資源管理對象是如此常見,所以使用對象管理資源的觀念也常常被稱為 Resource Acquisition Is Initialization (RAII)。有時被獲取的資源是被賦值給資源管理對象的,而不是初始化它們,但這兩種方法都是在獲取資源的同時就立即將它移交給資源管理對象。
資源管理對象使用它們的析構(gòu)函數(shù)確保資源被釋放。因為當(dāng)一個對象被銷毀時(例如,當(dāng)一個對象離開其活動范圍)會自動調(diào)用析構(gòu)函數(shù),無論控制流程是怎樣離開一個塊的,資源都會被正確釋放。如果釋放資源的動作會引起異常拋出,事情就會變得棘手,不過,關(guān)于那些問題以后我將專題講解,所以不必擔(dān)心它。
因為當(dāng)一個 auto_ptr 被銷毀的時候,會自動刪除它所指向的東西,所以不要讓超過一個的 auto_ptr 指向同一個對象非常重要。如果發(fā)生了這種事情,那個對象就會被刪除超過一次,而且會讓你的程序通過捷徑進入未定義行為。為了防止這個問題,auto_ptrs 具有不同尋常的特性:拷貝它們(通過拷貝構(gòu)造函數(shù)或者拷貝賦值運算符)就是將它們置為空,拷貝的指針被設(shè)想為資源的唯一所有權(quán)。
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 不是管理所有動態(tài)分配資源的最好方法。例如,STL 容器要求其內(nèi)含物能表現(xiàn)出“正常的”拷貝行為,所以 auto_ptrs 的容器是不被允許的。
相對于 auto_ptrs,另一個可選方案是一個引用計數(shù)智能指針(reference-counting smart pointer, RCSP)。一個 RCSP 是一個智能指針,它能持續(xù)跟蹤有多少對象指向一個特定的資源,并能夠在不再有任何東西指向那個資源的時候刪除它。就這一點而論,RCSP 提供的行為類似于垃圾收集(garbage collection)。與垃圾收集不同的是,無論如何,RCSP 不能打破循環(huán)引用(例如,兩個沒有其它使用者的對象互相指向?qū)Ψ剑?BR>
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 的工作“符合預(yù)期”,它們能被用于 STL 容器以及其它和 auto_ptr 的非正統(tǒng)的拷貝行為不相容的環(huán)境中。
不要搞錯,本文不是關(guān)于 about auto_ptr,tr1::shared_ptr 或任何其它種類的智能指針。而是關(guān)于使用對象管理資源的重要性的。about auto_ptr 和 tr1::shared_ptr 僅僅是做這些事的對象的例子。(關(guān)于 tr1::shared_ptr 的更多信息,請參考 Item 14,18 和 54。)
about auto_ptr 和 tr1::shared_ptr 都在它們的析構(gòu)函數(shù)中使用 delete,而不是 delete []。這就意味著將 about auto_ptr 或 tr1::shared_ptr 用于動態(tài)分配的數(shù)組是個餿主意,可是,可悲的是,那居然可以編譯:
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
你可能會吃驚地發(fā)現(xiàn) C++ 中沒有可用于動態(tài)分配數(shù)組的類似 auto_ptr 或 tr1::shared_ptr 這樣的東西,甚至在 TR1 中也沒有。那是因為 vector 和 string 幾乎總是能代替動態(tài)分配數(shù)組。如果你依然覺得有可用于數(shù)組的類似 auto_ptr 和類似 tr1::shared_ptr 的類更好一些的話,可以去看看 Boost。在那里,你將高興地找到 boost::scoped_array 和 boost::shared_array 兩個類提供你在尋找的行為。 本 Item 的關(guān)于使用對象管理資源的指導(dǎo)間接表明:如果你手動釋放資源(例如,使用 delete,而不使用資源管理類),你就是在自找麻煩。像 auto_ptr 和 tr1::shared_ptr 這樣的預(yù)制的資源管理類通常會使本文的建議變得容易,但有時,你使用了一個資源,而這些預(yù)加工的類不能如你所愿地做事。如果碰上這種情況,你就需要精心打造你自己的資源管理類。那也并非困難得可怕,但它包含一些需要你細心考慮的微妙之處。
作為最后的意見,我必須指出 createInvestment 的未加工指針的返回形式就是資源泄漏的請?zhí)?,因為調(diào)用者忘記在他們?nèi)』貋淼闹羔樕险{(diào)用 delete 實在是太容易了。(即使他們使用一個 auto_ptr 或 tr1::shared_ptr 來完成 delete,他們?nèi)匀槐仨氂涀?createInvestment 的返回值存儲到智能指針對象中。)對付這個問題需要改變 createInvestment 的接口。
Things to Remember
·為了防止資源泄漏,使用 RAII 對象,在 RAII 對象的構(gòu)造函數(shù)中獲得資源并在析構(gòu)函數(shù)中釋放它們。
·兩個通用的 RAII 是 tr1::shared_ptr 和 auto_ptr。tr1::shared_ptr 通常是更好的選擇,因為它的拷貝時的行為是符合直覺的??截愐粋€ auto_ptr 是將它置為空。
posted on 2005-10-29 21:14
空の軌跡 閱讀(83)
評論(0) 編輯 收藏 引用