構造函數與析構函數中不調用虛函數

本文參考《effective C++》第九條款
在C++中,提倡不能在構造函數和析構函數中調用虛函數。
這是爲什麼呢?

首先,我們先回顧一下C++虛函數的作用。 虛函數的引入是c++運行時多態的體現,通過調用虛函數可以在運行程序時實現動態綁定,體現了面向對象編程多態的思想。


那爲何提倡不能在構造函數與析構函數中不能調用虛函數。接下來我們通過代碼分析在構造函數或者虛構函數中調用虛函數是否可行。

假設我們有兩種商品A, B。 我們要記錄着兩種商品的銷售記錄,每當銷售一種商品時,我們就要記錄下來。

class item {
public:
    item();
    virtual void saleRecord() const=0;  //銷售記錄,純虛函數
    ...
};

item::item()
{
    ...
    virtual void saleRecord();
    ...
}

class itemA :  public item {
public:
    itemA();
    virtual void saleRecord();
    ...
};

class itemB : public item {
public:
    itemB();
    virtual void saleRecord();
    ...
};

我們執行如下代碼:

itemB b;

一個derived class B 對象會被創建, 在調用構造函數itemB() 之前, 基類的構造函數會首先被調用,即:基類成分會在派生類特有成分被構造之前被構造。 而在基類的構造函數中又調用了虛函數saleRecord(), 在此,你認爲基類構造函數中所調用的虛函數saleRecord的實現版本是哪一個呢,是基類的實現版本還是派生類B的實現版本呢?

我們會很自然的認爲, 現在構建的是itemB的對象, 構造函數中調用的當然是派生類B的實現版本。 其實我們可以通過運行程序確認基類構造函數中調用的虛函數實現版本是基類所有的,而不是派生類的實現版本。即使我們現在創建的是一個派生類。

原因其實很簡單,在創建派生類的時候,基類先於派生類被構造,編譯器或者程序其實目光是很短淺的或者說是很現實的,在調用基類構造函數的時候,它並不知道你最終是要創建一個基類還是派生類, 它只要把現在手頭上的工作做好——創建一個基類。 對編譯器來說,此時所有可見的信息包括data member 以及data function 都是基類所有的,它並不知道派生類的任何信息, 而且它所做的行爲也只是初始化派生類空間專屬於基類的那一部分, 不會越界。用一句話總結: 對象在調用什麼構造函數的時候,它就是什麼對象,他並不會像先知一樣能看的更遠。
以之前的代碼爲例。 創建派生類的時候, 基類首先被創建, 此時它只是一個基類,它的所見所爲都完完全全跟創建一個基類對象一樣,並不會下降到派生類。


我們可以極端一點,假設基類在創建的時候可以調用虛函數的派生類實現版本(其實不可能)。 此時,又會發生什麼呢。從意圖上講,我們要調用虛函數的不同版本,從根本上講,是要對不同的數據進行操作, 這樣函數纔有意義。比如說基類的虛函數版本是對基類的數據進行操作, 派生類的虛函數版本是對派生類的虛函數進行操作。

然而,我們不要忽略一點,假設基類可以調用虛函數的派生類實現版本,那麼我們操控的數據則是派生類所有的數據, 此時,派生類的構造函數還沒有調用,派生類的數據成員也不會有相應的初始化,這時候對派生類的數據成員操作完全是無意義的,從程序崩潰到機器冒煙都有可能。

對於析構函數, 與構造函數是幾乎一樣的,不能調用虛函數。

從上面的分析可以得出兩點結論:
1. 構造函數或者析構函數調用虛函數並不會發揮虛函數動態綁定的特性,跟普通函數沒區別。
2. 即使構造函數或者析構函數如果能成功調用虛函數, 程序的運行結果也是不可控的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章