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
来源:CSDN
作者:silenci
链接:https://blog.csdn.net/popvip44/article/details/72763004