C++中的有些析構函數也被定義為virtual虛函數,這是為什麼?

在閱讀C++項目(caffe)源碼時,發現不少基類不僅把常規的成員函數定義成虛函數(virtual),也會把

析構函數定義為虛函數,結合前面幾節的介紹,稍稍思考下,這樣做的確是有原因的,本文將結合C++代碼實例嘗試探討下。

C++中的有些析構函數也被定義為virtual虛函數,這是為什麼?

為什麼要把析構函數定義為虛函數?

常規

隨便寫一段C++代碼作為實例,在這個例子中,我們先不把析構函數定義為虛函數:

<code>class Base {
public:
    Base () {
        cout << "Base construct\n";
    }
    ~Base() {
        cout << "Base deconstruct\n";
    }
    virtual void foo() {
        cout << "Base::foo\n";    
    }
    
    char *buf;
};

class Child: public Base {
public:
    Child() {
        cout << "Child construct\n";    
        buf = new char[16];
    }
    ~Child() {
        delete[] buf;
        cout << "Child deconstruct, delete buf\n";
    }
    void foo() {
        buf[0] = 3;
        cout << "Child::foo\n";
    }
};/<code>

這段代碼的邏輯很簡單,無非就是定義了兩個類:類 Base 的成員函數 foo() 為虛函數,構造函數和析構函數都是常規函數,此外它還有個 public 的成員變量 buf。類 Child 則公開繼承了 Base,因此它可以直接使用 Base::buf——在構造函數中 new 了一段內存,並且在析構函數 delete 掉它。

<code>Child c;
c.foo();/<code>

我們直接使用 Child 實例化一個對象 c,調用 c.foo(),此時得到如下輸出:

<code>Base construct
Child construct
Child::foo
Child deconstruct, delete buf
Base deconstruct/<code>

一切盡在預料中。

不安全的問題

雖說對象 c 調用 foo() 的輸出完全符合預計,但像上面那樣定義類仍然是非常危險的做法。在這一節我們曾討論過,父類指針可以調用派生類的重寫函數,因此下面這兩行C++代碼也是合法的,請看:

<code>Base *pb = new Child();
pb->foo();

delete pb;/<code>

編譯這段C++代碼完全沒有問題,運行也不會報錯,輸出如下:

<code>Base construct
Child construct
Child::foo
Base deconstruct/<code>

可是,從輸出信息能夠看出,派生類 Child 的析構函數沒有被調用,對於本例而言,new 出來的 buf 沒有對應的 delete,勢必會造成內存洩漏。

解決問題

要解決所謂的“不安全問題”,其實很簡單,按照題目說的做——將基類的析構函數也定義為虛函數就可以了,請看修改後的C++代碼:

<code>class Base {
public:
    Base () {
        cout << "Base construct\n";
    }
    virtual ~Base() {
        cout << "Base deconstruct\n";
    }
.../<code>

也即盡在基類 Base 的析構函數前加上 virtual 關鍵字,其他的所有代碼都無需改動。現在再執行下面的這幾行C++代碼:

<code>Base *pb = new Child();
pb->foo();

delete pb;/<code>

輸出如下:

<code>Base construct
Child construct
Child::foo
Child deconstruct, delete buf
Base deconstruct/<code>

顯然,此時派生類 Child 的析構函數也會被調用了,內存洩漏的問題被解決了。

小結

C++ 中的 virtual 關鍵字是非常好用,也是C++程序員必須掌握的關鍵字,其實,“不安全問題”出現的原因也是簡單的:我們在靜態類型與動態綁定一節中提到過,基本上只有涉及到 virtual 函數時,才會發生動態綁定,此時通過對象指針(pb)調用的函數由它指向的類(Child)決定,所以此時派生類 Child 的析構函數會被調用。如果基類 Base 的析構函數不是虛函數,那麼對象指針(pb)調用的函數由其靜態類型(Base)決定,也即調用的其實只是基類 Base 的析構函數而已。


分享到:


相關文章: