C++中多态是通过虚函数实现,所以我们先了解虚函数。
虚函数
在函数前面加上virtual关键字后,函数变成虚函数。
如果通过对象调用,虚函数和普通函数没有区别。写代码验证,反汇编后代码都是E8 call ,调用方式是直接调用,没有区别。
class MyClazz
{
public:
void fun1() {
printf("fun1...\n");
}
virtual void fun2() {
printf("fun2...\n");
}
};
int main()
{
MyClazz myClazz;
myClazz.fun1();
myClazz.fun2();
}
反汇编代码:
18: MyClazz myClazz;
006319BF 8D 4D F4 lea ecx,[ebp-0Ch]
006319C2 E8 FF F8 FF FF call 006312C6
19: myClazz.fun1();
006319C7 8D 4D F4 lea ecx,[ebp-0Ch]
006319CA E8 D8 F9 FF FF call 006313A7
20: myClazz.fun2();
006319CF 8D 4D F4 lea ecx,[ebp-0Ch]
006319D2 E8 82 F7 FF FF call 00631159
如果通过指针来调用,调用虚函数时使用间接调用:
class MyClazz
{
public:
void fun1() {
printf("fun1...\n");
}
virtual void fun2() {
printf("fun2...\n");
}
};
int main()
{
MyClazz myClazz;
MyClazz* ptr = &myClazz;
ptr->fun1();
ptr->fun2();
}
18: MyClazz myClazz;
004919BF 8D 4D F4 lea ecx,[ebp-0Ch]
004919C2 E8 FF F8 FF FF call 004912C6
19: MyClazz* ptr = &myClazz;
004919C7 8D 45 F4 lea eax,[ebp-0Ch]
004919CA 89 45 E8 mov dword ptr [ebp-18h],eax
20: ptr->fun1();
004919CD 8B 4D E8 mov ecx,dword ptr [ebp-18h]
004919D0 E8 D2 F9 FF FF call 004913A7
21: ptr->fun2();
004919D5 8B 45 E8 mov eax,dword ptr [ebp-18h]
004919D8 8B 10 mov edx,dword ptr [eax]
004919DA 8B F4 mov esi,esp
004919DC 8B 4D E8 mov ecx,dword ptr [ebp-18h]
004919DF 8B 02 mov eax,dword ptr [edx]
004919E1 FF D0 call eax
类的大小
如果类中有虚函数的时候,对象大小会增加4个字节。多出来的4字节是一个地址,指向所有虚函数的地址,这个地址叫虚函数表,虚函数表中存的值是函数地址。
代码验证:
class MyClazz
{
public:
int x;
int y;
MyClazz() {
x = 1;
y = 2;
}
virtual void VirtualFun() {
printf("VirtualFun...\n");
}
};
int main()
{
MyClazz myClazz;
MyClazz* ptr = &myClazz;
ptr->VirtualFun();
}
查看ptr 数据,最前面4个字节0xd57b34 是多出来的虚函数表。
查看0xd57b34 虚函数表数据,虚函数表中存储的是函数地址。
程序执行的时候,编译器按照序号寻找虚函数,调用虚函数:
class MyClazz
{
public:
int x;
int y;
MyClazz() {
x = 1;
y = 2;
}
virtual void VirtualFun1() {
printf("VirtualFun1...\n");
}
virtual void VirtualFun2() {
printf("VirtualFun2...\n");
}
};
int main()
{
MyClazz myClazz;
MyClazz* ptr = &myClazz;
printf("虚函数表地址为:%x\n", *(int*)&myClazz);
typedef void(*pFunction)(void);
pFunction pFn;
pFn = (pFunction) * ((int*)(*(int*)&myClazz) + 0);
pFn();
pFunction pFn2;
pFn2 = (pFunction) * ((int*)(*(int*)&myClazz) + 1);
pFn2();
}
终端打印:
虚函数表地址为:eb7b34
VirtualFun1...
VirtualFun2...
总结虚函数特性:
1、虚函数表的数量是编译时候确定的。 2、单继承时,子类会生成1个虚函数表,子类会继承基类虚函数。如果函数原型相同,子类虚函数会覆盖基类虚函数。 3、多继承时会产生多个虚函数表。同样会覆盖基类虚函数。 4、多重继承,子类会继承所有基类虚函数,如果子类虚函数和基类虚函数原型相同,会覆盖基类虚函数。 5、如果有继承多个基类,子类会有多个虚函数表。
动态绑定
1、调用的代码和真正的函数关联到一起的过程叫绑定。
2、如果编译的时候能确定绑定,叫做编译期绑定,或者叫前期绑定。类的普通的成员和函数编译之后,地址已经确定,是编译器绑定。
3、如果编译的时候无法确定,叫做动态绑定,或者叫运行期绑定。虚函数在编译的时候无法确定。能够体现不同的行为,称为多态,动态绑定也叫多态。多态通过虚函数表来实现。
class Base
{
public:
int x;
public:
Base()
{
x = 1;
}
void Function_1()
{
printf("Base:Function_1...\n");
}
virtual void Function_2()
{
printf("Base:Function_2...virtual\n");
}
};
class Sub :public Base
{
public:
int x;
public:
Sub()
{
x = 2;
}
void Function_1()
{
printf("Sub:Function_1...\n");
}
virtual void Function_2()
{
printf("Sub:Function_2...virtual\n");
}
};
void TestBind(Base* pb)
{
int n = pb->x;
printf("%x\n", n);
pb->Function_1();
pb->Function_2();
}
int main(int argc, char* argv[])
{
Base base;
TestBind(&base);
Sub sub;
TestBind(&sub);
return 0;
}
|