# Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
有很多方法取得時間,所以有必要建立一個 TimeKeeper base class(基類),并為不同的計時方法建立 derived classes(派生類):
```
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };
```
很多客戶只是想簡單地取得時間而不關心如何計算的細節,所以一個 factory function(工廠函數)——返回 a base class pointer to a newly-created derived class object(一個指向新建派生類對象的基類指針)的函數——可以被用來返回一個指向 timekeeping object(計時對象)的指針:
```
TimeKeeper* getTimeKeeper(); // returns a pointer to a dynamic-
// ally allocated object of a class
// derived from TimeKeeper
```
與 factory function(工廠函數)的慣例一致,getTimeKeeper 返回的 objects(對象)是建立在 heap(堆)上的,所以為了避免泄漏內存和其它資源,每一個返回的 objects(對象)被完全 deleted 是很重要的。
```
TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object
// from TimeKeeper hierarchy
... // use it
delete ptk; // release it to avoid resource leak
```
Item 13 解釋了為什么依賴客戶執行刪除任務是 error-prone(錯誤傾向),Item 18 解釋了 factory function(工廠函數)的 interface(接口)應該如何改變以防止普通的客戶錯誤,但這些在這里都是次要的,因為在這個 Item 中,我們將精力集中于上面的代碼中一個更基本的缺陷:即使客戶做對了每一件事,也無法預知程序將如何運轉。
問題在于 getTimeKeeper 返回一個 pointer to a derived class object(指向派生類對象的指針)(比如 AtomicClock),那個 object(對象)經由一個 base class pointer(基類指針)(也就是一個 TimeKeeper\* pointer)被刪除,而且這個 base class(基類)(TimeKeeper) 有一個 non-virtual destructor(非虛擬析構函數)。禍端就在這里,因為 C++ 規定:當一個 derived class object(派生類對象)通過使用一個 pointer to a base class with a non-virtual destructor(指向帶有非虛擬析構函數的基類的指針)被刪除,則結果是未定義的。運行時比較典型的后果是 derived part of the object(這個對象的派生部分)不會被析構。如果 getTimeKeeper 返回一個指向 AtomicClock object(對象)的指針,則 object(對象)的 AtomicClock 部分(也就是在 AtomicClock class 中聲明的 data members(數據成員))很可能不會被析構,AtomicClock 的 destructor(析構函數)也不會運行。然而,base class part(基類部分)(也就是 TimeKeeper 部分)很可能已被析構,這就導致了一個古怪的 "partially destroyed" object(“部分被析構”對象)。這是一個導致泄漏資源,破壞數據結構以及消耗大量調試時間的絕妙方法。
消除這個問題很簡單:給 base class(基類)一個 virtual destructor(虛擬析構函數)。于是,刪除一個 derived class object(派生類對象)的時候就有了你所期望的行為。將析構 entire object(整個對象),包括全部的 derived class parts(派生類構件):
```
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk; // now behaves correctly
```
類似 TimeKeeper 的 base classes(基類)一般都包含除了 destructor(析構函數)以外的其它 virtual functions(虛擬函數),因為 virtual functions(虛擬函數)的目的就是允許 derived class implementations(派生類實現)的定制化(參見 Item 34)。例如,TimeKeeper 可以有一個 virtual functions(虛擬函數)getCurrentTime,它在各種不同的 derived classes(派生類)中有不同的實現。幾乎所有擁有 virtual functions(虛擬函數)的 class(類)差不多都應該有一個 virtual destructor(虛擬析構函數)。
如果一個 class(類)不包含 virtual functions(虛擬函數),這經常預示不打算將它作為 base class(基類)使用。當一個 class(類)不打算作為 base class(基類)時,將 destructor(析構函數)虛擬通常是個壞主意。考慮一個表現二維空間中的點的 class(類):
```
class Point { // a 2D point
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
```
如果一個 int 占用 32 bits,一個 Point object 正好適用于 64-bit 的寄存器。而且,這樣一個 Point object 可以被作為一個 64-bit 的量傳遞給其它語言寫的函數,比如 C 或者 FORTRAN。如果 Point 的 destructor(析構函數)被虛擬,情況就完全不一樣了。
virtual functions(虛擬函數)的實現要求 object(對象)攜帶額外的信息,這些信息用于在運行時確定該 object(對象)應該調用哪一個 virtual functions(虛擬函數)。典型情況下,這一信息具有一種被稱為 vptr ("virtual table pointer")(虛擬函數表指針)的指針的形式。vptr 指向一個被稱為 vtbl ("virtual table")(虛擬函數表)的 array of function pointers(函數指針數組),每一個帶有 virtual functions(虛擬函數)的 class(類)都有一個相關聯的 vtbl。當在一個 object(對象)上調用 virtual functions(虛擬函數)時,實際的被調用函數通過下面的步驟確定:找到 object(對象)的 vptr 指向的 vtbl,然后在 vtbl 中尋找合適的 function pointer(函數指針)。
virtual functions(虛擬函數)是如何實現的細節并不重要。重要的是如果 Point class 包含一個 virtual functions(虛擬函數),這個類型的 object(對象)的大小就會增加。在一個 32-bit 架構中,它們將從 64 bits(相當于兩個 ints)長到 96 bits(兩個 ints 加上 vptr);在一個 64-bit 架構中,它們可能從 64 bits 長到 128 bits,因為在這樣的架構中指針的大小是 64 bits 的。為 Point 加上 vptr 將會使它的大小增長 50-100%!Point object(對象)不再適合 64-bit 寄存器。而且,Point object(對象)在 C++ 和其它語言(比如 C)中,看起來不再具有相同的結構,因為它們在其它語言中的對應物沒有 vptr。結果,Points 不再可能傳入其它語言寫成的函數或從其中傳出,除非你為 vptr 做出明確的補償,而這是它自己的實現細節并因此失去可移植性。
最終結果就是無故地將所有 destructors(析構函數)聲明為 virtual(虛擬),和從不把它們聲明為 virtual(虛擬)一樣是錯誤的。實際上,很多人總結過這條規則:declare a virtual destructor in a class if and only if that class contains at least one virtual function(當且僅當一個類中包含至少一個虛擬函數時,則在類中聲明一個虛擬析構函數)。
甚至在完全沒有 virtual functions(虛擬函數)時,也有可能糾纏于 non-virtual destructor(非虛擬析構函數)問題。例如,標準 string 類型不包含 virtual functions(虛擬函數),但是被誤導的程序員有時將它當作 base class(基類)使用:
```
class SpecialString: public std::string { // bad idea! std::string has a
... // non-virtual destructor
};
```
一眼看上去,這可能無傷大雅,但是,如果在程序的某個地方因為某種原因,你將一個 pointer-to-SpecialString(指向 SpecialString 的指針)轉型為一個 pointer-to-string(指向 string 的指針),然后你將 delete 施加于這個 string pointer(指針),你就立刻被放逐到 undefined behavior(未定義行為)的領地:
```
SpecialString *pss = new SpecialString("Impending Doom");
std::string *ps;
...
ps = pss; // SpecialString* → std::string*
...
delete ps; // undefined! In practice,
// *ps's SpecialString resources
// will be leaked, because the
// SpecialString destructor won't
// be called.
```
同樣的分析可以適用于任何缺少 virtual destructor(虛擬析構函數)的 class(類),包括全部的 STL container(容器)類型(例如,vector,list,set,tr1::unordered_map(參見 Item 54)等)。如果你受到從 standard container(標準容器)或任何其它帶有 non-virtual destructor(非虛擬析構函數)的 class(類)繼承的誘惑,一定要挺住!(不幸的是,C++ 不提供類似 Java 的 final classes(類)或 C# 的 sealed classes(類)的 derivation-prevention mechanism(防派生機制)。)
有時候,給一個 class(類)提供一個 pure virtual destructor(純虛擬析構函數)能提供一些便利。回想一下,pure virtual functions(純虛擬函數)導致 abstract classes(抽象類)——不能被實例化的 classes(類)(也就是說你不能創建這個類型的 objects(對象))。然而,有時候,你有一個 class(類),你希望它是抽象的,但沒有任何 pure virtual functions(純虛擬函數)。怎么辦呢?因為一個 abstract classes(抽象類)注定要被用作 base class(基類),又因為一個 base class(基類)應該有一個 virtual destructor(虛擬析構函數),還因為一個 pure virtual functions(純虛擬函數)產生一個 abstract classes(抽象類),好了,解決方案很簡單:在你想要變成抽象的 class(類)中聲明一個 pure virtual destructor(純虛擬析構函數)。這是一個例子:
```
class AWOV { // AWOV = "Abstract w/o Virtuals"
public:
virtual ~AWOV() = 0; // declare pure virtual destructor
};
```
這個 class(類)有一個 pure virtual functions(純虛擬函數),所以它是抽象的,又因為它有一個 virtual destructor(虛擬析構函數),所以你不必擔心析構函數問題。這是一個螺旋。然而,你必須為 pure virtual destructor(純虛擬析構函數)提供一個 definition(定義):
```
AWOV::~AWOV() {} // definition of pure virtual dtor
```
destructors(析構函數)的工作方式是:most derived class(層次最低的派生類)的 destructor(析構函數)最先被調用,然后調用每一個 base class(基類)的 destructors(析構函數)。編譯器會生成一個從它的 derived classes(派生類)的 destructors(析構函數)對 ~AWOV 的調用,所以你不得不確保為函數提供一個函數體。如果你不這樣做,連接程序會提出抗議。
為 base classes(基類)提供 virtual destructor(虛擬析構函數)的規則僅僅適用于 polymorphic base classes(多態基類)—— base classes(基類)被設計成允許通過 base class interfaces(基類接口)對 derived class types(派生類類型)進行操作。TimeKeeper 就是一個 polymorphic base classes(多態基類),因為即使我們只有類型為 TimeKeeper 的 pointers(指針)指向它們的時候,我們也期望能夠操作 AtomicClock 和 WaterClock objects(對象)。
并非所有的 base classes(基類)都被設計用于 polymorphically(多態)。例如,無論是 standard string type(標準 string 類型),還是 STL container types(STL 容器類型)全被設計成 base classes(基類),可沒有哪個是 polymorphic(多態)的。一些 classes(類)雖然被設計用于 base classes(基類),但并非被設計用于 polymorphically(多態)。這樣的 classes(類)——例如 Item 6 中的 Uncopyable 和標準庫中的 input_iterator_tag(參見 Item 47)——沒有被設計成允許經由 base class interfaces(基類接口)對 derived class objects(派生類對象)進行操作。所以它們就不需要 virtual destructor(虛擬析構函數)。
Things to Remember
* polymorphic base classes(多態基類)應該聲明 virtual destructor(虛擬析構函數)。如果一個 class(類)有任何 virtual functions(虛擬函數),它就應該有一個 virtual destructor(虛擬析構函數)。
* 不是設計用來作為 base classes(基類)或不是設計用于 polymorphically(多態)的 classes(類)就不應該聲明 virtual destructor(虛擬析構函數)。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射