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)
這時候是直接呼叫當前物件的方法。並沒有去查虛擬函式表。
另外一篇有關單繼承,多重繼承的記憶體佈局,跟這裡講的關係比較密切,大家也可以看一下