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++ 单继承 多重继承的内存布局

猜你喜欢

转载自blog.csdn.net/fm_VAE/article/details/82977563