C++庖丁解牛:深入理解多态:从虚函数表到底层实现
深入理解多态:从虚函数表到底层实现
递归何不归:个人主页
个人专栏: 《C++庖丁解牛》《数据结构详解》
在广袤的空间和无限的时间中,能与你共享同一颗行星和同一段时光,是我莫大的荣幸
一、多态的概念
1.1 编译时多态
编译时多态主要就是函数重载和函数模版,他们实现多态主要是通过传不同类型的参数,之所以称为编译时多态,是因为参数的匹配是发生在编译阶段的
这种的也被称为静态
1.2 运行时多态
运行时多态,可以认为是通过对象来区分行为的,而编译时多态是通过类型的。
就好像是同样是“叫”这个行为,传猫过去就是“喵喵”,传狗过去就是“汪汪”
二、多态的定义和使用
2.1 多态的判定标准
1、要是用基类的引用或者是引用来调用
2、被调用得函数是虚函数
3、调用函数需要构成重写
我们这里着重讲一下为什么一定要用基类的指针或者是引用调用:
因为多态的内核就是“在运行时确定",假如使用的是基类的对象,那么在编译的时候对象的类型就是明确的基类类型,这时候就起不到运行时确定的效果了
假设是使用指针或者是引用来调用,在运行时是不知道具体指向的类型的,这时候就需要去查找虚函数表,就可以实现“运行时确认”
2.1.1 虚函数
在函数声明之前加上virtual关键字,这个函数就是虚函数
virtualvoidfunc()2.1.2 函数重写
函数重写需要构成以下条件:
1、两个函数的函数名、参数、返回值全部相等
2、两个函数全都是虚函数
补充:派生类中的函数不是虚函数也是可以的,但是基类中的函数一定是虚函数,此时相当于是将基类中函数的virtual关键字继承下来了,此时重写后的函数相当于是将基类的函数名和参数和派生类的函数实现拼在一起了
2.2 相关的题目
以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
classA{public:virtualvoidfunc(int val =1){ std::cout <<"A->"<< val << std::endl;}virtualvoidtest(){func();}};classB:publicA{public:voidfunc(int val =0){ std::cout <<"B->"<< val << std::endl;}};intmain(int argc,char* argv[]){ B* p =new B; p->test();return0;}在main函数中创建了一个B类型的对象,并将它的指针赋值给了B*类型的指针p
然后使用p来调用test函数,这实际上调用的是B中继承自A的部分中的test函数,但是在调用过程中传递给test函数中this指针的是B类型的指针
根据多态的对象决定论,实际上访问的func函数是派生类B重写后的func函数
但是派生类的func函数并没有声明是虚函数,所以实际上重写函数是由基类中的函数的声明和派生类的实现组成的,相当于是:
virtualvoidfunc(int val =1){ std::cout <<"B->"<< val << std::endl;}此时运行结果是B->1,选择B
2.3 析构函数的重写
在继承体系中基类的指针是可以指向派生类的对象的,这时候传统的析构就出现问题了
classA{public:virtual~A(){ cout <<"~A()"<< endl;}};classB:publicA{public:~B(){ cout <<"~B()->delete:"<< _p << endl;delete _p;}protected:int* _p =newint[10];};// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。intmain(){ A* p1 =new A; A* p2 =new B;delete p1;delete p2;return0;}这时候应该怎么析构,我们显然是想将指针指向的对象全部析构掉,但是碍于指针是A基类类型的,如果不加以处理,将只会调用基类的析构函数,派生类并没有被有效析构,这显然是不符合我们的预期的。
这时候我们将析构函数置为虚函数,派生类和基类中的析构函数就构成了函数重写,此时再delete基类的指针,调用的就是派生类的析构函数,就可以将空间释放干净。

