C++中的健壯指針和資源管理
作者:張巖
資源及它們的所有權(quán)
我最喜歡的對(duì)資源的定義是:"任何在你的程序中獲得并在此后釋放的東西。"內(nèi)存是一個(gè)相當(dāng)明顯的資源的例子。它需要用new來獲得,用delete來
釋放。同時(shí)也有許多其它類型的資源文件句柄、重要的片斷、Windows中的GDI資源,等等。將資源的概念推廣到程序中創(chuàng)建、釋放的所有對(duì)象也是十分方
便的,無論對(duì)象是在堆中分配的還是在棧中或者是在全局作用于內(nèi)生命的。
對(duì)于給定的資源的擁有著,是負(fù)責(zé)釋放資源的一個(gè)對(duì)象或者是一段代碼。所有權(quán)分立為兩種級(jí)別--自動(dòng)的和顯式的(automatic and
explicit),如果一個(gè)對(duì)象的釋放是由語言本身的機(jī)制來保證的,這個(gè)對(duì)象的就是被自動(dòng)地所有。例如,一個(gè)嵌入在其他對(duì)象中的對(duì)象,他的清除需要其他
對(duì)象來在清除的時(shí)候保證。外面的對(duì)象被看作嵌入類的所有者。
類似地,每個(gè)在棧上創(chuàng)建的對(duì)象(作為自動(dòng)變量)的釋放(破壞)是在控制流離開了對(duì)象被定義的作用域的時(shí)候保證的。這種情況下,作用于被看作是對(duì)象的所
有者。注意所有的自動(dòng)所有權(quán)都是和語言的其他機(jī)制相容的,包括異常。無論是如何退出作用域的--正常流程控制退出、一個(gè)break語句、一個(gè)
return、一個(gè)goto、或者是一個(gè)throw--自動(dòng)資源都可以被清除。
到目前為止,一切都很好!問題是在引入指針、句柄和抽象的時(shí)候產(chǎn)生的。如果通過一個(gè)指針訪問一個(gè)對(duì)象的話,比如對(duì)象在堆中分配,C++不自動(dòng)地關(guān)注它
的釋放。程序員必須明確的用適當(dāng)?shù)某绦蚍椒▉磲尫胚@些資源。比如說,如果一個(gè)對(duì)象是通過調(diào)用new來創(chuàng)建的,它需要用delete來回收。一個(gè)文件是用
CreateFile(Win32
API)打開的,它需要用CloseHandle來關(guān)閉。用EnterCritialSection進(jìn)入的臨界區(qū)(Critical
Section)需要LeaveCriticalSection退出,等等。一個(gè)"裸"指針,文件句柄,或者臨界區(qū)狀態(tài)沒有所有者來確保它們的最終釋放。
基本的資源管理的前提就是確保每個(gè)資源都有他們的所有者。
第一規(guī)則
一個(gè)指針,一個(gè)句柄,一個(gè)臨界區(qū)狀態(tài)只有在我們將它們封裝入對(duì)象的時(shí)候才會(huì)擁有所有者。這就是我們的第一規(guī)則:在構(gòu)造函數(shù)中分配資源,在析構(gòu)函數(shù)中釋放資源。
當(dāng)你按照規(guī)則將所有資源封裝的時(shí)候,你可以保證你的程序中沒有任何的資源泄露。這點(diǎn)在當(dāng)封裝對(duì)象(Encapsulating
Object)在棧中建立或者嵌入在其他的對(duì)象中的時(shí)候非常明顯。但是對(duì)那些動(dòng)態(tài)申請(qǐng)的對(duì)象呢?不要急!任何動(dòng)態(tài)申請(qǐng)的東西都被看作一種資源,并且要按照
上面提到的方法進(jìn)行封裝。這一對(duì)象封裝對(duì)象的鏈不得不在某個(gè)地方終止。它最終終止在最高級(jí)的所有者,自動(dòng)的或者是靜態(tài)的。這些分別是對(duì)離開作用域或者程序
時(shí)釋放資源的保證。
下面是資源封裝的一個(gè)經(jīng)典例子。在一個(gè)多線程的應(yīng)用程序中,線程之間共享對(duì)象的問題是通過用這樣一個(gè)對(duì)象聯(lián)系臨界區(qū)來解決的。每一個(gè)需要訪問共享資源的客戶需要獲得臨界區(qū)。例如,這可能是Win32下臨界區(qū)的實(shí)現(xiàn)方法。
class CritSect
{
friend class Lock;
public:
CritSect () { InitializeCriticalSection (&_critSection); }
~CritSect () { DeleteCriticalSection (&_critSection); }
private
void Acquire ()
{
EnterCriticalSection (&_critSection);
}
void Release ()
{
LeaveCriticalSection (&_critSection);
}
CRITICAL_SECTION _critSection;
};
這里聰明的部分是我們確保每一個(gè)進(jìn)入臨界區(qū)的客戶最后都可以離開。"進(jìn)入"臨界區(qū)的狀態(tài)是一種資源,并應(yīng)當(dāng)被封裝。封裝器通常被稱作一個(gè)鎖(lock)。
class Lock
{
public:
Lock (CritSect& critSect)
: _critSect (critSect)
{
_critSect.Acquire ();
}
~Lock ()
{
_critSect.Release ();
}
private
CritSect & _critSect;
};
鎖一般的用法如下:
void Shared::Act () throw (char *)
{
Lock lock (_critSect);
// perform action -- may throw
// automatic destructor of lock
}
注意無論發(fā)生什么,臨界區(qū)都會(huì)借助于語言的機(jī)制保證釋放。
還有一件需要記住的事情--每一種資源都需要被分別封裝。這是因?yàn)橘Y源分配是一個(gè)非常容易出錯(cuò)的操作,是要資源是有限提供的。我們會(huì)假設(shè)一個(gè)失敗的資
源分配會(huì)導(dǎo)致一個(gè)異常--事實(shí)上,這會(huì)經(jīng)常的發(fā)生。所以如果你想試圖用一個(gè)石頭打兩只鳥的話,或者在一個(gè)構(gòu)造函數(shù)中申請(qǐng)兩種形式的資源,你可能就會(huì)陷入麻
煩。只要想想在一種資源分配成功但另一種失敗拋出異常時(shí)會(huì)發(fā)生什么。因?yàn)闃?gòu)造函數(shù)還沒有全部完成,析構(gòu)函數(shù)不可能被調(diào)用,第一種資源就會(huì)發(fā)生泄露。
這種情況可以非常簡單的避免。無論何時(shí)你有一個(gè)需要兩種以上資源的類時(shí),寫兩個(gè)笑的封裝器將它們嵌入你的類中。每一個(gè)嵌入的構(gòu)造都可以保證刪除,即使包裝類沒有構(gòu)造完成。
Smart Pointers
我們至今還沒有討論最常見類型的資源--用操作符new分配,此后用指針訪問的一個(gè)對(duì)象。我們需要為每個(gè)對(duì)象分別定義一個(gè)封裝類嗎?(事實(shí)上,C++
標(biāo)準(zhǔn)模板庫已經(jīng)有了一個(gè)模板類,叫做auto_ptr,其作用就是提供這種封裝。我們一會(huì)兒在回到auto_ptr。)讓我們從一個(gè)極其簡單、呆板但安全
的東西開始??聪旅娴腟mart Pointer模板類,它十分堅(jiān)固,甚至無法實(shí)現(xiàn)。
template <class T>
class SPtr
{
public:
~SPtr () { delete _p; }
T * operator->() { return _p; }
T const * operator->() const { return _p; }
protected:
SPtr (): _p (0) {}
explicit SPtr (T* p): _p (p) {}
T * _p;
};
為什么要把SPtr的構(gòu)造函數(shù)設(shè)計(jì)為protected呢?如果我需要遵守第一條規(guī)則,那么我就必須這樣做。資源--在這里是class
T的一個(gè)對(duì)象--必須在封裝器的構(gòu)造函數(shù)中分配。但是我不能只簡單的調(diào)用new
T,因?yàn)槲也恢繲的構(gòu)造函數(shù)的參數(shù)。因?yàn)?,在原則上,每一個(gè)T都有一個(gè)不同的構(gòu)造函數(shù);我需要為他定義個(gè)另外一個(gè)封裝器。模板的用處會(huì)很大,為每一個(gè)新
的類,我可以通過繼承SPtr定義一個(gè)新的封裝器,并且提供一個(gè)特定的構(gòu)造函數(shù)。
class SItem: public SPtr<Item>
{
public:
explicit SItem (int i)
: SPtr<Item> (new Item (i)) {}
};
為每一個(gè)類提供一個(gè)Smart
Pointer真的值得嗎?說實(shí)話--不!他很有教學(xué)的價(jià)值,但是一旦你學(xué)會(huì)如何遵循第一規(guī)則的話,你就可以放松規(guī)則并使用一些高級(jí)的技術(shù)。這一技術(shù)是讓
SPtr的構(gòu)造函數(shù)成為public,但是只是是用它來做資源轉(zhuǎn)換(Resource
Transfer)我的意思是用new操作符的結(jié)果直接作為SPtr的構(gòu)造函數(shù)的參數(shù),像這樣:
SPtr<Item> item (new Item (i));
這個(gè)方法明顯更需要自控性,不只是你,而且包括你的程序小組的每個(gè)成員。他們都必須發(fā)誓出了作資源轉(zhuǎn)換外不把構(gòu)造函數(shù)用在人以其他用途。幸運(yùn)的是,這條規(guī)矩很容易得以加強(qiáng)。只需要在源文件中查找所有的new即可。
Resource Transfer
到目前為止,我們所討論的一直是生命周期在一個(gè)單獨(dú)的作用域內(nèi)的資源?,F(xiàn)在我們要解決一個(gè)困難的問題--如何在不同的作用域間安全的傳遞資源。這一問
題在當(dāng)你處理容器的時(shí)候會(huì)變得十分明顯。你可以動(dòng)態(tài)的創(chuàng)建一串對(duì)象,將它們存放至一個(gè)容器中,然后將它們?nèi)〕?,并且在最終安排它們。為了能夠讓這安全的工
作--沒有泄露--對(duì)象需要改變其所有者。
這個(gè)問題的一個(gè)非常顯而易見的解決方法是使用Smart Pointer,無論是在加入容器前還是還找到它們以后。這是他如何運(yùn)作的,你加入Release方法到Smart Pointer中:
template <class T>
T * SPtr<T>::Release ()
{
T * pTmp = _p;
_p = 0;
return pTmp;
}
注意在Release調(diào)用以后,Smart Pointer就不再是對(duì)象的所有者了--它內(nèi)部的指針指向空。
現(xiàn)在,調(diào)用了Release都必須是一個(gè)負(fù)責(zé)的人并且迅速隱藏返回的指針到新的所有者對(duì)象中。在我們的例子中,容器調(diào)用了Release,比如這個(gè)Stack的例子:
void Stack::Push (SPtr <Item> & item) throw (char *)
{
if (_top == maxStack)
throw "Stack overflow";
_arr [_top++] = item.Release ();
};
同樣的,你也可以再你的代碼中用加強(qiáng)Release的可靠性。
相應(yīng)的Pop方法要做些什么呢?他應(yīng)該釋放了資源并祈禱調(diào)用它的是一個(gè)負(fù)責(zé)的人而且立即作一個(gè)資源傳遞它到一個(gè)Smart Pointer?這聽起來并不好。
Strong Pointers
資源管理在內(nèi)容索引(Windows NT Server上的一部分,現(xiàn)在是Windows
2000)上工作,并且,我對(duì)這十分滿意。然后我開始想……這一方法是在這樣一個(gè)完整的系統(tǒng)中形成的,如果可以把它內(nèi)建入語言的本身豈不是一件非常好?我
提出了強(qiáng)指針(Strong Pointer)和弱指針(Weak Pointer)。一個(gè)Strong
Pointer會(huì)在許多地方和我們這個(gè)SPtr相似--它在超出它的作用域后會(huì)清除他所指向的對(duì)象。資源傳遞會(huì)以強(qiáng)指針賦值的形式進(jìn)行。也可以有Weak
Pointer存在,它們用來訪問對(duì)象而不需要所有對(duì)象--比如可賦值的引用。
任何指針都必須聲明為Strong或者Weak,并且語言應(yīng)該來關(guān)注類型轉(zhuǎn)換的規(guī)定。例如,你不可以將Weak
Pointer傳遞到一個(gè)需要Strong Pointer的地方,但是相反卻可以。Push方法可以接受一個(gè)Strong
Pointer并且將它轉(zhuǎn)移到Stack中的Strong Pointer的序列中。Pop方法將會(huì)返回一個(gè)Strong
Pointer。把Strong Pointer的引入語言將會(huì)使垃圾回收成為歷史。
這里還有一個(gè)小問題--修改C++標(biāo)準(zhǔn)幾乎和競選美國總統(tǒng)一樣容易。當(dāng)我將我的注意告訴給Bjarne Stroutrup的時(shí)候,他看我的眼神好像是我剛剛要向他借一千美元一樣。
然后我突然想到一個(gè)念頭。我可以自己實(shí)現(xiàn)Strong Pointers。畢竟,它們都很想Smart
Pointers。給它們一個(gè)拷貝構(gòu)造函數(shù)并重載賦值操作符并不是一個(gè)大問題。事實(shí)上,這正是標(biāo)準(zhǔn)庫中的auto_ptr有的。重要的是對(duì)這些操作給出一
個(gè)資源轉(zhuǎn)移的語法,但是這也不是很難。
template <class T>
SPtr<T>::SPtr (SPtr<T> & ptr)
{
_p = ptr.Release ();
}
template <class T>
void SPtr<T>::operator = (SPtr<T> & ptr)
{
if (_p != ptr._p)
{
delete _p;
_p = ptr.Release ();
}
}
使這整個(gè)想法迅速成功的原因之一是我可以以值方式傳遞這種封裝指針!我有了我的蛋糕,并且也可以吃了。看這個(gè)Stack的新的實(shí)現(xiàn):
class Stack
{
enum { maxStack = 3 };
public:
Stack ()
: _top (0)
{}
void Push (SPtr<Item> & item) throw (char *)
{
if (_top >= maxStack)
throw "Stack overflow";
_arr [_top++] = item;
}
SPtr<Item> Pop ()
{
if (_top == 0)
return SPtr<Item> ();
return _arr [--_top];
}
private
int _top;
SPtr<Item> _arr [maxStack];
};
Pop方法強(qiáng)制客戶將其返回值賦給一個(gè)Strong
Pointer,SPtr<Item>。任何試圖將他對(duì)一個(gè)普通指針的賦值都會(huì)產(chǎn)生一個(gè)編譯期錯(cuò)誤,因?yàn)轭愋筒黄ヅ?。此外?因?yàn)镻op以值方式返回一個(gè)Strong
Pointer(在Pop的聲明時(shí)SPtr<Item>后面沒有&符號(hào)),編譯器在return時(shí)自動(dòng)進(jìn)行了一個(gè)資
源轉(zhuǎn)換。他調(diào)用了operator =來從數(shù)組中提取一個(gè)Item,拷貝構(gòu)造函數(shù)將他傳遞給調(diào)用者。調(diào)用者最后擁有了指向Pop賦值的Strong
Pointer指向的一個(gè)Item。
我馬上意識(shí)到我已經(jīng)在某些東西之上了。我開始用了新的方法重寫原來的代碼。
分析器(Parser)
我過去有一個(gè)老的算術(shù)操作分析器,是用老的資源管理的技術(shù)寫的。分析器的作用是在分析樹中生成節(jié)點(diǎn),節(jié)點(diǎn)是動(dòng)態(tài)分配的。例如分析器的
Expression方法生成一個(gè)表達(dá)式節(jié)點(diǎn)。我沒有時(shí)間用Strong
Pointer去重寫這個(gè)分析器。我令Expression、Term和Factor方法以傳值的方式將Strong
Pointer返回到Node中??聪旅娴腅xpression方法的實(shí)現(xiàn):
SPtr<Node> Parser::Expression()
{
// Parse a term
SPtr<Node> pNode = Term ();
EToken token = _scanner.Token();
if ( token == tPlus || token == tMinus )
{
// Expr := Term { ('+' | '-') Term }
SPtr<MultiNode> pMultiNode = new SumNode (pNode);
do
{
_scanner.Accept();
SPtr<Node> pRight = Term ();
pMultiNode->AddChild (pRight, (token == tPlus));
token = _scanner.Token();
} while (token == tPlus || token == tMinus);
pNode = up_cast<Node, MultiNode> (pMultiNode);
}
// otherwise Expr := Term
return pNode; // by value!
}
最開始,Term方法被調(diào)用。他傳值返回一個(gè)指向Node的Strong Pointer并且立刻把它保存到我們自己的Strong
Pointer,pNode中。如果下一個(gè)符號(hào)不是加號(hào)或者減號(hào),我們就簡單的把這個(gè)SPtr以值返回,這樣就釋放了Node的所有權(quán)。另外一方面,如果
下一個(gè)符號(hào)是加號(hào)或者減號(hào),我們創(chuàng)建一個(gè)新的SumMode并且立刻(直接傳遞)將它儲(chǔ)存到MultiNode的一個(gè)Strong
Pointer中。這里,SumNode是從MultiMode中繼承而來的,而MulitNode是從Node繼承而來的。原來的Node的所有權(quán)轉(zhuǎn)給
了SumNode。
只要是他們在被加號(hào)和減號(hào)分開的時(shí)候,我們就不斷的創(chuàng)建terms,我們將這些term轉(zhuǎn)移到我們的MultiNode中,同時(shí)MultiNode得
到了所有權(quán)。最后,我們將指向MultiNode的Strong Pointer向上映射為指向Mode的Strong
Pointer,并且將他返回調(diào)用著。
我們需要對(duì)Strong
Pointers進(jìn)行顯式的向上映射,即使指針是被隱式的封裝。例如,一個(gè)MultiNode是一個(gè)Node,但是相同的is-a關(guān)系在
SPtr<
MultiNode>和SPtr<Node>之間并不存在,因?yàn)樗鼈兪欠蛛x的類(模板實(shí)例)并不存在繼承關(guān)
系。up-cast模板是像下面這樣定義的:
template<class To, class From>
inline SPtr<To> up_cast (SPtr<From> & from)
{
return SPtr<To> (from.Release ());
}
如果你的編譯器支持新加入標(biāo)準(zhǔn)的成員模板(member template)的話,你可以為SPtr<T>定義一個(gè)新的構(gòu)造函數(shù)用來從接受一個(gè)class U。
template <class T>
template <class U> SPtr<T>::SPtr (SPrt<U> & uptr)
: _p (uptr.Release ())
{}
這里的這個(gè)花招是模板在U不是T的子類的時(shí)候就不會(huì)編譯成功(換句話說,只在U is-a
T的時(shí)候才會(huì)編譯)。這是因?yàn)閡ptr的緣故。Release()方法返回一個(gè)指向U的指針,并被賦值為_p,一個(gè)指向T的指針。所以如果U不是一個(gè)T的
話,賦值會(huì)導(dǎo)致一個(gè)編譯時(shí)刻錯(cuò)誤。
std::auto_ptr
后來我意識(shí)到在STL中的auto_ptr模板,就是我的Strong
Pointer。在那時(shí)候還有許多的實(shí)現(xiàn)差異(auto_ptr的Release方法并不將內(nèi)部的指針清零--你的編譯器的庫很可能用的就是這種陳舊的實(shí)
現(xiàn)),但是最后在標(biāo)準(zhǔn)被廣泛接受之前都被解決了。
Transfer Semantics(轉(zhuǎn)換語義學(xué))
目前為止,我們一直在討論在C++程序中資源管理的方法。宗旨是將資源封裝到一些輕量級(jí)的類中,并由類負(fù)責(zé)它們的釋放。特別的是,所有用new操作符分配的資源都會(huì)被儲(chǔ)存并傳遞進(jìn)Strong Pointer(標(biāo)準(zhǔn)庫中的auto_ptr)的內(nèi)部。
這里的關(guān)鍵詞是傳遞(passing)。一個(gè)容器可以通過傳值返回一個(gè)Strong Pointer來安全的釋放資源。容器的客戶只能夠通過提供一個(gè)相應(yīng)的Strong Pointer來保存這個(gè)資源。任何一個(gè)將結(jié)果賦給一個(gè)"裸"指針的做法都立即會(huì)被編譯器發(fā)現(xiàn)。
auto_ptr<Item> item = stack.Pop (); // ok
Item * p = stack.Pop (); // Error! Type mismatch.
以傳值方式被傳遞的對(duì)象有value semantics 或者稱為 copy semantics。Strong
Pointers是以值方式傳遞的--但是我們能說它們有copy
semantics嗎?不是這樣的!它們所指向的對(duì)象肯定沒有被拷貝過。事實(shí)上,傳遞過后,源auto_ptr不在訪問原有的對(duì)象,并且目標(biāo)
auto_ptr成為了對(duì)象的唯一擁有者(但是往往auto_ptr的舊的實(shí)現(xiàn)即使在釋放后仍然保持著對(duì)對(duì)象的所有權(quán))。自然而然的我們可以將這種新的行
為稱作Transfer Semantics。
拷貝構(gòu)造函數(shù)(copy construcor)和賦值操作符定義了auto_ptr的Transfer Semantics,它們用了非const的auto_ptr引用作為它們的參數(shù)。
auto_ptr (auto_ptr<T> & ptr);
auto_ptr & operator = (auto_ptr<T> & ptr);
這是因?yàn)樗鼈兇_實(shí)改變了他們的源--剝奪了對(duì)資源的所有權(quán)。
通過定義相應(yīng)的拷貝構(gòu)造函數(shù)和重載賦值操作符,你可以將Transfer Semantics加入到許多對(duì)象中。例如,許多Windows中的資源,比如動(dòng)態(tài)建立的菜單或者位圖,可以用有Transfer Semantics的類來封裝。
Strong Vectors
標(biāo)準(zhǔn)庫只在auto_ptr中支持資源管理。甚至連最簡單的容器也不支持ownership semantics。你可能想將auto_ptr和標(biāo)準(zhǔn)容器組合到一起可能會(huì)管用,但是并不是這樣的。例如,你可能會(huì)這樣做,但是會(huì)發(fā)現(xiàn)你不能夠用標(biāo)準(zhǔn)的方法來進(jìn)行索引。
vector< auto_ptr<Item> > autoVector;
這種建造不會(huì)編譯成功;
Item * item = autoVector [0];
另一方面,這會(huì)導(dǎo)致一個(gè)從autoVect到auto_ptr的所有權(quán)轉(zhuǎn)換:
auto_ptr<Item> item = autoVector [0];
我們沒有選擇,只能夠構(gòu)造我們自己的Strong Vector。最小的接口應(yīng)該如下:
template <class T>
class auto_vector
{
public:
explicit auto_vector (size_t capacity = 0);
T const * operator [] (size_t i) const;
T * operator [] (size_t i);
void assign (size_t i, auto_ptr<T> & p);
void assign_direct (size_t i, T * p);
void push_back (auto_ptr<T> & p);
auto_ptr<T> pop_back ();
};
你也許會(huì)發(fā)現(xiàn)一個(gè)非常防御性的設(shè)計(jì)態(tài)度。我決定不提供一個(gè)對(duì)vector的左值索引的訪問,取而代之,如果你想設(shè)定(set)一個(gè)值的話,你必須用
assign或者assign_direct方法。我的觀點(diǎn)是,資源管理不應(yīng)該被忽視,同時(shí),也不應(yīng)該在所有的地方濫用。在我的經(jīng)驗(yàn)里,一個(gè)strong
vector經(jīng)常被許多push_back方法充斥著。
Strong vector最好用一個(gè)動(dòng)態(tài)的Strong Pointers的數(shù)組來實(shí)現(xiàn):
template <class T>
class auto_vector
{
private
void grow (size_t reqCapacity);
auto_ptr<T> *_arr;
size_t _capacity;
size_t _end;
};
grow方法申請(qǐng)了一個(gè)很大的auto_ptr<T>的數(shù)組,將所有的東西從老的書組類轉(zhuǎn)移出來,在其中交換,并且刪除原來的數(shù)組。
auto_vector的其他實(shí)現(xiàn)都是十分直接的,因?yàn)樗匈Y源管理的復(fù)雜度都在auto_ptr中。例如,assign方法簡單的利用了重載的賦值操作符來刪除原有的對(duì)象并轉(zhuǎn)移資源到新的對(duì)象:
void assign (size_t i, auto_ptr<T> & p)
{
_arr = p;
}
我已經(jīng)討論了push_back和pop_back方法。push_back方法傳值返回一個(gè)auto_ptr,因?yàn)樗鼘⑺袡?quán)從auto_vector轉(zhuǎn)換到auto_ptr中。
對(duì)auto_vector的索引訪問是借助auto_ptr的get方法來實(shí)現(xiàn)的,get簡單的返回一個(gè)內(nèi)部指針。
T * operator [] (size_t i)
{
return _arr .get ();
}
沒有容器可以沒有iterator。我們需要一個(gè)iterator讓auto_vector看起來更像一個(gè)普通的指針向量。特別是,當(dāng)我們廢棄
iterator的時(shí)候,我們需要的是一個(gè)指針而不是auto_ptr。我們不希望一個(gè)auto_vector的iterator在無意中進(jìn)行資源轉(zhuǎn)換。
template<class T>
class auto_iterator: public
iterator<random_access_iterator_tag, T *>
{
public:
auto_iterator () : _pp (0) {}
auto_iterator (auto_ptr<T> * pp) : _pp (pp) {}
bool operator != (auto_iterator<T> const & it) const
{ return it._pp != _pp; }
auto_iterator const & operator++ (int) { return _pp++; }
auto_iterator operator++ () { return ++_pp; }
T * operator * () { return _pp->get (); }
private
auto_ptr<T> * _pp;
};
我們給auto_vect提供了標(biāo)準(zhǔn)的begin和end方法來找回iterator:
class auto_vector
{
public:
typedef auto_iterator<T> iterator;
iterator begin () { return _arr; }
iterator end () { return _arr + _end; }
};
你也許會(huì)問我們是否要利用資源管理重新實(shí)現(xiàn)每一個(gè)標(biāo)準(zhǔn)的容器?幸運(yùn)的是,不;事實(shí)是strong vector解決了大部分所有權(quán)的需求。當(dāng)你把你的對(duì)象都安全的放置到一個(gè)strong vector中,你可以用所有其它的容器來重新安排(weak)pointer。
設(shè)想,例如,你需要對(duì)一些動(dòng)態(tài)分配的對(duì)象排序的時(shí)候。你將它們的指針保存到一個(gè)strong
vector中。然后你用一個(gè)標(biāo)準(zhǔn)的vector來保存從strong
vector中獲得的weak指針。你可以用標(biāo)準(zhǔn)的算法對(duì)這個(gè)vector進(jìn)行排序。這種中介vector叫做permutation
vector。相似的,你也可以用標(biāo)準(zhǔn)的maps, priority queues, heaps, hash tables等等。
Code Inspection(編碼檢查)
如果你嚴(yán)格遵照資源管理的條款,你就不會(huì)再資源泄露或者兩次刪除的地方遇到麻煩。你也降低了訪問野指針的幾率。同樣的,遵循原有的規(guī)則,用delete刪除用new申請(qǐng)的德指針,不要兩次刪除一個(gè)指針。你也不會(huì)遇到麻煩。但是,那個(gè)是更好的注意呢?
這兩個(gè)方法有一個(gè)很大的不同點(diǎn)。就是和尋找傳統(tǒng)方法的bug相比,找到違反資源管理的規(guī)定要容易的多。后者僅需要一個(gè)代碼檢測或者一個(gè)運(yùn)行測試,而前者則在代碼中隱藏得很深,并需要很深的檢查。
設(shè)想你要做一段傳統(tǒng)的代碼的內(nèi)存泄露檢查。第一件事,你要做的就是grep所有在代碼中出現(xiàn)的new,你需要找出被分配空間地指針都作了什么。你需要
確定導(dǎo)致刪除這個(gè)指針的所有的執(zhí)行路徑。你需要檢查break語句,過程返回,異常。原有的指針可能賦給另一個(gè)指針,你對(duì)這個(gè)指針也要做相同的事。
相比之下,對(duì)于一段用資源管理技術(shù)實(shí)現(xiàn)的代碼。你也用grep檢查所有的new,但是這次你只需要檢查鄰近的調(diào)用:
● 這是一個(gè)直接的Strong Pointer轉(zhuǎn)換,還是我們在一個(gè)構(gòu)造函數(shù)的函數(shù)體中?
● 調(diào)用的返回知是否立即保存到對(duì)象中,構(gòu)造函數(shù)中是否有可以產(chǎn)生異常的代碼。?
● 如果這樣的話析構(gòu)函數(shù)中時(shí)候有delete?
下一步,你需要用grep查找所有的release方法,并實(shí)施相同的檢查。
不同點(diǎn)是需要檢查、理解單個(gè)執(zhí)行路徑和只需要做一些本地的檢驗(yàn)。這難道不是提醒你非結(jié)構(gòu)化的和結(jié)構(gòu)化的程序設(shè)計(jì)的不同嗎?原理上,你可以認(rèn)為你可以應(yīng)付goto,并且跟蹤所有的可能分支。另一方面,你可以將你的懷疑本地化為一段代碼。本地化在兩種情況下都是關(guān)鍵所在。
在資源管理中的錯(cuò)誤模式也比較容易調(diào)試。最常見的bug是試圖訪問一個(gè)釋放過的strong pointer。這將導(dǎo)致一個(gè)錯(cuò)誤,并且很容易跟蹤。
共享的所有權(quán)
為每一個(gè)程序中的資源都找出或者指定一個(gè)所有者是一件很容易的事情嗎?答案是出乎意料的,是!如果你發(fā)現(xiàn)了一些問題,這可能說明你的設(shè)計(jì)上存在問題。還有另一種情況就是共享所有權(quán)是最好的甚至是唯一的選擇。
共享的責(zé)任分配給被共享的對(duì)象和它的客戶(client)。一個(gè)共享資源必須為它的所有者保持一個(gè)引用計(jì)數(shù)。另一方面,所有者再釋放資源的時(shí)候必須通報(bào)共享對(duì)象。最后一個(gè)釋放資源的需要在最后負(fù)責(zé)free的工作。
最簡單的共享的實(shí)現(xiàn)是共享對(duì)象繼承引用計(jì)數(shù)的類RefCounted:
class RefCounted
{
public:
RefCounted () : _count (1) {}
int GetRefCount () const { return _count; }
void IncRefCount () { _count++; }
int DecRefCount () { return --_count; }
private
int _count;
};
按照資源管理,一個(gè)引用計(jì)數(shù)是一種資源。如果你遵守它,你需要釋放它。當(dāng)你意識(shí)到這一事實(shí)的時(shí)候,剩下的就變得簡單了。簡單的遵循規(guī)則--再構(gòu)造函數(shù)中獲得引用計(jì)數(shù),在析構(gòu)函數(shù)中釋放。甚至有一個(gè)RefCounted的smart pointer等價(jià)物:
template <class T>
class RefPtr
{
public:
RefPtr (T * p) : _p (p) {}
RefPtr (RefPtr<T> & p)
{
_p = p._p;
_p->IncRefCount ();
}
~RefPtr ()
{
if (_p->DecRefCount () == 0)
delete _p;
}
private
T * _p;
};
注意模板中的T不比成為RefCounted的后代,但是它必須有IncRefCount和DecRefCount的方法。當(dāng)然,一個(gè)便于使用的
RefPtr需要有一個(gè)重載的指針訪問操作符。在RefPtr中加入轉(zhuǎn)換語義學(xué)(transfer semantics)是讀者的工作。
所有權(quán)網(wǎng)絡(luò)
鏈表是資源管理分析中的一個(gè)很有意思的例子。如果你選擇表成為鏈(link)的所有者的話,你會(huì)陷入實(shí)現(xiàn)遞歸的所有權(quán)。每一個(gè)link都是它的繼承者的所有者,并且,相應(yīng)的,余下的鏈表的所有者。下面是用smart pointer實(shí)現(xiàn)的一個(gè)表單元:
class Link
{
// ...
private
auto_ptr<Link> _next;
};
最好的方法是,將連接控制封裝到一個(gè)弄構(gòu)進(jìn)行資源轉(zhuǎn)換的類中。
對(duì)于雙鏈表呢?安全的做法是指明一個(gè)方向,如forward:
class DoubleLink
{
// ...
private
DoubleLink *_prev;
auto_ptr<DoubleLink> _next;
};
注意不要?jiǎng)?chuàng)建環(huán)形鏈表。
這給我們帶來了另外一個(gè)有趣的問題--資源管理可以處理環(huán)形的所有權(quán)嗎?它可以,用一個(gè)mark-and-sweep的算法。這里是實(shí)現(xiàn)這種方法的一個(gè)例子:
template<class T>
class CyclPtr
{
public:
CyclPtr (T * p)
:_p (p), _isBeingDeleted (false)
{}
~CyclPtr ()
{
_isBeingDeleted = true;
if (!_p->IsBeingDeleted ())
delete _p;
}
void Set (T * p)
{
_p = p;
}
bool IsBeingDeleted () const { return _isBeingDeleted; }
private
T * _p;
bool _isBeingDeleted;
};
注意我們需要用class T來實(shí)現(xiàn)方法IsBeingDeleted,就像從CyclPtr繼承。對(duì)特殊的所有權(quán)網(wǎng)絡(luò)普通化是十分直接的。
將原有代碼轉(zhuǎn)換為資源管理代碼
如果你是一個(gè)經(jīng)驗(yàn)豐富的程序員,你一定會(huì)知道找資源的bug是一件浪費(fèi)時(shí)間的痛苦的經(jīng)歷。我不必說服你和你的團(tuán)隊(duì)花費(fèi)一點(diǎn)時(shí)間來熟悉資源管理是十分值
得的。你可以立即開始用這個(gè)方法,無論你是在開始一個(gè)新項(xiàng)目或者是在一個(gè)項(xiàng)目的中期。轉(zhuǎn)換不必立即全部完成。下面是步驟。
首先,在你的工程中建立基本的Strong Pointer。然后通過查找代碼中的new來開始封裝裸指針。
最先封裝的是在過程中定義的臨時(shí)指針。簡單的將它們替換為auto_ptr并且刪除相應(yīng)的delete。如果一個(gè)指針在過程中沒有被刪除而是被返回,
用auto_ptr替換并在返回前調(diào)用release方法。在你做第二次傳遞的時(shí)候,你需要處理對(duì)release的調(diào)用。注意,即使是在這點(diǎn),你的代碼也
可能更加"精力充沛"--你會(huì)移出代碼中潛在的資源泄漏問題。
下面是指向資源的裸指針。確保它們被獨(dú)立的封裝到auto_ptr中,或者在構(gòu)造函數(shù)中分配在析構(gòu)函數(shù)中釋放。如果你有傳遞所有權(quán)的行為的話,需要調(diào)用release方法。如果你有容器所有對(duì)象,用Strong Pointers重新實(shí)現(xiàn)它們。
接下來,找到所有對(duì)release的方法調(diào)用并且盡力清除所有,如果一個(gè)release調(diào)用返回一個(gè)指針,將它修改傳值返回一個(gè)auto_ptr。
重復(fù)著一過程,直到最后所有new和release的調(diào)用都在構(gòu)造函數(shù)或者資源轉(zhuǎn)換的時(shí)候發(fā)生。這樣,你在你的代碼中處理了資源泄漏的問題。對(duì)其他資源進(jìn)行相似的操作。
你會(huì)發(fā)現(xiàn)資源管理清除了許多錯(cuò)誤和異常處理帶來的復(fù)雜性。不僅僅你的代碼會(huì)變得精力充沛,它也會(huì)變得簡單并容易維護(hù)。