對指針說拜拜。承認吧,你從未真正喜歡過它,對不?
好,你不需要對所有指針說拜拜,但是你真的得對那些用來操控局部性資源(local resources)的指針說莎唷娜拉了。
舉個例子,你正在為“小動物收養保護中心”(一個專門為小狗小貓尋找收養家庭的組織)編寫一個軟件。收養中心每天都會產生一個文件,其中有它所安排的當天收養個案。你的工作就是寫一個程序,讀這些文件,然后為每一個收養個案做適當處理。
合理的想法是定義一個抽象基類(abstract base class)ALA("Adorable Litle Animal"),再從中派生出針對小狗和小貓的具體類(concrete classes)。其中有個虛函數processAdoption,負責“因動物種類而異”的必要處理動作。
class ALA{
public:
virtual void processAdoption()= 0;
...
};class Puppy: public ALA
{
public:
virtual ?void processAdoption();
...
};class Kitten: public ALA
{
public:
virtual void processAdoption ();
...
};
你需要一個函數,讀取文件內容,并視文件內容產生一個Puppyobject或一個Kitten object。這個任務非常適合用virtual constructor完成,那是條款25中描述的一種函數。
對本目的而言,以下聲明便是我們所需要的:
//從s讀取動物信息,然后返回一個指針,指向一個
// 新分配的對象,有著適當的類型(Puppy或Kitten)
ALA * readALA(istream& s);
你的程序核心大約是一個類似這樣的函數:
void processAdoptions(istream& dataSource)
{while (datasource)//如果還有數據,{ALA* pa = readALA(dataSource);//取出下一只動物,pa->processAdoption();//處理收養事宜,delete pa;//刪除readALA返回的對象。}
}
這個函數走遍dataSource,處理它所獲得的每一條信息。
唯一需要特別謹慎的是,它必須在每次迭代的最后,記得將pa刪除。這是必要的,因為每當readALA被調用,便產生一個新的 heap object。如果沒有調用 delete,這個循環很快便會出現資源泄漏的問題。
現在請考慮:如果pa->processAdoption拋出一個exception,會發生什么事情。processAdoptions 無法捕捉它,所以這個 exception 會傳播到 processAdoptions的調用端。processAdoptions 函數內“位于pa->processAdoption 之后的所有語句”都會被跳過,不再執行,這也意味pa不會被刪除。結果呢,只要pa->processAdoption 拋出一個 exception,processAdoptions 便發生一次資源泄漏。
要避免這一點,很簡單:
void processAdoptions(istream& dataSource)
{while (dataSource){ALA* pa = readALA(dataSource),try {pa->processAdoption();}catch (...)//捕捉所有的exceptions。{delete pa;//當exception 被拋出,避免資源泄漏。throw;//將exception 傳播給調用端。}delete pa;//如果沒有exception被拋出,也要避免資源泄漏。}
}
但你的程序因而被try 語句塊和catch 語句塊搞得亂七八糟。
更重要的是,你被迫重復撰寫其實可被正常路線和異常路線共享的清理代碼(cleanup code)本例指的是delete動作。
這對程序的維護造成困擾,撰寫時很煩人,感覺也不理想。不論我們是以正常方式或異常方式(拋出一個exception)離開processAdoptions函數,我們都需要刪除pa,那么何不集中于一處做這件事情呢?
其實不必大費周章,只要我們能夠將“一定得執行的清理代碼”移到processAdoptions函數的某個局部對象的destructor 內即可。因為局部對象總是會在函數結束時被析構,不論函數如何結束(唯一例外是你調用longjmp而結束。longjmp 的這個缺點正是C++ 支持exceptions的最初的主要原因)。于是,我們真正感興趣的是,如何把delete 動作從 processAdoptions 函數移到函數內某個局部對象的destructor內。
解決辦法就是,以一個“類似指針的對象”取代指針pa,如此一來,當這個類似指針的對象被(自動)銷毀,我們可以令其destructor 調用delete。
“行為類似指針(但動作更多)”的對象我們稱為smart pointers。如條款28所言,你可以做出非常靈巧的“指針類似物”。本例倒是不需要什么特別高檔的產品,我們只要求它在被銷毀(由于即將離開其scope)之前刪除它所指的對象,就可以啦。
技術上這并不困難,我們甚至不需要自己動手。C++標準程序庫(見條款E49)提供了一個名為auto_ptr的class template,其行為正是我們所需要的。每個auto_ptr的constructor 都要求獲得一個指向 heap object 的指針;其destructoy會將該heapobject 刪除。
如果只顯示這些基本功能,auto_ptr 看起來像這樣:
template<class T>
class auto_ptr
{
public:auto_ptr(T* p = 0) :ptr(p) {}// 存儲對象。~auto ptr(){delete ptr;// 刪除對象。}private:T* ptr;//原始指針(指向對象).
};
auto_ptr 標準版遠比上述復雜得多。上述這個剝掉一層皮的東西并不適合實際運用2(至少還需加上copy constructor,assignment operator 及條款28所討論的指針仿真函數operator*和operator->),但是其背后的觀念應該很清楚了:以auto_ptr 對象取代原始指針,就不需再擔心heap objects沒有被刪除一一即使是在exceptions被拋出的情況下。
注意,由于auto ptr destructor 采用“單一對象”形式的delete,所以auto ptr 不適合取代(或說包裝)數組對象的指針。如果你希望有一個類似 autoptr的template可用于數組身上,你得自己動手寫一個。不過如果真是這樣,或許更好的選擇是以vector 取代數組。
以auto_ptr對象取代原始指針之后,processAdoptions 看起來像這樣:
void processAdoptions(istream& dataSource)
{while (dataSource){auto_ptr<ALA> pa(readALA(dataSource)pa->processAdoption();}
}
這一版和原先版本的差異只有兩處。
- 第一,pa被聲明為一個 auto_ptr<ALA>對象,不再是原始的 ALA*指針;
- 第二,循環最后不再有delete 語句。就這樣啦,其他每樣東西都沒變,除了析構動作外,auto_ptr 對象的行為和正常指針完全一樣。很簡單,不是嗎?
隱藏在auto ptr背后的觀念一—以一個對象存放“必須自動釋放的資源”,并依賴該對象的destructor 釋放一—亦可對“以指針為本”以外的資源施行。
考慮圖形界面(GUT)應用軟件中的某個函數,它必須產生一個窗口以顯示某些信息:
//此函數可能會在拋出一個 exception 之后發生資源泄漏問題。
void displayInfo(const Informations info)
{WINDOW_HANDLE w(createwindow());displayinfo in window corresponding to w,destroyWindow(w);
}
許多窗口系統都有 C語言接口,運行諸如 createWindow 和 destroyWindow這類函數,取得或釋放窗口(被視為一種資源)。如果在信息顯示于的過程中發生exception,w所持有的那個窗口將會遺失,其他動態分配的任何資源也會遺失。
解決之道和先前一樣,設計一個class,令其constructor 和destructor 分別取得資源和釋放資源:
//這個class 用來取得及釋放一個 window handle。
class WindowHandle {
public:WindowHandle(WINDOW HANDLE handle) :w(handle) {}~WindowHandle() {destroyWindow(w);}operator WINDOW_HANDLE(){return w;//詳述于下。}
private:WINDOW_HANDLE w;//以下函數被聲明為private,用以阻止產生多個 WINDOW HANDLE。//條款28討論了一個更彈性的做法。WindowHandle(const WindowHandle&);WindowHandle& operator=(const WindowHandle&);
};
這看起來就像auto_ptr template 一樣,只不過其賦值動作(assignment)和復制動作(copying)被明確禁止了(見條款E27)。
此外,它有一個隱式類型轉換操作符,可用來將一個 windowHandle 轉換為一個 WINDOW HANDLE。
這項能力對于WindowHandleobject 的實用性甚有必要,意味你可以像在任何地方正常使用原始的WINDOW_HANDLE一樣使用一個 windowHandle(不過條款5也告訴你為什么應該特別小心隱式類型轉換操作符)。
?
有了這個 WindowHandle class,我們可以重寫 displayInfo如下:
// 此函數可以在exception 發生時避免出現資源泄漏問題。
void displayInfo(const Information& info)
{WindowHandle w(createwindow());display info in window corresponding to w;
}
現在即使 displayInfo函數內拋出 exception,createWindow 所產生的窗口還是會被銷毀。
只要堅持這個規則,把資源封裝在對象內,通常便可以在exceptions 出現時避免泄漏資源。
但如果exception 是在你正取得資源的過程中拋出的,例如在一個“正在抓取資源”的class constructor 內,會發生什么事呢?
如果exception 是在此類資源的自動析構過程中拋出的,又會發生什么事呢?此情況下constructors和destructors 是否需要特殊設計?
是的,它們需要,你可以在條款 10和條款 11中學到這些技術。
?