可以看到:析构p2指向空间的时候调用了B的析构函数
2.4 override 和 final关键字
在继承体系中重写的判断条件是比较苛刻的,两个函数只要是出现一点不一样就会达不到重写的条件,这时候还不一定会报错,这是比较危险的(可能会实现不了预期效果),这时候就引入了override关键字和final关键字,override关键字用来检查是否构成重写,final关键字用来声明这个函数不能被重写
示例:
classA{public:virtual~A(){ cout <<"~A()"<< endl;}virtualvoidfunc1(){}virtualvoidfunc2()final{}};classB:publicA{public:virtual~B(){ cout <<"~B()->delete:"<< _p << endl;delete _p;}virtualvoidfunc1(size_t i)override{}virtualvoidfunc2(){}protected:int* _p =newint[10];};
如图所示:会直接编译报错
2.5 纯虚函数和抽象类
在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现)
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。
2.6 协变(不重要,了解即可)
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引
⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。
classPerson{public:virtual A*BuyTicket(){ cout <<"买票-全价"<< endl;returnnullptr;}};classStudent:publicPerson{public:virtual B*BuyTicket(){ cout <<"买票-打折"<< endl;returnnullptr;}};三、多态的底层实现
3.1 虚函数表简介
在以上情景中的类的内存中,都存在一个** _vfptr指针**,这就是虚函数表,,也叫虚表,指向了类中的虚函数

3.2 多态的实现
在满足多态后,要调用哪个函数并不是在编译时就通过指向对象确定的,而是需要在运行时到指向虚函数地址的虚函数表内确定虚函数的地址然后再调用,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数

classPerson{public:virtual A*BuyTicket(){ cout <<"买票-全价"<< endl;returnnullptr;}};classStudent:publicPerson{public:virtual B*BuyTicket(){ cout <<"买票-打折"<< endl;returnnullptr;}};voidFunc(Person* ptr){ ptr->BuyTicket();}intmain(){ Person ps; Student st;Func(&ps);Func(&st);return0;}3.3 静态绑定和动态绑定
• 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定,函数重载和函数模版都是静态绑定。
• 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
3.4 虚函数表详解
3.4.1 虚函数表的相关规则
1、同类型的对象的虚函数表是同一个(不直接将虚函数的函数指针存在对象中而是存在一个指针数组中就是为了节省空间)
2、派生类先回将基类的虚函数表继承下来(但是是拷贝,指向的不是同一块空间)
3、构成了重写的函数会将原来的函数指针覆盖掉
4、派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分。
5、虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)
还有需要注意的是:
当虚函数继承了多个基类时,新增的虚函数,统一放在第一个基类的虚函数表中。
3.4.2 虚函数存放的位置问题(这里有坑)
大部分人看到这个问题可能想到的是:虚函数明显是存在虚函数里的
实则不然,虚函数也是函数,是存在代码段中的,只不过是也在虚函数表中存了一份
3.4.3 虚函数表存放的位置
这个C++委员会并未明确规定存放位置,但是可以写程序验证
classBase{public:virtualvoidfunc1(){ cout <<"Base::func1"<< endl;}virtualvoidfunc2(){ cout <<"Base::func2"<< endl;}voidfunc5(){ cout <<"Base::func5"<< endl;}protected:int a =1;};classDerive:publicBase{public:// 重写基类的func1virtualvoidfunc1(){ cout <<"Derive::func1"<< endl;}virtualvoidfunc3(){ cout <<"Derive::func1"<< endl;}voidfunc4(){ cout <<"Derive::func4"<< endl;}protected:int b =2;};intmain(){int i =0;staticint j =1;int* p1 =newint;constchar* p2 ="xxxxxxxx";printf("栈:%p\n",&i);printf("静态区:%p\n",&j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2); Base b; Derive d; Base* p3 =&b; Derive* p4 =&d;printf("Person虚表地址:%p\n",*(int*)p3);printf("Student虚表地址:%p\n",*(int*)p4);printf("虚函数地址:%p\n",&Base::func1);printf("普通函数地址:%p\n",&Base::func5);return0;}验证得:大概是在常量区