雖然可以對虛函數進行實調用,但程序員編寫虛函數的本意應該是實現動態聯編。在構造函數中調用虛函數,函數的入口地址是在編譯時靜態確定的,並未實現虛調用。但是為什么在構造函數中調用虛函數,實際上沒有發生動態聯編呢?
第一個原因,在概念上,構造函數的工作是為對象進行初始化。在構造函數完成之前,被構造的對象被認為“未完全生成”。當創建某個派生類的對象時,如果在它的基類的構造函數中調用虛函數,那么此時派生類的構造函數並未執行,所調用的函數可能操作還沒有被初始化的成員,將導致災難的發生。
第二個原因,即使想在構造函數中實現動態聯編,在實現上也會遇到困難。這涉及到對象虛指針(vptr)的建立問題。在Visual C++中,包含虛函數的類對象的虛指針被安排在對象的起始地址處,並且虛函數表(vtable)的地址是由構造函數寫入虛指針的。所以,一個類的構造函數在執行時,並不能保證該函數所能訪問到的虛指針就是當前被構造對象最后所擁有的虛指針,因為后面派生類的構造函數會對當前被構造對象的虛指針進行重寫,因此無法完成動態聯編。
同樣的,在析構函數中調用虛函數,函數的入口地址也是在編譯時靜態決定的。也就是說,實現的是實調用而非虛調用。
考察如下例子:
#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;
B b;
}
程序輸出結果是:
in A
in A
在類B的對象b退出作用域時,會先調用類B的析構函數,然后調用類A的析構函數,在析構函數~A()中,調用了虛函數show()。從輸出結果來看,類A的析構函數對show()調用並沒有發生虛調用。
從概念上說,析構函數是用來銷毀一個對象的,在銷毀一個對象時,先調用該對象所屬類的析構函數,然后再調用其基類的析構函數,所以,在調用基類的析構函數時,派生類對象的“善后”工作已經完成了,這個時候再調用在派生類中定義的函數版本已經沒有意義了。
因此,一般情況下,應該避免在構造函數和析構函數中調用虛函數,如果一定要這樣做,必須清楚,對虛函數的調用其實是實調用。