條款14 基類的解構函式一定要定義為虛擬函式(From Effective C++) (轉)
有時一個類需要知道當前有多少個該類的,達到這個目的最直接的方式是定義一個用於統計物件個數的靜態成員變數。該變數被初始化為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 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 new NamedArray Array ... pa = pna; // NamedArray ... 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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- C++之類解構函式為什麼是虛擬函式C++函式
- 介面、虛擬函式、純虛擬函式、抽象類函式抽象
- C++ 派生類函式過載與虛擬函式繼承詳解C++函式繼承
- 虛擬函式,虛擬函式表函式
- 虛擬函式 純虛擬函式函式
- C++建構函式和解構函式呼叫虛擬函式時使用靜態聯編C++函式
- 抽象基類和純虛擬函式抽象函式
- c++虛擬函式表C++函式
- 【C++筆記】虛擬函式(從虛擬函式表來解析)C++筆記函式
- 【C++筆記】虛擬函式(從虛擬函式概念來解析)C++筆記函式
- 深入C++成員函式及虛擬函式表C++函式
- 基類指標、虛純虛擬函式、多型性、虛析構指標函式多型
- C++ 介面(純虛擬函式)C++函式
- C++ 虛擬函式表解析C++函式
- 建構函式定義的隱式型別轉換函式型別
- C++解構函式C++函式
- C++多型之虛擬函式C++多型函式
- 詳解C++中的多型和虛擬函式C++多型函式
- C++ 虛解構函式簡單測試C++函式
- 類的建構函式和解構函式函式
- 避免對派生的非虛擬函式進行重定義函式
- C++ 建構函式和解構函式C++函式
- [Lang] 虛擬函式函式
- C++虛擬函式學習總結C++函式
- C++中建構函式,拷貝建構函式和賦值函式的詳解C++函式賦值
- Rust中的into函式和from函式Rust函式
- 虛擬函式的呼叫原理函式
- 基類指標,子類指標,虛擬函式,override與final指標函式IDE
- [cpp]C++中的解構函式C++函式
- 02_函式定義及使用函式函式
- 如何在函式內部定義函式?函式
- C++入門教程(12):定義函式C++函式
- 建構函式與解構函式函式
- C++:建構函式的分類和呼叫C++函式
- 內聯(inline)函式與虛擬函式(virtual)的討論inline函式
- 虛擬函式表-C++多型的實現原理函式C++多型
- 兄弟連go教程(11)函式 - 函式定義Go函式
- 什麼是Python函式?如何定義函式?Python函式
- C++入門記-建構函式和解構函式C++函式