程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> Effective C++讀書筆記(5)

Effective C++讀書筆記(5)

編輯:C++入門知識

條款07:為多態基類聲明virtual析構函數

Declare destructors virtual inpolymorphic base classes

建立一個 TimeKeeper基類,並為不同的計時方法建立派生類:

class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};

class AtomicClock: public TimeKeeper { ... };//原子鐘

class WaterClock: public TimeKeeper { ... };//水鐘

class WristWatch: public TimeKeeper { ... };//腕表

TimeKeeper* getTimeKeeper();

//返回一個指針,指向一個TimeKeeper派生類的動態分配對象

TimeKeeper *ptk = getTimeKeeper(); //從TimeKeeper繼承體系獲得一個動態分配對象

...                                  //運用它

delete ptk;                            //釋放它,避免資源洩漏

很多客戶只是想簡單地取得時間而不關心如何計算的細節,所以一個 factoryfunction(工廠函數)——返回一個指向新建派生類對象的基類指針的函數——可以被用來返回一個指向計時對象的指針。與工廠函數的慣例一致,getTimeKeeper 返回的對象建立在堆上的,所以為了避免洩漏內存和其它資源,每一個返回的對象被適當delete掉是很重要的。

C++ 規定:當一個派生類對象通過使用一個指向non-virtual析構函數的基類的指針被刪除時,則這個對象的派生部分沒被銷毀。如果 getTimeKeeper 返回一個指向 AtomicClock對象的指針,則對象的 AtomicClock 部分(也就是在 AtomicClock class中聲明的數據成員)很可能不會被析構,AtomicClock 的析構函數也不會運行。然而,基類部分(也就是 TimeKeeper 部分)很可能已被析構,這就導致了一個詭異的局部銷毀對象,導致洩漏資源。

消除這個問題很簡單:給基類一個 virtual析構函數。於是,刪除一個派生類對象的時候就將析構整個對象,包括所以的派生類成分。

類似 TimeKeeper 的基類一般都包含除了析構函數以外的其它virtual函數,因為virtual函數的目的就是允許派生類實現的定制化。例如,TimeKeeper 可以有一個virtual函數getCurrentTime,它在各種不同的派生類中有不同的實現。任何類只要帶有virtual函數都幾乎確定應該也有一個virtual析構函數。

 

如果一個類不包含virtual函數,這通常預示不打算將它作為基類使用。當一個類不打算作為基類時,令其析構函數為virtual通常是個壞主意。考慮一個表現二維空間中的點類:

class Point { // a 2D point
public:
Point(int xCoord, int yCoord);
~Point();

private:
int x, y;
};

如果一個 int 占用 32 bits,一個 Point 對象 正好適用於 64-bit 緩存器。而且,這樣一個 Point 對象 可以被作為一個 64-bit 量傳遞給其它語言寫的函數,比如 C 或者FORTRAN。而當Point 的析構函數為virtual時,要表現出virtual函數,對象必須攜帶額外的信息,用於在運行時確定該對象應該調用哪一個virtual虛擬函數。這一信息通常由被稱為 vptr ("virtual table pointer")的指針指出,vptr 指向一個被稱為 vtbl("virtual table")的函數指針數組;每一個帶有 virtual函數的類都有一個相關聯的 vtbl。當在一個對象上調用 virtual函數時,實際的被調用函數通過下面的步驟確定:找到對象vptr 指向的 vtbl,然後在 vtbl 中尋找合適的函數指針。

如果 Point類 包含一個 virtual函數,會為 Point 加上 vptr,將會使對象大小增長 50-100%! Point對象不再適合64-bit 寄存器。而且,Point對象在 C++ 和其它語言(比如 C)中不再具有相同的結構,因為其它語言中的對應物沒有 vptr。結果,Points 不再可能傳入其它語言寫成的函數或從其中傳出,並失去可移植性。

無故地將所有析構函數聲明為 virtual,和從不把它們聲明為 virtual一樣是錯誤的。實際上,很多人總結過這條規則:當且僅當一個類中包含至少一個虛擬函數時,則在類中聲明一個虛擬析構函數。

·    多態基類應該聲明virtual析構函數。如果一個類帶有任何 virtual函數,它就應該有一個virtual析構函數。

 

即使完全沒有virtual函數,也有可能糾纏於 non-virtual析構函數問題。例如,標准 string 類型不包含 virtual函數,但是程序員有時將它當作基類使用:

class SpecialString: public std::string {
... //bad idea!std::string有個non-virtual析構函數
};

如果在程序中將一個指向 SpecialString 的指針轉型為一個指向 string 的指針,然後delete 那個string指針,將導致內存洩漏,行為不明確:

SpecialString *pss = newSpecialString("Impending Doom");

std::string *ps;
...
ps = pss; // SpecialString* => std::string*
...
delete ps; /*未有定義!現實中*ps的SpecialString資源會洩漏,因為SpecialString析構函數未被調用。*/

不要企圖繼承標准容器(例如,vector,list,set,tr1::unordered_map)或任何其他“帶有non-virtual析構函數”的類。C++ 不提供類似 Java 的 final classes或 C# 的 sealed classes那樣的禁止派生機制。

有時候,給一個類提供一個 pure virtual析構函數能提供一些便利。pure virtualfunctions函數導致抽象類,也就是說你不能創建這個類型的對象。然而有時候你希望類是抽象的,但沒有任何 pure virtual函數。怎麼辦呢?

解決方案很簡單:在你想要變成抽象的類中聲明一個 pure virtual析構函數:

class AWOV { // AWOV = "Abstract w/oVirtuals"
public:
virtual ~AWOV() = 0; // declare pure virtual destructor
};

這個類有一個 purevirtual函數,所以它是抽象的,又因為它有一個 virtual析構函數,所以你不必擔心析構函數問題。然而,你必須為 purevirtual析構函數提供一個定義:

AWOV::~AWOV() {} // pure virtual析構函數的定義

析構函數的工作方式是:最深層派生的那個類其析構函數最先被調用,然後調用其每一個基類)的析構函數。編譯器會生成一個從其派生類的析構函數對 ~AWOV 的調用動作,所以你不得不為這個函數提供一份定義,不然連接器會發出抱怨。

為基類提供virtual析構函數的規則僅僅適用於 polymorphic(帶多態性質的)基類上。這種基類的設計目的就是為了用來“通過基類接口處理派生類對象”。TimeKeeper 就是一個多態基類,因為即使我們只有類型為 TimeKeeper 的指針指向它們時,也期望能夠操作 AtomicClock 和 WaterClock對象。

並非所有的基類的設計目的都是為了多態用途。例如,無論是標准 string還是 STL容器都不被設計成基類使用,更別提多態了。某些類雖然被設計用於基類,但並非用於多態用途。如Uncopyable 和標准庫中的 input_iterator_tag,它們並非被設計用來“經由基類接口處理派生類對象”,因此不需要virtual析構函數。

·    不是設計用來作為基類或為了具備多態性的類,就不應該聲明 virtual析構函數

 摘自 pandawuwyj的專欄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved