C++不要在構造函數和析構函數中調用虛函數

這裏先運行個示例代碼:

#include<iostream>  
using namespace std;
class Base {
public:
    Base() {
        cout << "Base::Base()" << endl;
        fun();
        //fun_();//編譯錯誤  
    }
    virtual void fun() {
        cout << "Base::fun()" << endl;
    }
    virtual void fun_() = 0;
    ~Base() {
        cout << "Base::~Base()" << endl;
        fun();
    }
};
class Derived :public Base {
public:
    Derived() {
        cout << "Derived::Derived()" << endl;
        fun();
    }
    virtual void fun() {
        cout << "Derived::fun()" << endl;
    }
    virtual void fun_() {
        cout << "Derived::fun_()" << endl;
    }
    ~Derived() {
        cout << "Derived::~Derived()" << endl;
        fun();
    }
};
int main() {
    Derived* d = new Derived();
    delete d;
    system("pause");
    return 0;
}

輸出結果:

Base::Base()    //基類構造
Base::fun()
Derived::Derived()  //子類構造
Derived::fun()
Derived::~Derived()    //子類析構
Derived::fun()
Base::~Base()  //基類析構
Base::fun()

在建立一個對象時,會依次調用父構造函數->子構造函數,同樣,在析構時也會逆過來調用子析構函數->父析構函數。那麼在調用的時候,如果析構函數中對於虛函數還執行虛機制,就有可能已經執行過一個子對象的析構函數,又去調用子對象的函數,這樣會很危險。所以在虛析構函數中,對於虛函數,只會執行目前最外一級的那個函數。

雖然可以對虛函數進行實調用,但程序員編寫虛函數的本意應該是實現動態聯編。在構造函數中調用虛函數,函數的入口地址是在編譯時靜態確定的,並未實現虛調用。但是爲什麼在構造函數中調用虛函數,實際上沒有發生動態聯編呢?

1. 不要在構造函數中調用虛函數的原因

第一個原因,在概念上,構造函數的工作是爲對象進行初始化。在構造函數完成之前,被構造的對象被認爲“未完全生成”。當創建某個派生類的對象時,如果在它的基類的構造函數中調用虛函數,那麼此時派生類的構造函數並未執行,所調用的函數可能操作還沒有被初始化的成員,浙江導致災難的發生。

第二個原因,即使想在構造函數中實現動態聯編,在實現上也會遇到困難。這涉及到對象虛指針(vptr)的建立問題。在Visual C++中,包含虛函數的類對象的虛指針被安排在對象的起始地址處,並且虛函數表(vtable)的地址是由構造函數寫入虛指針的。所以,一個類的構造函數在執行時,並不能保證該函數所能訪問到的虛指針就是當前被構造對象最後所擁有的虛指針,因爲後面派生類的構造函數會對當前被構造對象的虛指針進行重寫,因此無法完成動態聯編。

2.不要在析構函數中調用虛函數的原因

同樣的,在析構函數中調用虛函數,函數的入口地址也是在編譯時靜態決定的。也就是說,實現的是實調用而非虛調用。
考察如下例子。

#include <iostream>
using namespace std;

class A {
public:
    virtual void show() {
        cout << "in A" << endl;
    }
    virtual ~A() { show(); }
};

class B :public A {
public:
    void show() {
        cout << "in B" << endl;
    }
};

int main() {
    A* a = new A();
    B* b = new B();

    delete a;
    delete b;
    system("pause");
}

程序輸出結果是:
in A
in A

在類B的對象b退出作用域時,會先調用類B的析構函數,然後調用類A的析構函數,在析構函數~A()中,調用了虛函數show()。從輸出結果來看,類A的析構函數對show()調用並沒有發生虛調用。

從概念上說,析構函數是用來銷燬一個對象的,在銷燬一個對象時,先調用該對象所屬類的析構函數,然後再調用其基類的析構函數,所以,在調用基類的析構函數時,派生類對象的“善後”工作已經完成了,這個時候再調用在派生類中定義的函數版本已經沒有意義了。

因此,一般情況下,應該避免在構造函數和析構函數中調用虛函數,如果一定要這樣做,程序猿必須清楚,這是對虛函數的調用其實是實調用。

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