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

来源:互联网 发布:淘宝运营讲师 编辑:程序博客网 时间:2024/05/22 14:46

 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=1I'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  | num1>      +---1>1>  D::$vftable@:1>      | &D_meta1>      |  01>   0  | &D::print1>  class E size(8):1>      +---1>   0  | +--- (base class D)1>   0  | | {vfptr}1>   4  | | num1>      | +---1>      +---1>1>  E::$vftable@:1>      | &E_meta1>      |  01>   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获取vfptr000C2854  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

原创粉丝点击