C++多态:深入虚函数,理解晚绑定

可紊 提交于 2019-12-03 21:59:11

 C++的多态特性是通过晚绑定实现的。晚绑定(late binding),指的是编译器或解释器程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需要检查对象是否支持特性和方法即可。
 在C++中,晚绑定通常发生在使用virtual声明成员函数时。此时,C++创建一个虚函数表,当某个函数被调用时需要从这个表中查找该函数的实际位置。通常,晚绑定也叫做动态函数分派(dynamic dispatch)。
 考虑如下的代码:

#include<iostream>
using namespace std;

class D {
public:
    int num;
    D(int i = 0) { num = i; }
    virtual void print() { cout << "I'm a D. my num=" << num << endl; };

};
class E :public D {
public:
    E(int i = 0) { num = i; }
    void print() { cout << "I'm a E. my num=" << num << endl; }
    void ppp() { int ttt = 1; }
};

int main()
{
    void (D::*i)() = &D::print;
    E* e = new E(1);
    e->print();
    (((D*)e)->*i)();
    delete e;
    return 0;
}

输出结果为:

I'm a E. my num=1
I'm a E. my num=1

使用VS命令/d1 reportSingleClassLayoutD和/d1 reportSingleClassLayoutE,可以得到类D和类E的内存布局。可以看到,D的大小是8个字节,头四个字节存储指向虚函数表的指针vfptr,后四个字节存储成员变量num。E的大小也是8个字节,头四个字节存储指向虚函数表的指针,后四个字节存储从基类继承的成员变量num。

1>  class D size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | num
1>      +---
1>
1>  D::$vftable@:
1>      | &D_meta
1>      |  0
1>   0  | &D::print


1>  class E size(8):
1>      +---
1>   0  | +--- (base class D)
1>   0  | | {vfptr}
1>   4  | | num
1>      | +---
1>      +---
1>
1>  E::$vftable@:
1>      | &E_meta
1>      |  0
1>   0  | &E::print

内存布局图:

这里写图片描述

接下来从汇编角度解释一下晚绑定是怎么发生的。

int main()
{
000C27B0  push        ebp  
000C27B1  mov         ebp,esp  
000C27B3  push        0FFFFFFFFh  
000C27B5  push        0C7242h  
000C27BA  mov         eax,dword ptr fs:[00000000h]  
000C27C0  push        eax  
000C27C1  sub         esp,100h  
000C27C7  push        ebx  
000C27C8  push        esi  
000C27C9  push        edi  
000C27CA  lea         edi,[ebp-10Ch]  
000C27D0  mov         ecx,40h  
000C27D5  mov         eax,0CCCCCCCCh  
000C27DA  rep stos    dword ptr es:[edi]  
000C27DC  mov         eax,dword ptr [__security_cookie (0CC004h)]  
000C27E1  xor         eax,ebp  
000C27E3  push        eax  
000C27E4  lea         eax,[ebp-0Ch]  
000C27E7  mov         dword ptr fs:[00000000h],eax  
    void (D::*i)() = &D::print;//vcall是虚函数表,vcall{0}就是虚函数D::print(),这里把D::print()偏移地址赋给ptr[i]
000C27ED  mov         dword ptr [i],offset D::`vcall'{0}' (0C146Fh)  
    E* e = new E(1);
000C27F4  push        8  
000C27F6  call        operator new (0C1311h)  
000C27FB  add         esp,4  
000C27FE  mov         dword ptr [ebp-0F8h],eax  
000C2804  mov         dword ptr [ebp-4],0  
000C280B  cmp         dword ptr [ebp-0F8h],0  
000C2812  je          main+79h (0C2829h)  
000C2814  push        1  
000C2816  mov         ecx,dword ptr [ebp-0F8h]  
000C281C  call        E::E (0C137Fh)  
000C2821  mov         dword ptr [ebp-10Ch],eax  
000C2827  jmp         main+83h (0C2833h)  
000C2829  mov         dword ptr [ebp-10Ch],0  
000C2833  mov         eax,dword ptr [ebp-10Ch]  
000C2839  mov         dword ptr [ebp-0ECh],eax  
000C283F  mov         dword ptr [ebp-4],0FFFFFFFFh  
000C2846  mov         ecx,dword ptr [ebp-0ECh]  
000C284C  mov         dword ptr [e],ecx  
    e->print();
000C284F  mov         eax,dword ptr [e]//e的指针赋给eax  
000C2852  mov         edx,dword ptr [eax]//打开e的指针,e中vfptr存在头四个字节,所以edx获取vfptr
000C2854  mov         esi,esp  
000C2856  mov         ecx,dword ptr [e]//成员函数调用是this->func(),这里this指针(也就是e)存入ecx    
000C2859  mov         eax,dword ptr [edx]//因为vcall{0}就是函数print(),所以这里直接把edx存储的指针,也就是vfptr,解引用之后赋值给eax调用就可以了 
    e->print();
000C285B  call        eax//调用eax指向的函数。由于这个过程是运行时确定的而不是编译时确定的,所以也叫动态函数分派,即晚绑定。(((D*)e)->*i)()更能体现动态性。
000C285D  cmp         esi,esp  
000C285F  call        __RTC_CheckEsp (0C1195h)  
    (((D*)e)->*i)();
000C2864  mov         esi,esp  
000C2866  mov         ecx,dword ptr [e]//成员函数调用是this->func(),这里this指针(也就是e)存入ecx  
000C2869  call        dword ptr [i]//打开指针i,获取偏移地址。此时基址变成了e所在的内存段,所以配合ecx中的指针e获取的是E::print(),而不是D::print()。因为E重写了D的print()。也可以不重写,那样的话调用的就是D::print(),读者可以自己验证。
000C286C  cmp         esi,esp  
000C286E  call        __RTC_CheckEsp (0C1195h)  
    delete e;
000C2873  mov         eax,dword ptr [e]  
000C2876  mov         dword ptr [ebp-104h],eax  
000C287C  push        8  
000C287E  mov         ecx,dword ptr [ebp-104h]  
000C2884  push        ecx  
000C2885  call        operator delete (0C105Ah)  
000C288A  add         esp,8  
000C288D  cmp         dword ptr [ebp-104h],0  
000C2894  jne         main+0F2h (0C28A2h)  
000C2896  mov         dword ptr [ebp-10Ch],0  
000C28A0  jmp         main+102h (0C28B2h)  
000C28A2  mov         dword ptr [e],8123h  
000C28A9  mov         edx,dword ptr [e]  
000C28AC  mov         dword ptr [ebp-10Ch],edx  
    return 0;
000C28B2  xor         eax,eax  
}

总结

 运行时多态通过多次对地址指针解引用,获得虚函数实体的地址,进而执行对应的虚函数。
多态配合泛型算法简化编程,见我的另一篇博文:http://blog.csdn.net/popvip44/article/details/72674326

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!