注:以下示例均是在VS2019环境下
一、多态的概念
通俗来讲,多态就是多种形态,当不同的对象去完成某个行为时,会产生出不同的状态。即不同继承关系的类对象,去调用同一函数时,产生不同的行为。
比如”叫“这个行为,不同的动物,发出的声音是不同的。
二、多态的定义及实现
1.多态的构成条件
(1)必须是在继承环境下。
(2)被调用的函数必须是虚函数,且派生类中必须对该虚函数进行重写。
(3)必须通过基类的指针或引用去调用虚函数。
2.虚函数
被virtual修饰的类成员函数。
3.虚函数的重写
3.1虚函数的重写(覆盖)
派生类中有一个跟基类中完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表都完全相同),称为子类的虚函数重写了基类的虚函数。
注意:
(1) 在重写基类虚函数时,派生类的虚函数不加virtual关键字修饰时,也可以构成重写(因为基类的虚函数被继承下来了,在派生类中依旧保持虚函数属性),但是这种写法不太规范,不建议这样使用。
(2)基类和子类虚函数的访问权限可以不同,但是一般都会将基类的虚函数设置为public。
3.2虚函数重写的两个例外
(1)协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数的返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。
注意:
基类和派生类必须属于同一继承体系;基类的返回值和派生类的返回值必须属于同一继承体系。但是返回值和基类、派生类不一定属于同一继承体系。
(2)析构函数的重写(基类与派生类析构函数名不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否使用virtual关键字修饰,都与基类析构函数构成重写,但是析构函数名不同。
函数名不同,看起来似乎违背了重写的规则,单其实是编译器对析构函数名做了特殊处理,编译后析构函数名称统一处理成了destructor。
4.C++11——override和final
在某些情况下,由于疏忽可能会导致函数名字不同而无法构成重写,而这种错误在编译期间是不会报错的,只有在程序运行时没有得到预期结果才通过调试寻找错误,代价较高。
因此C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
4.1override关键字
检查派生类虚函数是否重写了基类某个虚函数,若没有则编译报错。
4.2final关键字
只能修饰虚函数。
修饰虚函数,表示该虚函数不能再被重写。
5.重载、重写(覆盖)、隐藏(重定义)的对比
注意:
(1)重写和重定义必须在继承体系内。
(2)重写只能是成员函数,而且是重写基类的虚函数;重定义既可以是成员函数,也可以是成员变量。
三、抽象类
1.抽象类概念
(1)在虚函数后面加上=0,则该函数为纯虚函数。
(2)包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化对象。
(3)派生类继承抽象类后,也不能直接实例化对象,必须对基类所有纯虚函数进行重写后才能实例化对象,否则派生类也是抽象类。
(4)纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口的继承。
注意:纯虚函数可以不用写函数体,写了不影响但没有意义。
2.接口继承和实现继承
2.1实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
2.2接口继承
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所有若不需要实现多态,不要把成员函数定义为虚函数。
四、多态的原理
1.虚函数表
只要类中存在虚函数,对象大小就会多4个字节:
该指针指向的是一段连续的空间,即虚函数表,里面存放的是虚函数入口地址,且顺序与定义顺序相同。
对象模型:
基类虚表:
(1)虚函数入口地址存放顺序与虚函数定义顺序相同。
(2)一个类的多个对象公用同一个虚表
2.虚函数与虚表存放位置
(1)虚函数和普通函数一样存放在代码段,虚表存放的是虚函数指针。
(2)对象中存放的是虚表指针,不是虚表。
(3)在vs环境下,虚表是存放在代码段的。
3.静态多态与动态多态
3.1静态多态
在程序编译阶段,就已经确定了程序的行为,也叫静态绑定、前期绑定(早绑定),比如:函数重载、模板。
3.2动态多态
在程序运行时,根据程序拿到的具体类型确定程序的具体行为,调用具体的函数,比如根据基类的指针或引用指向不同类的对象,选择对应的虚函数进行调用。也叫动态绑定、后期绑定(晚绑定)。
五、单、多继承中的虚函数表
5.1单继承中的虚函数表
基类虚表:
(1)按虚函数声明顺序存放入口地址。
(2)一个类的多个对象公用同一个虚表
子类虚表:
(1)子类有自己独立的虚表,不与父类共用。
(2)子类会将基类虚表拷贝一份放入自己的虚表中。
(3)如果子类重写了基类的某个虚函数,就会用子类自己虚函数的地址去覆盖虚表中被重写的基类虚函数的地址。
(4)子类自己定义的虚函数,其地址入口会按声明顺序依次存放在虚表的末尾。
5.2多继承中的虚函数表
基类虚表:
(1)按虚函数声明顺序存放入口地址。
(2)一个类的多个对象公用同一个虚表
子类虚表:
(1)子类会将基类虚表拷贝一份放入自己的虚表中。
(2)如果子类重写了基类的某个虚函数,就会用子类自己虚函数的地址去覆盖虚表中被重写的基类虚函数的地址。
(3)子类自己新增的虚函数,会将其入口地址添加在第一张虚表之后,第一张虚表即子类第一个继承的基类所拷贝的虚表。
六、常见问题
1.什么是多态?
2.什么是重载、重写、重定义?
3.多态的实现原理?
虚表的构造?
虚函数的调用原理:
(1)获取对象虚函数表指针(对象前4个字节)
(2)传递this指针
(3)从虚表中找到对应虚函数的入口地址
(4)调用该虚函数
4.inline函数可以是虚函数吗?
语法上可以。不过编译器会忽略inline属性,这个函数就不再是inline,因为虚函数的地址入口需要放入到虚表中去。
5.静态成员可以是虚函数吗?
不可以。因为静态成员函数没有this指针,使用“类名::成员函数”的调用方式无法访问虚函数表,所以静态成员不能放入虚函数表。
6.构造函数可以是虚函数吗?
不可以。因为对象的虚函数表指针是在构造函数初始化列表阶段才初始化的。
7.析构函数可以是虚函数吗?什么场景是?
可以。最好是把基类的析构函数定义为虚函数。场景:在继承体系中,基类的析构函数最好设置为虚函数,防止用基类的指针去销毁派生类对象时,只调用基类的析构函数而不调用子类的析构函数。
8.对象访问普通函数快还是虚函数快?
若是普通对象,则一样快;若是指针对象或引用对象,则调用普通函数快。因为通过指针或引用访问虚函数时,需要在运行过程中去查询虚表才能确定函数入口地址,而普通对象在编译时就已经确定了函数的入口地址。
9.虚函数表是在哪个阶段生成的,存放在哪儿?
虚函数表在编译阶段生成,一般情况存放在代码段(常量区)。
10.菱形继承的问题?虚拟原理?
注意不要混淆虚函数表和虚基表。
11.什么是抽象类?抽象类的作用?
……
作用:抽象类规范了派生类必须重写纯虚函数,另外纯虚函数更体现出了接口的继承。