【C++】多态(下)
多态
四、多态的原理
1、虚表的存储位置
classA{public:virtualvoidfunc1(){ cout <<"A::func1"<< endl;}virtualvoidfunc2(){ cout <<"A::func2"<< endl;}private:int _a;};voidfunc(){ cout <<"void func()"<< endl;}intmain(){ A a1; A a2;staticint a =0;int b =0;int* p1 =newint;constchar* p2 ="hello world";printf("静态区:%p\n",&a);printf("栈:%p\n",&b);printf("堆:%p\n", p1);printf("代码段:%p\n", p2);printf("虚表:%p\n",*((int*)&a1));printf("虚函数地址:%p\n",&A::func1);printf("普通函数地址:%p\n", func);return0;}
被static修饰的变量a存放在静态区,局部变量b存储在栈区,指针p1指向在堆上开辟出的对象,常量字符串的指针存放在代码段
虚表这里,因为a1是一个类对象,它的地址存放了虚表指针和内置类型_a两部分,虚表指针是一个void类型的指针,占4个字节,把它强制转换成int*类型的指针,再解引用,得到的是虚表的指针
虚函数地址就是固定用法,要把是哪个类的虚函数标注出来,然后用取地址符号
从上图我们可以观察到,虚函数和普通函数存放位置接近,代码段和虚表存放位置接近,而虚表和虚函数相对于静态区栈区以及堆区来说还是离代码段更近近,也就是说,虚函数和普通函数以及虚表存放在代码段
2、多态的原理
classA{public:virtualvoidD(){ cout <<"A : virtual void D()"<< endl;}};classB:publicA{public:virtualvoidD(){ cout <<"B : virtual void D()"<< endl;}};voidfunc(A& ra){ ra.D();}voidtest(){ A a; B b;func(a);func(b);}

当ra为A对象时,函数调用时在A的虚表中找到func,当ra为B对象时,函数调用时在B的虚表中找到func,然后调用,这样就实现出了不同对象去完成同一行为时,展现出不同的形态
我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数
classA{public:virtualvoidD(){ cout <<"A : virtual void D()"<< endl;}};classB:publicA{public:virtualvoidD(){ cout <<"B : virtual void D()"<< endl;}};voidfunc(A* p){ p->D();}voidtest(){ A a;func(&a); a.D();}

对于多态调用:
p中存的是A对象的指针,将p移动到eax中:
00382571 mov eax,dword ptr [p]
[eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx:
00382574 mov edx,dword ptr [eax]
[edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax:
0038257B mov eax,dword ptr [edx]
call eax中存虚函数的指针,这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的
对于普通调用:
因为不满足多态调用所以是普通函数调用,直接call地址
3、动态绑定和静态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
五、单继承和多继承关系的虚函数表
1、单继承中的虚函数表
classA{public:virtualvoidfunc1(){ cout <<"A::func1"<< endl;}virtualvoidfunc2(){ cout <<"A::func2"<< endl;}private:int _a;};classB:publicA{public:virtualvoidfunc1(){ cout <<"B::func1"<< endl;}virtualvoidfunc3(){ cout <<"B::func3"<< endl;}virtualvoidfunc4(){ cout <<"B::func4"<< endl;}private:int _b;};intmain(){ A a; B b;return0;}
图中的监视窗口中我们发现看不见func3和func4,这里是编译器的监视窗口故意隐藏了这两个函数
那我们如何查看整个b的虚表呢
typedefvoid(*VFPTR)();voidPrintVTable(VFPTR vTable[]){// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数 cout <<"虚表地址>"<< vTable << endl;for(int i =0; vTable[i]!=nullptr;++i){printf("第%d个虚函数地址 :0X%x,->", i, vTable[i]); VFPTR f = vTable[i];f();} cout << endl;}intmain(){ A a; B b; VFPTR * vTablea =(VFPTR*)(*(int*)&a);PrintVTable(vTablea); VFPTR* vTableb =(VFPTR*)(*(int*)&b);PrintVTable(vTableb);return0;}PrintVTable函数:
核心点就是这个函数指针VFPTR,这是一个函数指针,指向的类型是void*,参数为(),也就是无参,也就是说这个指针可以指向任意一个返回类型为void*并且无参的函数
PrintVTable函数的参数也可以写成VFPTR* vTable,虚表的地址就是指针vTable,后加[]就是对表中的指针进行访问,打印出它们的指针,并且将这些指针指向的函数调用表示出来,让我们可以看到这个地址对应的是哪个函数
main函数:
取出a、b对象的头4bytes,就是虚表的指针,虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
1.先取a的地址,强转成一个int*的指针
2.再解引用取值,就取到了a对象头4bytes的值,这个值就是指向虚表的指针
3.再强转成 VFPTR* ,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组
4.虚表指针传递给PrintVTable进行打印虚表
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题,我们只需要点重新生成解决方案就行
2、多继承中的虚函数表
classA1{public:virtualvoidfunc1(){ cout <<"A1::func1"<< endl;}virtualvoidfunc2(){ cout <<"A1::func2"<< endl;}private:int a1;};classA2{public:virtualvoidfunc1(){ cout <<"A2::func1"<< endl;}virtualvoidfunc2(){ cout <<"A2::func2"<< endl;}private:int a2;};classB:publicA1,publicA2{public:virtualvoidfunc1(){ cout <<"B::func1"<< endl;}virtualvoidfunc3(){ cout <<"B::func3"<< endl;}private:int b;};typedefvoid(*VFPTR)();voidPrintVTable(VFPTR vTable[]){ cout <<"虚表地址>"<< vTable << endl;for(int i =0; vTable[i]!=nullptr;++i){printf("第%d个虚函数地址 :0X%x,->", i, vTable[i]); VFPTR f = vTable[i];f();} cout << endl;}intmain(){ B b; VFPTR* vTablea1 =(VFPTR*)(*(int*)&b);PrintVTable(vTablea1); VFPTR* vTablea2 =(VFPTR*)(*(int*)((char*)&b +sizeof(A1)));PrintVTable(vTablea2);return0;}
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中,也就是func3函数,第一个继承基类就是最左边继承的这个基类

六、多态中的一些小tips
内联函数可以是虚函数,但是如果被inline修饰的函数是虚函数,那么inline特性将会消失,被修饰的函数相当于没被修饰
静态成员不可以是虚函数,因为静态成员没有this指针使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表
构造函数不能是虚函数,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的
最好把基类的析构函数定义为虚函数,因为如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数,这会导致派生类部分的对象没有被正确析构,可能会引发资源泄露
对象在访问虚函数与普通函数速度的对比,如果是普通对象访问,两者一样快,如果是多态对象访问(指针对象或者引用对象),则调用普通函数更快,因为虚函数构成多态,运行时需要到虚函数表中去查找
虚函数表在编译阶段就生成了
今日分享就到这里了~