您的位置:首页 > 编程语言 > C语言/C++

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

2017-05-26 09:34 645 查看
 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 <
4000
;< 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息