C++多态:深入虚函数,理解晚绑定
2017-05-26 09:34
645 查看
C++的多态特性是通过晚绑定实现的。晚绑定(late binding),指的是编译器或解释器程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需要检查对象是否支持特性和方法即可。
在C++中,晚绑定通常发生在使用
考虑如下的代码:
输出结果为:
使用VS命令/d1 reportSingleClassLayoutD和/d1 reportSingleClassLayoutE,可以得到类D和类E的内存布局。可以看到,D的大小是8个字节,头四个字节存储指向虚函数表的指针vfptr,后四个字节存储成员变量num。E的大小也是8个字节,头四个字节存储指向虚函数表的指针,后四个字节存储从基类继承的成员变量num。
内存布局图:
接下来从汇编角度解释一下晚绑定是怎么发生的。
多态配合泛型算法简化编程,见我的另一篇博文:http://blog.csdn.net/popvip44/article/details/72674326
在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
相关文章推荐
- 【转】深入理解C++的动态绑定和静态绑定 & 不要重定义虚函数中的默认参数
- 深入理解C++对象模型-成员函数的本质以及虚函数的实现(非虚继承)
- 深入理解C++的动态绑定和静态绑定
- c/C++ 函数参数返回值的深入理解
- 深入理解C++的动态绑定和静态绑定
- 【原创翻译】深入理解javascript事件处理函数绑定三部曲(三)——高级事件函数绑定模型
- 深入理解C++的动态绑定和静态绑定
- c++多重继承和虚继承及虚函数深入理解
- 深入理解C++的动态绑定和静态绑定
- 深入理解C++的动态绑定和静态绑定
- 用例子来理解c++的类,构造函数,类的多态,成员函数,成员变量
- 深入理解C++的动态绑定和静态绑定
- 深入理解C++的动态绑定和静态绑定
- 【原创翻译】深入理解javascript事件处理函数绑定三部曲(一)——早期的事件处理函数
- 深入剖析C++继承,多态以及隐藏(一)。(虚函数探究)
- 深入理解C++的动态绑定和静态绑定转
- 深入理解C++的动态绑定和静态绑定
- 深入理解C++的动态绑定和静态绑定
- 深入理解C++的多态
- 深入理解javascript事件处理函数绑定三部曲(二)——传统处理函数绑定模型