天天看點

條款07(一):為多态基類聲明virtual析構函數條款07:為多态基類聲明virtual析構函數

條款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.可能有基類指針,指向的确是派生類成員的情況。(這是很正常的)

那麼當析構一個指向派生類成員的基類指針時,程式就不知道怎麼辦了。

是以要保證運作适當的析構函數,基類中的析構函數必須為虛析構。