條款07:為多态基類聲明virtual析構函數
Declare destructors virtual in polymorphic base classes
該條款内容較多,分成兩章來進行學習記錄。
Virtual析構函數
首先,也是從一個例子入手。
對于時間的記錄,可以有許多種方法。是以,設計一個TimeKeeper base class和一些derived classes以作為不同的計時方法是一種比較可取的方法:
class TimeKeeper { //base class
public:
TimeKeeper();
~TimeKeeper(); //Non-vitrual的析構函數
...
};
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(); //從TimeKeeper繼承體系中擷取一個動态配置設定對象
... //使用這個對象
delete ptk; //釋放這個對象,避免資源洩露
但是,在上述的代碼中,縱使使用者把每一件事都做對了,仍然沒有辦法知道程式如何行動!
原因在于:
- getTimeKeeper傳回的指針指向一個derived class對象(例如AtomicClock),而這個對象卻經由一個base class指針(例如TimeKeeper*指針)删除。但是,目前的base class(TimeKeeper)有一個non-virtual析構函數。
之是以會引來錯誤,是因為C++明确指出,當derived class對象經由一個base class指針被删除,而該base class又帶有一個non-virtual析構函數,這樣操作的結果并沒有定義:
- 實際執行時通常發生的是對象的derived成分沒有被銷毀。
也就是說,如果getTimeKeeper傳回指針指向一個AtomicClock對象,其中的AtomicClock成分(即聲明于AtomicClock class内的成員變量)很可能沒有被銷毀,而AtomicClock的析構函數也未能執行。
然而,其中的base class成分(即TimeKeeper部分)卻通常會被銷毀,于是就造成了一種“局部銷毀”對象。
解決辦法:
-
給base class一個virtual析構函數。
此後,删除derived class對象,就會銷毀整個對象,包括所有的derived class的成分:
class TimeKeeper { //base class
public:
TimeKeeper();
virtual ~TimeKeeper(); //vitrual的析構函數
...
};
TimeKeeper* ptk = getTimeKeeper();
...
delete ptk;
像TimeKeeper這樣的base classes除了析構函數之外通常還有其他的virtual函數,因為:
- virtual函數的目的是允許derived class的實作得以客制化。
例如TimeKeeper就可能擁有一個virtual getCurrentTime,它在不同的derived classes中有不同的實作代代碼。任何class隻要帶有virtual函數,就幾乎可以确定也有一個virtual析構函數。
基類指針可以指向派生類的對象(多态性),如果删除該指針delete []p;就會調用該指針指向的派生類析構函數,而派生類的析構函數又自動調用基類的析構函數,這樣整個派生類的對象完全被釋放。如果析構函數不被聲明成虛函數,則編譯器實施靜态綁定,在删除基類指針時,隻會調用基類的析構函數而不調用派生類析構函數,這樣就會造成派生類對象析構不完全。是以,将析構函數聲明為虛函數是十分必要的。
1.每個析構函數(不加 virtual) 隻負責清除自己的成員。
2.可能有基類指針,指向的确是派生類成員的情況。(這是很正常的)
那麼當析構一個指向派生類成員的基類指針時,程式就不知道怎麼辦了。
是以要保證運作适當的析構函數,基類中的析構函數必須為虛析構。