C++中建構函式能呼叫虛擬函式嗎?
環境:XPSP3 VS2005
今天黑總給應聘者出了一個在C++的建構函式中呼叫虛擬函式的問題,具體的題目要比標題複雜,大體情況可以看如下的程式碼:
class Base { public: Base() { Fuction(); } virtual void Fuction() { cout << "Base::Fuction" << endl; } }; class A : public Base { public: A() { Fuction(); } virtual void Fuction() { cout << "A::Fuction" << endl; } }; // 這樣定義一個A的物件,會輸出什麼? A a;
首先回答標題的問題,呼叫當然是沒有問題的,但是獲得的是你想要的結果嗎?或者說你想要什麼樣的結果?
有人說會輸出:
A::Fuction
A::Fuction
如果是這樣,首先我們回顧下C++物件模型裡面的構造順序,在構造一個子類物件的時候,首先會構造它的基類,如果有多層繼承關係,實際上會從最頂層的基類逐層往下構造(虛繼承、多重繼承這裡不討論),如果是按照上面的情形進行輸出的話,那就是說在構造Base的時候,也就是在Base的建構函式中呼叫Fuction的時候,呼叫了子類A的Fuction,而實際上A還沒有開始構造,這樣函式的行為就是完全不可預測的,因此顯然不是這樣,實際的輸出結果是:
Base::Fuction
A::Fuction
據說在JAVA中是上一種輸出(感覺有點匪夷所思)。
我們來單步看一下到底發生了什麼?在A的建構函式裡面首先會去呼叫Base的建構函式,Base的建構函式如下:
class Base
{
public:
Base()
00411600 push ebp
00411601 mov ebp,esp
00411603 sub esp,0CCh
00411609 push ebx
0041160A push esi
0041160B push edi
0041160C push ecx
0041160D lea edi,[ebp-0CCh]
00411613 mov ecx,33h
00411618 mov eax,0CCCCCCCCh
0041161D rep stos dword ptr es:[edi]
0041161F pop ecx
00411620 mov dword ptr [ebp-8],ecx
00411623 mov eax,dword ptr [this]
00411626 mov dword ptr [eax],offset Base::`vftable' (41770Ch)
Fuction();
0041162C mov ecx,dword ptr [this]
0041162F call Base::Fuction (4111A9h)
}
00411634 mov eax,dword ptr [this]
00411637 pop edi
00411638 pop esi
00411639 pop ebx
0041163A add esp,0CCh
00411640 cmp ebp,esp
00411642 call @ILT+460(__RTC_CheckEsp) (4111D1h)
00411647 mov esp,ebp
00411649 pop ebp
0041164A ret
從單步跟蹤來看,注意黑色加粗的那部分彙編程式碼,ecx中存放的是物件的地址(0x0012ff60,我的機器上的情況看下圖,有圖有真相),首先是設定vtable的地址到物件的前四個位元組(不同的編譯器可能不同),然後就直接呼叫了Base::Fuction函式,並沒有走虛機制,而我們此時看虛表中的狀態,虛表已經填充的是0x4111a9,注意虛表的地址0x0041770c,而此時物件地址0x0012FF60前四個位元組存放的正是0x0041770c。
繼續跟蹤,流程又回到A的建構函式中,再次注意加粗部分的程式碼,從基類Base的建構函式返回後,在A的建構函式中,重設了虛表指標,現在的虛表指標是(0x417700h),同樣呼叫Fuction的時候直接呼叫了A::Fuction函式,並沒有使用虛機制,而且此時虛表0x417700h指向的位置存放的0x41110e正是A::Fuction的地址。
class A : public Base
{
public:
A()
00411590 push ebp
00411591 mov ebp,esp
00411593 sub esp,0CCh
00411599 push ebx
0041159A push esi
0041159B push edi
0041159C push ecx
0041159D lea edi,[ebp-0CCh]
004115A3 mov ecx,33h
004115A8 mov eax,0CCCCCCCCh
004115AD rep stos dword ptr es:[edi]
004115AF pop ecx
004115B0 mov dword ptr [ebp-8],ecx
004115B3 mov ecx,dword ptr [this]
004115B6 call Base::Base (411140h)
004115BB mov eax,dword ptr [this]
004115BE mov dword ptr [eax],offset A::`vftable' (417700h)
{
Fuction();
004115C4 mov ecx,dword ptr [this]
004115C7 call A::Fuction (41110Eh)
}
004115CC mov eax,dword ptr [this]
004115CF pop edi
004115D0 pop esi
004115D1 pop ebx
004115D2 add esp,0CCh
004115D8 cmp ebp,esp
004115DA call @ILT+460(__RTC_CheckEsp) (4111D1h)
004115DF mov esp,ebp
004115E1 pop ebp
004115E2 ret
其實事情就是這麼簡單。