declare destructors virtual in polymorphic base classes.
有許多種做法可以記錄時間,因此,設計乙個timekeeper base class和一些derived classes作為不同的計時方法,相當合情合理:
class timekeeper ;
class atomicclock:public timekeeper ;//原子鐘
class waterclock:public timekeeper ;//水鐘
class wristwatch:public timekeeper //腕表
許多客戶只想在程式中使用時間,不想操心時間如何計算等細節,這時候我們可以設計factory(工廠)函式,返回指標指向乙個計時物件。factory函式會「返回乙個base class指標,指向新生成的derived class物件」:timekeeper *gettimekeeper();
為遵守factory函式的規範,被gettimekeeper()返回的物件必須位於heap。因此為了避免洩露記憶體和其他資源,將factory函式返回的每乙個物件適當的delete掉很重要:
timekeeper* ptk = gettimekeeper();
...delete ptk;
但是這裡卻存在問題,問題出在gettimekeeper()返回的指標指向乙個derived class物件,而那個物件卻經由乙個base class指標被刪除,而目前的base有個non-virtual析構函式。這是乙個引來災難的秘訣,因為c++明白指出,當derived class物件經由乙個base class指標被刪除,而該base class帶著乙個non-virtual析構函式,其結果為定義------實際執行時通常發生的是物件的derived成分沒有被銷毀。如果gettimekeeper()返回指標指向乙個atomicclock物件,其內的成分很可能沒被銷毀,而atomicclock的析構函式也未能執行起來。然而其base class成分通常會被銷毀,於是造成乙個詭異的「區域性銷毀」物件。這可是形成資源洩露、敗壞之資料結構、在偵錯程式上浪費許多時間的絕佳途徑。
消除這個問題的做法很簡單:給base class乙個virtual析構函式。此後刪除derived class物件就會如你想要的那般。
class timekeeper ;
timekeeper* ptk = gettimekeeper();
...delete ptk;
像timekeeper這樣的base classes除了析構函式之外通常還有其他virtual函式,因為virtual函式的目的是允許derived class的實現得以客製化。例如timekeeper就可能擁有乙個virtual getcurrenttime,它在不同的derived classes中有不同的實現碼。任何class只要帶有virtual函式都幾乎確定應該也有乙個virtual析構函式。
如果class不含virtual函式,通常表示它並不意圖被用作乙個base class。當class不企圖被當作base class,令其析構函式為virtual往往是個餿主意。考慮乙個用來表示二維空間點座標的class:
class point;
如果int占用32bits,那麼point物件可塞入乙個64-bits快取器中。更有甚者,這樣乙個point物件可被當作乙個「64bit量」傳給以其他語言如c或fortran撰寫的函式。然而當point的析構函式是virtual,形勢起了變化。欲實現出virtual函式,物件必須攜帶某些資訊,主要用來在執行期決哪乙個virtual函式被呼叫。這份資訊通常是由乙個所謂vptr(virtual table pointer)指標指出。vptr指向乙個由函式指標構成的陣列,稱為vtbl(virtual table);每乙個帶有virtual函式的class都有乙個相應的vtbl。當物件呼叫某個virtual函式,實際被呼叫的函式取決於該物件的vptr所指的那個vtbl---編譯器在其中尋找適當的函式指標。
virtual函式的實現細節不重要。重要的是如果point class內含virtual函式,其物件的提及會增加:在32-bit計算機體系結構中將占用64bits(為了存放兩個ints)至96bits(兩個ints加上vptr)。因此,為point新增乙個vptr會增加其物件大小達50%~100%!point物件不再能夠塞入乙個64bit快取器,而c++的point物件也不再和其他語言內的相同的宣告有著一樣的結構,因此就不再可能把它傳遞至其他語言所寫的函式,除非你明確補償vptr。因此無端地將所有classes的析構函式宣告為virtual,就像從未宣告它們為virtual一樣,都是錯誤的。許多人的心得是:只有當class內含至少乙個virtual函式,才為它宣告virtual析構函式。即使class完全不帶virtual函式,被「non-virtual析構函式問題」給咬傷還是有可能的。舉個例子,標誌string不含任何virtual函式,但有時候程式設計師會錯誤地把它當作base class:
class specialstring:public std::string;
咋看似乎無害,但如果你再程式任意某處無意間將乙個pointer-to-specialstring轉換為pointer-to-string,然後將轉換所得的那個string指標delete掉,你立刻被流放到「行為不明確」的惡地上。
相同的分析適用於任何不帶virtual析構函式的class,包括所有stl容器如vector,list,set等等。如果你曾經企圖繼承乙個標準容器或任何其他「帶有non-virtual析構函式」的class,拒絕**把!
有時候令class帶乙個pure virtual析構函式,可能頗為便利。pure virtual函式導致abstract classes---也就是不能被實體化的class。也就是說,你不能為那種型別建立物件。然而有時候你希望擁有抽象class,但手上沒有任何pure virtual函式,怎麼辦?由於抽象class總是企圖被當作乙個base class來用,而有由於base class應該有個virtual析構函式,並且由於pure virtual函式會導致抽象class,因此解法很簡單:為你希望它成為抽象的那個class 宣告乙個pure virtual析構函式。
class awov;
這個class有乙個pure virtual函式,所以它是個抽象class,又由於它有個virtual析構函式,所以你不需要擔心析構函式的問題。然而這裡有個竅門:你必須為這個pure virtual析構函式提供乙份定義。析構函式的運作方式是,最深層派生的那個class其析構函式最先被呼叫,然後是其每乙個base class的析構函式被呼叫。編譯器會在awov的derived class的析構函式中建立乙個對~awov的呼叫動作,所以你必須為這個函式提供乙份定義。如果不這樣做,聯結器會發出抱怨。
「給base classes乙個virtual析構函式」,這個規則只適用於polymorphic(帶多型性質的)base classes身上。這種base classes的設計目的是為了用來「通過base class介面處理derived class物件」。timekeeper就是乙個polymorphic base class,因為我們希望處理atomicclock和waterclock物件,縱使我們只有timekeeper指標指向它們。
並非所有base class的設計目的都是為了多型用途。例如標準string和stl容器都不被設計作為base classes使用,更別提多型了。某些classes的設計目的是作為base class使用,但不是為了多型用途,因此它們也不需要virtual析構函式。
請記住:
條款07 為多型基類宣告virtual析構函式
結論1 polymorphic 帶多型性質的 base classes 應宣告乙個virtual 析構函式。如果class帶有任何virtual函式,它就應該擁有乙個virtual析構函式。c 指出當derived class物件經由乙個base class指標被刪除,而該base class帶有乙...
條款07 為多型基類宣告virtual析構函式
1 任何帶有虛函式的類都幾乎確定應該定義乙個虛析構函式。乙個經驗是 只有當類含有至少乙個virtual函式才會為它宣告virtual析構函式。2 如果乙個類不含有virtual函式,通常表示它並不意圖作為基類 當類不意圖作為基類,令其析構函式為virtual是個餿主意。因為有虛函式,該類就要有指向虛...
條款07 為多型基類宣告virtual析構函式
條款07 為多型基類宣告virtual析構函式 1.c 明確指出,當子類物件經由乙個基類指標刪除,而該基類帶著乙個non virtual析構函式,其結果未有定義。實際執行時通常發生的是物件的derived成分沒有被銷毀。也就是不光子類 裡面的成員變數可能沒被銷毀,而子類的析構函式也未能執行起來。而基...