C++ --- 什么是多态
多态的概念
多态就是当要完成某个行为,当不同的对象去完成时会产生不同的效果。比如:在火车站买票,普通成年人,需要全价买票,学生可以半价买票,军人可以优先买票。
条件:被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写;必须通过基类的指针或者引用调用虚函数。
注意点是:如果基类的函数不是虚函数,重写函数只是构成隐藏。
如果不满足多态条件:
1.不重写虚函数
2.不是虚函数,重写
3.不用指针或者引用调用
重写
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类的虚函数的返回值类型,函数名字,参数列表完全相同),但是函数的实现不同,称派生类重写了基类的虚函数。
虚函数的重写,需要函数名,参数,返回值类型一样。但是派生类只是继承了函数的接口,接口就是函数名,参数,返回值类型。派生类重写只是实现不同。
析构函数的重写
问题:
所以析构函数需要定义成虚函数,来构成多态:
C++11里的override和final关键字
final:修饰虚函数,表示该虚函数不能被继承,不能进行重写。
修饰类,类不能被继承
在C++98中,为了不让类被继承,可以将基类的构造函数私有化(private),于是派生类就不能构造属于基类的成员。
override 检查派生类虚函数是否重写了基类的虚函数,如果没有则编译错误。
这个只能修饰派生类的虚函数,不能修饰基类虚函数。
override关键字最好用来检查派生类虚函数接口(函数名,参数,返回值)是否写错。
抽象类
在虚函数后面写上=0,这个函数称为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承之后也不能实例化出对象,只有重写的纯虚函数,派生类才能实例化出对象。
只有派生类经过纯虚函数重写才能实例化出对象。但是基类还是抽象类,不能实例化对象
纯虚函数的作用:
1.一定程度上强制了派生类对纯虚函数的重写,如果不重写,派生类就不能实例化对象。
2.表示抽象的类型
应用场景:
比如:比如,只说一辆车,车是抽象的,是一个很笼统的概念。因为有多车,不知道具体什么车,并且你不会拿车实例化对象,车这个类就可以定义成抽象类,里可以写成员函数,但是不写具体实现。
但是某个品牌的车,比如奔驰,可以继承车这个抽象类,只需要重写纯虚函数,就可以实例化对象。
具体的继承抽象的。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。只是可以有隐藏。
虚函数的继承是一种接口继承,派生类只是继承了基类虚函数的接口,目的是为了重写,达成多态。
所以如果不实现多态,不要定义成虚函数。
多态的原理
虚函数表
通过观察我们发现,对象b是8个字节,除了_num外,还有_vfptr指针。这个指针是我们叫做虚函数表指针,简称虚表指针。一个含有虚函数的类中至少有一个这样的指针。
_vfptr指针变量保存的是虚函数表的起始地址。
虚函数表实际是一个函数指针数组,虚函数表简称虚表。虚表里面保存的都是虚函数的地址。
派生类中的虚表指针
派生类不重写基类的虚函数
通过上面现象说明一个结论:派生类会继承基类的虚函数,会继承基类的虚表。但是派生类和基类的_vfptr变量内容不相等,说明两个虚表不是同一种虚表,只是虚表里的内容相同,所以会调用同一个函数。
派生类重写虚函数
通过上面现象说明一个结论:派生类重写基类虚函数,会重写派生类虚函数表里的内容,将对应位置覆盖层重写虚函数的指针。
派生类增加虚函数
注意:
1.类中有虚函数只是这个类中多了虚函数表指针,不是将虚函数表保存到类中。
2.虚函数表最后会以nullptr结尾。
3.同类型的对象共用一张虚表,可以理解成一个类的虚表属于这个类的,实例化的对象,都公用这一张虚表。
总结派生类虚表的生成:
1.派生类会继承基类的虚表,当然两个虚表表不是一张虚表。派生类先将基表虚表的内容拷贝一份到派生类的虚表中
2.如果虚表重写虚函数,用派生类重写虚函数的地址覆盖掉虚表中对应虚函数的地址。
3.派生类增加虚函数,会在派生类虚表中声明次序增加到虚表的最后(不会新增虚表)。
虚表保存在哪
首先说明,一个具有虚函数的类_vfptr保存在前面还是后面是由平台决定的,根据上面的现象,我们平台_vfptr是保存在最前面的
多态原理
多态是基于虚函数的虚函数表。构成多态,跟对象有关。如果是基类对象,会去基类的虚表中找要调用虚函数的地址,去执行虚函数的代码。如果是派生类对象,会去派生类类的虚表中找要调用虚函数的地址,去执行虚函数的代码。
派生类虚函数重写之后,可以实现不同的对象,有不同的实现方法,展现不同的效果。
再来段代码理解一下:
通过汇编分析,看出满足多态以后函数调用,不是在编译时确定的,是在运行起来后到对象的需表中找的。
不满足多态,是在编译时确定好的。
动态绑定和静态绑定
静态绑定又称为前期绑定,在程序编译期间确定了程序的行为,也称静态多态。比如函数重载。在编译的时候确定了调用的函数。
动态绑定也称后期绑定,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称动态多态。就像上面的,运行时在到虚表中找调用函数的地址。
多态,多数都是动态绑定。
单继承和多继承的虚函数表
单继承中的虚表
结论是否如上图所述:我们来打印一下虚表
打印代码
class Person { public: virtual void func1(){ cout << "Person func1()" << endl; } virtual void func2(){ cout << "Person func2()" << endl; } }; class Student :public Person { public: virtual void func1(){ cout << "Student func1()" << endl; } virtual void func3(){ cout << "Student func3()" << endl; } protected: }; typedef void(*VFPTR)();//声明一个函数指针即 typedef void (*)() VFPTR; //打印代码 void PrintVfTable(VFPTR *vftable){ for (int i = 0; vftable[i] != nullptr; i++){ //打印虚表内容 printf("vftable[%d]:%p\n", i, vftable[i]); //调用这个函数 VFPTR fun = vftable[i]; fun(); } cout << endl; } int main() { Person p; Student s; //要得到虚表指针的内容,由于这个平台,虚表指针是保存在开始的 //先得到对象地址,强转成int *得到前四个字节,就是虚表指针的地址 //再解引用,得到虚表指针的内容 //再强转成函数二级指针 PrintVfTable((VFPTR *)*(int *)&p); PrintVfTable((VFPTR *)*(int *)&s); getchar(); return 0; }
多继承中的虚表
根据上面的结论可以得到这样一张图:
#include<iostream> using namespace std; class Base1 { public: virtual void func1(){ cout << "Base1 : func1()" << endl; } virtual void func2(){ cout << "Base1 : func2()" << endl; } protected: int _a; }; class Base2 { public: virtual void func1(){ cout << "Base2 : func1()" << endl; } virtual void func2(){ cout << "Base2 : func2()" << endl; } virtual void func3(){ cout << "Base2 : func3()" << endl; } protected: int _b; }; //多继承 class Deirve :public Base1, public Base2 { public: virtual void func1(){ cout << "Deirve : func1()" << endl; } virtual void func4(){ cout << "Deirve : func4()" << endl; } virtual void func5(){ cout << "Deirve : func5()" << endl; } protected: int _c; }; typedef void(*VFPTR)();//声明一个函数指针即 typedef void (*)() VFPTR; void PrintVfTable(VFPTR *vftable){ printf("虚表地址:%p\n", vftable); for (int i = 0; vftable[i] != nullptr; i++){ //打印虚表内容 printf("vftable[%d]:%p\n", i, vftable[i]); //调用这个函数 VFPTR fun = vftable[i]; fun(); } cout << endl; } int main() { Deirve d; //打印继承Base1的虚表 PrintVfTable((VFPTR *)*(int *)&d); //打印继承Base2的虚表 //加Base1大小的字节数,到Base2的虚表指针。 //要加先强转成char *,步长为一个字节。不强转的话,步长为Deirve PrintVfTable((VFPTR *)*(int *)((char *)&d + sizeof(Base1))); getchar(); return 0; }
画图表示为: