條款14 基類的解構函式一定要定義為虛擬函式(From Effective C++) (轉)

gugu99發表於2008-06-09
條款14 基類的解構函式一定要定義為虛擬函式(From Effective C++) (轉)[@more@]

  有時一個類需要知道當前有多少個該類的,達到這個目的最直接的方式是定義一個用於統計物件個數的靜態成員變數。該變數被初始化為0,類構造時增加1,呼叫解構函式時減少1。:namespace prefix = o ns = "urn:schemas--com::office" />

假設你正在編寫一個軍用,其中一個表示敵軍目標的類定義如下:

class EnemyTarget {


public:


  EnemyTarget() { ++numTargets; }


  EnemyTarget(const EnemyTarget&) { ++numTargets; }


  ~EnemyTarget() { --numTargets; }


  static size_t numberOfTargets()


  { return numTargets; }


  virtual bool destroy();  //當摧毀敵軍目標成功時返回true


private:


  static size_t numTargets;  //物件數量


};


//類靜態成員變數必須在類外定義;


//預設將其初始化為0


size_t EnemyTarget::numTargets;


當然,這個類的功能遠遠達不到國防部的要求,所以不可能為你贏得政府國防合同,但在這裡已經足夠我們說明問題的了。

假定在你的模擬中有一種特殊的敵軍目標是敵軍坦克,而敵軍坦克(EnemyTank)是從EnemyTarget公有繼承而來。由於你不僅要知道所有敵軍目標的數量,而且對敵軍坦克的數量也感興趣,所以你在派生類中使用與基類相同的技巧:

class EnemyTank: public EnemyTarget {


public:


  EnemyTank() { ++numTanks; }


  EnemyTank(const EnemyTank& rhs)


  : EnemyTarget(rhs)


  { ++numTanks; }


  ~EnemyTank() { --numTanks; }


  static size_t numberOfTanks()


  { return numTanks; }


  virtual bool destroy();


private:


  static size_t numTanks;  // 坦克的數量


};


現在,在新增兩個不同類的程式碼之後,你已經理解了條款M26(條款M26 限制一個類的物件個數——譯註)所介紹的這類問題的常規解決方案,並可能已經向正確的方向邁進了一步。

最後,假定在程式的某個地方,你使用new動態的建立了一個EnemyTank物件,然後你用delete刪除它:

EnemyTarget *targetPtr = new EnemyTank;


...


delete targetPtr;


到目前為止,好像一切正常。不僅兩個類的解構函式分別做了與其建構函式一致的“撤銷”操作,而且在你的程式中一定沒有任何錯誤——因為你將用new建立的物件很小心地delete掉了。但是,這裡隱藏著非常煩人的問題:該程式的行為是未定義的(undefined)——你不知道可能發生什麼事情。

在這一點上,C++語言標準的敘述異乎尋常的清楚:當你試圖使用基類的指標刪除派生類的物件時,如果基類沒有將解構函式定義為虛擬函式(就象EnemyTarget一樣),那麼結果是未定義的。這就是說可以隨心所欲生成程式碼,格式化你的,向你的老闆發密信,將傳真給你的競爭對手,什麼事都幹得出來。(執行程式時最可能發生的是派生類的解構函式從來不被呼叫。在這個例子中,這意味著當targetPtr被刪除時EnemyTank的數量並沒有改變,當然你所統計的敵軍坦克的數量就是錯誤的,依賴準確戰場資訊的戰士們就慘了!)

為了避免這個問題,你只能將EnemyTarget的解構函式定義為虛擬的。將解構函式定義為虛擬的將確保該類的行為是充分定義的(well-defined),從而讓它依你的意願行事:不論是EnemyTank還是EnemyTarget,當存放它們物件的被釋放時,對應的解構函式將被正確地呼叫。

這裡,EnemyTarget類擁有一個虛擬函式,這是定義基類的常規方式。畢竟虛擬函式的目的是允許在派生類中重新定製該函式的行為,幾乎所有的基類均擁有虛擬函式。

如果一個類沒有定義任何虛擬函式,通常表示它不準備用作基類。當一個類不準備用作基類時,定義虛擬解構函式通常是個餿主意。請看一個例子,這個例子來源於ARM(The Annotated C++ Reference Manual , Margaret Ellis 和 著 Addison-Wesley, 1990——譯註)

//表示2D點的類


class Point {


public:


  Point(short int xCoord, short int yCoord);


  ~Point();


private:


  short int x, y;


};


如果一個short int佔16位,一個Point物件正好適合一個32位暫存器。此外,一個Point物件可以作為32位的量傳給其他語言(如C或FORTRAN等)寫的函式。但是,如果將Point的解構函式改為虛擬的,情形就不同了。

實現虛擬函式需要該物件附帶一些附加資訊,從而使該物件在執行時能夠確定應該呼叫那個虛擬函式。在大多數編譯其中,這個附加資訊以一個叫做vptr(virtual table point)指標的形式存在,vptr指向一個稱為vtbl(virtual table)的函式指標陣列,每個擁有虛擬函式的類中都有相應的vtbl。當一個物件的虛擬函式被呼叫時,利用指向vtbl的vptr在vtbl中查詢適當的函式指標,從而確定哪個實際函式被呼叫。

如何實現虛擬函式的細節並不重要,重要的是如果Point類中如果存在虛擬函式,該類物件的實際大小就會翻倍,從兩個16位short變成兩個16位short加上一個32位vptr!Point物件就不能剛好存放在32位暫存器中了。此外,在C++中宣告的Point物件與其他語言(如C)宣告的類似結構不是一回事了,因為其他語言宣告的結構中並沒有vptr。這樣就不可能在C++函式與其它語言的函式之間傳遞Point物件,除非你顯式地新增vptr,而這樣要考慮到實現細節,當然不可移植。

總是將解構函式宣告為虛擬的與總是不將其宣告為虛擬的一樣不妙。事實上,許多人得出這個規律:當且僅當一個類中包含至少一個虛擬函式時定義該類的虛擬解構函式。

這是一個很好的規律,可以在大多數情況下工作,但是不幸的是,即使沒有任何虛擬函式,它也很可能由於解構函式非虛擬而出現問題。舉個例子,條款13(條款13:與宣告同樣的順序初始化成員變數——譯註)設計了一個用於實現定義範圍的陣列的類别範本,假設你想為其派生類寫一個模板,使派生類能夠代表命名陣列,也就是說,派生類例項化所得到的每個陣列都有自己的名字:

template  // 基模板類


class Array {   // (來自條款13)


public:


  Array(int lowBound, int highBound);


  ~Array();


private:


  vector data;


  size_t size;


  int lBound, hBound;


};


template


class NamedArray: public Array {


public:


  NamedArray(int lowBound, int highBound, const string& name);


  ...


private:


  string arrayName;


};


如果在程式的任何地方你將指向NamedArray的指標用某種方式轉換為指向Array的指標,並且對Array指標使用delete操作,你會立即陷入未定義程式行為的境地中:

NamedArray *pna =


  new NamedArray(10, 20, "Impending Doom");


Array *pa;


...


 


pa = pna;   // NamedArray* -> Array*


...


delete pa;  // 未定義! 實際上,pa->arrayName 的記憶體通


//常被洩漏了,因為*pa指向的NamedArray根


//本沒有被釋放


這類情形比你想象中發生的更頻繁,因為人們希望一個現存的類(如Array)及其派生類(NamedArray)做同樣的事情,這種情況並不少見。在上例中,NamedArray沒有重新定義Array的任何行為,它繼承了Array的函式但沒有改變它們,只是新增了額外的功能。然而,解構函式非虛擬所帶來的問題依然存在。

最後,值得一提的是在一些類中宣告純虛擬函式是很有好處的。我們知道定義純虛擬函式的類是抽象類——不能被例項化的類(也就是說你不能建立該型別的物件)。然而,有時你希望你的類是一個抽象類,但卻碰巧沒有任何函式是純虛擬函式,你該怎麼辦?好辦!因為這個抽象類必然要作為一個基類,而基類應該擁有一個虛擬解構函式,同時純虛擬函式的定義產生一個抽象類,所以答案非常簡單:如果你想將某個類定義為抽象類,只需為該類定義一個純虛擬解構函式。

請看這個例子:

class AWOV {  // AWOV = "Abstract w/o Virtuals"


public:


  virtual ~AWOV() = 0;   //宣告純虛擬解構函式


};


這個類有一個純虛擬函式,所以它是抽象類;而且這個虛擬函式是其解構函式,所以可以保證你不必擔心解構函式會帶來問題。但是,以上只是其一,你還必須為純虛擬解構函式提供定義:

AWOV::~AWOV() {}  // 純虛擬解構函式的定義


這個定義是必須的,因為虛擬解構函式的工作順序是這樣的:派生類的解構函式首先被呼叫,然後基類的解構函式被呼叫。這意味著即使AWOV是個抽象類,編譯器仍要產生一個對~AWOV的呼叫,因此你必須為這個函式提供函式實現。如果沒有提供的話,聯結器將提示你缺少符號,此時你只有回去加上它。

你可以在這個函式中做任何喜歡做的事,但就如上個例子所示,一般不讓它做任何事情。如果是這樣的話,你可能會將解構函式宣告為內聯的(inline),從而避免呼叫空函式體帶來的額外開銷。這是一個非常明智的策略,但是你應該知道這個策略的實質。

因為你的解構函式是虛擬的,它的地址必須放到該類的vtbl中,但是行內函數假設不是以獨立函式存在的(這就是內聯的意思,明白嗎?),所以必須求助於特殊措施來獲得它們的地址。條款33(條款33 明智地使用行內函數——譯註)提供了詳盡的解釋,但簡單地說真相是這樣的:如果宣告瞭一個內聯虛擬解構函式,你可能避免了呼叫該函式的額外開銷,但你的編譯器仍然會在某個地方為這個函式生成一個外聯(out-of-line)複製。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-1005434/,如需轉載,請註明出處,否則將追究法律責任。

相關文章