C++多态的一个重要应用就是虚函数。但是当我们再基类的构造函数中调用一个子类重载的虚函数会出现多态的效果吗?我们具体看一下下面的实例:
#include
#define P(x) std::cout<
这个例子中子类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
#define P(x) std::cout<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++ 单继承 多重继承的内存布局