在下面的示例中,我們的工作將圍繞一個(gè)為投資(或者是股票、證券等等)建模的庫展開,在這個(gè)庫中,各種各樣的投資類型都繼承自同一個(gè)基類——Investment:
class Investment { ... }; // 投資類型層次結(jié)構(gòu)的基類
繼續(xù)上面的示例,我們考慮通過工廠函數(shù)(參見條目7)來提供具體的Investment對象:
Investment* createInvestment(); // 返回一個(gè)指針,指向Investment
// 層次結(jié)構(gòu)中動態(tài)分配的對象,
// 調(diào)用者必須要自行將其刪除
// (省略參數(shù)表以簡化代碼)
從上面代碼中的注釋中可以看出,當(dāng)調(diào)用者完成對于createInvestment函數(shù)返回對象的操作后,有義務(wù)刪除這一對象。請看下邊代碼中一個(gè)履行這一義務(wù)的函數(shù)——f:
void f()
{
Investment *pInv = createInvestment(); // 調(diào)用工廠函數(shù)
... // 使用pInv
delete pInv; // 釋放該對象
}
這看上去可以正常運(yùn)行,但是在一些情況下f可能無法成功刪除它從createInvestment獲得的對象。在上述代碼的“...”部分中的某處可能存在不成熟的return語句。一旦這樣的return語句得到了執(zhí)行,那么程序?qū)⒂肋h(yuǎn)無法到達(dá)delete語句。當(dāng)createInvestment和delete在一個(gè)循環(huán)內(nèi)使用時(shí),也會出現(xiàn)類似的情形,這樣的循環(huán)有可能在遇到continue或goto語句而提前退出。最后,“...”中的一些語句還有可能拋出異常。如果真的有異常拋出,程序同樣也不會達(dá)到delete。無論delete是如何被跳過的,包含Investment對象的內(nèi)存都將泄漏,同時(shí)這些對象所包含的資源都有可能得不到釋放。
當(dāng)然,用心編程就有可能防止這類錯(cuò)誤發(fā)生,但是請想象一下代碼會多么的不固定——你需要不停地修改代碼。隨著軟件不斷得到維護(hù),為一個(gè)函數(shù)添加return或continue語句可能會對其資源管理策略造成怎樣的影響呢,一些人可能由于不完全理解這一問題就這樣做了。還有更嚴(yán)重的,就是f函數(shù)的“...”部分可能調(diào)用了一個(gè)這樣的函數(shù):它原先不會拋出異常,但在其得到“改進(jìn)”之后,它突然又開始能夠拋出異常了。寄希望于f函數(shù)總能達(dá)到其中的delete語句并不可靠。
為了確保createInvestment所返回的資源總能得到釋放,我們需要將這類資源放置在一個(gè)對象中,并讓該對象的析構(gòu)函數(shù)在程序離開f時(shí)自動釋放資源。實(shí)際上,這就是本條目蘊(yùn)含理念的一半了:通過將資源放置在對象中,我們可以依賴C++對默認(rèn)析構(gòu)函數(shù)的自動調(diào)用來確保資源及時(shí)得到釋放。(另一半理念稍后講解。)
許多資源是在堆上動態(tài)分配的,并且僅僅在單一的程序塊或函數(shù)中使用,這類資源應(yīng)該在程序離開這一程序塊或函數(shù)之前得到釋放。標(biāo)準(zhǔn)庫中的auto_ptr就是為這類情況量身定做的。auto_ptr是一個(gè)類似于指針的對象(智能指針),其析構(gòu)函數(shù)可以自動對其所指內(nèi)容執(zhí)行delete。以下代碼描述了如何使用auto_ptr來防止f潛在的資源泄漏。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
// 調(diào)用工廠函數(shù)
... // 像前文示例一樣使用pInv
} // 通過auto_ptr的析構(gòu)函數(shù)
// 自動刪除pInv
這一簡單的示例向我們展示了使用對象管理資源的兩大關(guān)鍵問題:
l 獲取資源后,資源將立即轉(zhuǎn)交給資源管理對象。上邊的示例中,createInvestment返回的資源將初始化一個(gè)auto_ptr,從而實(shí)現(xiàn)對這類資源的管理。事實(shí)上,使用對象來管理資源的理念通常稱為“資源獲取即初始化”(Resource Acquisition Is Initialization,簡稱RAII),這是因?yàn)椋谕粋€(gè)語句中獲取一個(gè)資源并且初始化一個(gè)資源管理對象是很平常的。某些時(shí)候獲取的資源就是會賦值給一個(gè)資源管理對象,而不是初始化。但是無論是哪種途徑,在獲取到一個(gè)資源時(shí),每個(gè)資源都都會立即轉(zhuǎn)向一個(gè)資源管理對象。
l 資源管理對象使用其析構(gòu)函數(shù)來確保資源得到釋放。由于析構(gòu)函數(shù)是在對象銷毀時(shí)自動調(diào)用的(比如,當(dāng)對象在其作用域外時(shí)),所以不管程序是如何離開一個(gè)塊的,資源都會被正確地釋放。如果釋放資源會帶來異常,那么事情就會變得錯(cuò)綜復(fù)雜。但那是條目8中介紹的內(nèi)容,我們這里不關(guān)心這些。
由于當(dāng)一個(gè)auto_ptr被銷毀時(shí),它將自動刪除其所指向的內(nèi)容,所以永遠(yuǎn)不存在多個(gè)auto_ptr指向同一個(gè)對象的情況,這一點(diǎn)很重要。如果存在的話,這個(gè)對象就會被多次刪除,這樣你的程序就會立即陷入未定義行為。為了防止此類問題發(fā)生,auto_ptr有一個(gè)不同尋常的特性:如果你復(fù)制它們(通過拷貝構(gòu)造函數(shù)或者拷貝賦值運(yùn)算符),它們就會被重設(shè)為null,然后資源的所有權(quán)將由復(fù)制出的指針獨(dú)占!
std::auto_ptr<Investment> pInv1(createInvestment());
// pInv1指向createInvestment
// 所返回的對象
std::auto_ptr<Investment> pInv2(pInv1);
// 現(xiàn)在pInv2指向這一對象,
// pInv1被重設(shè)為null
pInv1 = pInv2; // 現(xiàn)在pInv1指向這一對象
// pInv2被重設(shè)為null
在這一古怪的復(fù)制方法中,由于auto_ptr必須僅僅指向一個(gè)資源,因此增加了對于資源管理的潛在需求。這意味著auto_ptr并不適合于所有動態(tài)分配的資源。比如說,STL容器要求其內(nèi)容表現(xiàn)出“正常”的復(fù)制行為,所以在容器中放置auto_ptr是不允許的。
引用計(jì)數(shù)智能指針(reference-counting smart pointer,簡稱RCSP)是auto_ptr的一個(gè)替代品。RCSP是這樣的智能指針:它可以跟蹤有多少的對象指向了一個(gè)特定的資源,同時(shí)當(dāng)沒有指針在指向這一資源時(shí),智能指針會自動刪除該資源。可以看出,RCSP的行為與垃圾回收器很相似。然而,與垃圾回收器不同的是,RCSP不能夠打斷循環(huán)引用(比如兩個(gè)不同的、空閑的、互相指向?qū)Ψ降膶ο螅?/p>
TR1的tr1::shared_ptr就是一個(gè)RCSP,于是你可以按下面的方式來編寫f:
void f()
{
...
std::tr1::shared_ptr<Investment>
pInv(createInvestment()); // 調(diào)用工廠函數(shù)
... // 像前文示例一樣使用pInv
} // 通過shared_ptr的析構(gòu)函數(shù)
// 自動刪除pInv
上面的代碼與使用auto_ptr時(shí)幾乎完全相同,但是復(fù)制shared_ptr的行為更加自然:
void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment());
// pInv1指向createInvestment
// 所返回的對象
std::tr1::shared_ptr<Investment> pInv2(pInv1);
// 現(xiàn)在pInv1與pInv2均指向同一對象
pInv1 = pInv2; // 同上 — 因?yàn)槭裁炊紱]有改變
...
} // pInv1與pInv2被銷毀,
// 它們所指向的對象也被自動刪除了
由于復(fù)制tr1::shared_ptr的工作可以“如期進(jìn)行”,所以在auto_ptr會出現(xiàn)另類的復(fù)制行為的地方,比如STL容器以及其它一些上下文中,這類指針能夠安全地應(yīng)用。
但是,請不要迷失方向。本條目并不是專門講解auto_ptr和tr1::shared_ptr的,也不是講解智能指針的。本條目的核心內(nèi)容是使用對象管理資源的重要性。auto_ptr和tr1::shared_ptr僅僅是這類對象的示例。(關(guān)于tr1::shared_ptr的更多信息,請參見條目14、18和54。)
auto_ptr和tr1::shared_ptr在析構(gòu)函數(shù)中使用的都是delete語句,而不是delete[]。(條目16中描述了二者的區(qū)別。)這就意味著對于動態(tài)分配的數(shù)組使用auto_ptr和tr1::shared_ptr不是一個(gè)好主意。但是遺憾的是,這樣的代碼會通過編譯:
std::auto_ptr<std::string> aps(new std::string[10]);
// 壞主意!
// 這里將使用錯(cuò)誤的刪除格式
std::tr1::shared_ptr<int> spi(new int[1024]);
// 同樣的問題
你可能會很吃驚,因?yàn)樵贑++中沒有類似于auto_ptr和tr1::shared_ptr的方案來解決動態(tài)分配數(shù)組的問題,甚至TR1中也沒有。這是因?yàn)?span style="font-family:"Courier New";">vector和string通常都可以代替動態(tài)分配的數(shù)組。如果你仍然希望存在類似于auto_ptr和tr1::shared_ptr的數(shù)組類,請參見Boost的相關(guān)內(nèi)容(見條目55)。那兒會滿足你需求:Boost提供了boost::scoped_array和boost::shared_array來處理相關(guān)問題。
本條目中指引你使用對象來管理資源。如果你手動釋放資源(比如使用delete而不是使用資源管理類),你就在做一些錯(cuò)事。諸如auto_ptr和tr1::shared_ptr等封裝好的資源管理類通常可以讓遵循本條目的建議變成一件很容易的事情,但是某些情況下,你的問題無法使用這些現(xiàn)成的類來解決,此時(shí)你便需要創(chuàng)建自己的資源管理類。但這并沒有想象中那么難,但是確實(shí)需要你考慮一些細(xì)節(jié)問題。這些細(xì)節(jié)問題就是條目14和條目15的主題。
最后說一下,我必須指出createInvestment的原始指針返回類型存在著潛在的內(nèi)存泄漏問題,因?yàn)檎{(diào)用者十分容易忘記在返回時(shí)調(diào)用delete。(甚至在調(diào)用者使用auto_ptr或tr1::shared_ptr來運(yùn)行delete時(shí),他們?nèi)匀恍枰谝粋€(gè)智能指針對象中保存createInvestment的返回值。)解決這一問題需要改變createInvestment的接口,這是條目18的主題。
時(shí)刻牢記
l 為了避免資源泄漏,可以使用RAII對象,這類對象使用構(gòu)造函數(shù)獲取資源,析構(gòu)函數(shù)釋放資源。
l auto_ptr或tr1::shared_ptr是兩個(gè)常用并且實(shí)用的RAII類。通常情況下tr1::shared_ptr是更好的選擇,因?yàn)樗膹?fù)制行為更加直觀。復(fù)制一個(gè)auto_ptr將會使其重設(shè)為null。