1. 程式人生 > >C++建構函式中呼叫虛擬函式是否有多型的效果

C++建構函式中呼叫虛擬函式是否有多型的效果

C++多型的一個重要應用就是虛擬函式。但是當我們再基類的建構函式中呼叫一個子類過載的虛擬函式會出現多型的效果嗎?我們具體看一下下面的例項:

#include <iostream>

#define P(x) std::cout<<x<<std::endl;

class A
{
public:
	A(){ func(); }
	~A(){}

	virtual void func(){ P("A func call"); }
	int a = 10;
};

class B : public A
{
public:
	B(){ func(); }
	~B(){}
	
	virtual void func(){ P("B func call"); }
	int b = 20;
};

void main()
{
	B* b = new B();
}

這個例子中子類B過載了基類A的func這個虛擬函式,但是在A的建構函式中我們呼叫了這個虛擬函式,當我們建立B的物件時候,A的建構函式中的這個func的呼叫會去查虛擬函式表嗎?此時的輸出會是什麼呢。為了一探究竟,我們反彙編看一下A和B的建構函式。

A建構函式的反彙編

009B3770  push        ebp  
009B3771  mov         ebp,esp  
009B3773  sub         esp,0CCh  
009B3779  push        ebx  
009B377A  push        esi  
009B377B  push        edi  
009B377C  push        ecx  
009B377D  lea         edi,[ebp-0CCh]  
009B3783  mov         ecx,33h  
009B3788  mov         eax,0CCCCCCCCh  
009B378D  rep stos    dword ptr es:[edi]  
009B378F  pop         ecx  
009B3790  mov         dword ptr [this],ecx  
009B3793  mov         eax,dword ptr [this]  
009B3796  mov         dword ptr [eax],9BDA58h  
009B379C  mov         eax,dword ptr [this]  
009B379F  mov         dword ptr [eax+4],0Ah  
009B37A6  mov         ecx,dword ptr [this]  
009B37A9  call        A::func (09B104Bh)  
009B37AE  mov         eax,dword ptr [this]  
009B37B1  pop         edi  
009B37B2  pop         esi  
009B37B3  pop         ebx  
009B37B4  add         esp,0CCh  
009B37BA  cmp         ebp,esp  
009B37BC  call        __RTC_CheckEsp (09B134Dh)  
009B37C1  mov         esp,ebp  
009B37C3  pop         ebp  
009B37C4  ret  

具體的函式呼叫的時候的過程我們再花一篇來專門寫。這裡我們只關注本問題的答案。首先看下面兩個指令

009B3793  mov         eax,dword ptr [this]  
009B3796  mov         dword ptr [eax],9BDA58h  

需要一點彙編知識,VS的反彙編還是比較容易理解的。[this]指向的當前物件的首地址,跟我們程式碼中用到的this有點類似,第二句是將9BDA58h 這個地址賦值給暫存器eax指向的記憶體區。我們知道此時eax裡面存的是物件的首地址。所以我們很容易知道這兩個指令是將虛擬函式表的指標賦值到物件的前面四個位元組。9BDA58h指向的就是虛擬函式表的地址。

我們接著往後面看,在A的建構函式中對func的呼叫,可以很直觀的發現並沒有去虛擬函式表裡面拿函式地址,而是顯式的呼叫A::func的(後面我們具體給一個去虛擬函式表拿函式地址的例子)。所以多型在這裡並沒有生效。其實退一萬步說,即使這時候去虛擬函式表裡面拿func的函式地址,也是A的func的地址,跟這裡直接呼叫時一致的。因為9BDA58h是A物件的虛擬函式表的地址,而不是B物件虛擬函式表地址。為什麼這麼說呢?我們接著看B的建構函式反彙編

009B8CC0  push        ebp  
009B8CC1  mov         ebp,esp  
009B8CC3  push        0FFFFFFFFh  
009B8CC5  push        9BA148h  
009B8CCA  mov         eax,dword ptr fs:[00000000h]  
009B8CD0  push        eax  
009B8CD1  sub         esp,0CCh  
009B8CD7  push        ebx  
009B8CD8  push        esi  
009B8CD9  push        edi  
009B8CDA  push        ecx  
009B8CDB  lea         edi,[ebp-0D8h]  
009B8CE1  mov         ecx,33h  
009B8CE6  mov         eax,0CCCCCCCCh  
009B8CEB  rep stos    dword ptr es:[edi]  
009B8CED  pop         ecx  
009B8CEE  mov         eax,dword ptr ds:[009C0000h]  
009B8CF3  xor         eax,ebp  
009B8CF5  push        eax  
009B8CF6  lea         eax,[ebp-0Ch]  
009B8CF9  mov         dword ptr fs:[00000000h],eax  
009B8CFF  mov         dword ptr [this],ecx  
009B8D02  mov         ecx,dword ptr [this]  
009B8D05  call        A::A (09B122Bh)  
009B8D0A  mov         dword ptr [ebp-4],0  
009B8D11  mov         eax,dword ptr [this]  
009B8D14  mov         dword ptr [eax],9BDA78h  
009B8D1A  mov         eax,dword ptr [this]  
009B8D1D  mov         dword ptr [eax+8],14h  
009B8D24  mov         ecx,dword ptr [this]  
009B8D27  call        B::func (09B1307h)  
009B8D2C  mov         dword ptr [ebp-4],0FFFFFFFFh  
009B8D33  mov         eax,dword ptr [this]  
009B8D36  mov         ecx,dword ptr [ebp-0Ch]  
009B8D39  mov         dword ptr fs:[0],ecx  
009B8D40  pop         ecx  
009B8D41  pop         edi  
009B8D42  pop         esi  
009B8D43  pop         ebx  
009B8D44  add         esp,0D8h  
009B8D4A  cmp         ebp,esp  
009B8D4C  call        __RTC_CheckEsp (09B134Dh)  
009B8D51  mov         esp,ebp  
009B8D53  pop         ebp  
009B8D54  ret 

我們這裡只提取出我們需要的關鍵指令,有興趣的可以把整個反彙編全部理解一下,其實不難

009B8D05  call        A::A (09B122Bh)  
009B8D0A  mov         dword ptr [ebp-4],0  
009B8D11  mov         eax,dword ptr [this]  
009B8D14  mov         dword ptr [eax],9BDA78h  
009B8D1A  mov         eax,dword ptr [this]  
009B8D1D  mov         dword ptr [eax+8],14h  
009B8D24  mov         ecx,dword ptr [this]  
009B8D27  call        B::func (09B1307h)  

這一段就足以說明問題,顯示呼叫了A::A的建構函式,然後再將B的虛擬函式表的地址賦值給前四個位元組。注意這時候是在基類A的建構函式之後呼叫的,所以9BDA78h這個地址會沖掉在A的建構函式賦值的A類的虛擬函式表的地址。使得這個時候物件的虛擬函式表地址指向的是B類的虛擬函式表。這就是上面我們所說的,即使A在建構函式中去虛擬函式表取func這個虛擬函式的地址,取到的也是A物件自身的func函式地址,因為這時候拿不到B類的虛擬函式表的地址。在B的建構函式中對func的呼叫也是直接呼叫,也沒有通過虛擬函式表去拿地址。

所以很明顯的知道整個程式的輸出是

A func call
B func call

 

補充說一下虛擬函式的多型在彙編中的體現。我們上面的例子稍微修改了一下:

#include <iostream>

#define P(x) std::cout<<x<<std::endl;

class A
{
public:
	A(){  }
	~A(){}

	virtual void func1(){ P("A func1 call"); }
	virtual void func2(){ P("A func2 call"); }
	int a = 10;
};

class B : public A
{
public:
	B(){  }
	~B(){}
	
	virtual void func1(){ P("B func1 call"); }
	virtual void func2(){ P("B func2 call"); }
	int b = 20;
};

void main()
{
	A* b = new B();
	b->func1();
	b->func2();
}

我們看一下main函式的關鍵的彙編程式碼

001D50B6  mov         eax,dword ptr [b]      
001D50B9  mov         edx,dword ptr [eax]  //[eax]取物件前四個位元組的內容也就是將虛擬函式表的地址    
                                           //賦給edx暫存器
001D50BB  mov         esi,esp  
001D50BD  mov         ecx,dword ptr [b]  
001D50C0  mov         eax,dword ptr [edx]  //[edx]取的虛擬函式表的第一個元素,也就是B::func1的地 
                                           //址 
001D50C2  call        eax         //b->func1()
001D50C4  cmp         esi,esp  
001D50C6  call        __RTC_CheckEsp (01D134Dh)  
001D50CB  mov         eax,dword ptr [b]  
001D50CE  mov         edx,dword ptr [eax]  
001D50D0  mov         esi,esp  
001D50D2  mov         ecx,dword ptr [b]  
001D50D5  mov         eax,dword ptr [edx+4]  //虛擬函式表地址偏移四個位元組(32位程式),也就是下一            
                                             //個虛擬函式的地址即B::func2的地址
001D50D8  call        eax         //b->func2()
001D50DA  cmp         esi,esp  
001D50DC  call        __RTC_CheckEsp (01D134Dh)  

這段程式碼也很直觀。[b]指的是變數b的值,也就是物件的地址。[暫存器]指的是取以暫存器中的內容為地址的記憶體區域的值。具體的說明已經加到上面的上面的彙編程式碼中。

我們知道虛擬函式的多型只針對指標和引用的物件,也就是說只要通過類物件的指標或者引用去呼叫虛擬函式,都會去虛擬函式表中查詢,這時候才有多型的效果。純物件呼叫虛擬函式,是直接的函式呼叫,所以不會產生多型。將上面的mian函式稍作修改

void main()
{
	A b = B();
	b.func1();
	b.func2();
}

我們看看這時候的反彙編

002850C2  lea         ecx,[b]  
002850C5  call        A::func1 (0281505h)  
002850CA  lea         ecx,[b]  
002850CD  call        A::func2 (02814FBh)  

這時候是直接呼叫當前物件的方法。並沒有去查虛擬函式表。

 

另外一篇有關單繼承,多重繼承的記憶體佈局,跟這裡講的關係比較密切,大家也可以看一下

C++ 單繼承 多重繼承的記憶體佈局