目录
一.多态的概念
在之前的C++中,我们学习过函数重载,可以按不同的方法实现同一个函数,用来适应多种情况。而多态也类似于此。
下面是一个简单的函数重载
下面是一个多态实现
是不是很相似呢?其实后面我们就会知道,重载也是多态的一种形式,只不过是静态的。
二.多态的定义和实现
1.多态的构成条件
①.必须通过基类的指针或者引用调用虚函数
②.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
为什么要是基类的指针或者引用呢?
其实是因为继承中,向下转换是安全的,向上转换存在风险;并且通过指针或引用才能进行动态绑定,进而在运行时确定对象的实际类型。
那什么又是虚函数呢?
虚函数:被virtual修饰的类成员函数称为虚函数
那重写又是什么呢?
这里Dog类中的speak函数就是重写,即派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
这里派生类的虚函数不加virtual关键字,也构成重写,因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性。
虚函数重写的例外
协变
协变是虚函数重写的一个例外,即派生类虚函数的返回类型和基类不一样,但是派生类中重写的函数的返回类型必须是基类函数返回类型的子类型。
析构函数的重写
先来看一串代码
大家注意到了吗,我们构造了Dog对象,但是最后没有析构Dog对象,这就会发生内存泄漏问题。
而我们只要在基类的虚构函数前加上关键字virtual让其变成虚函数,结果就不会发生内存泄漏了。
为什么呢?
其实是派生类虚构函数重写了基类的虚构函数!
那有人会问了,明明基类和派生类的虚构函数都不是同名,为什么能重写?
其实在编译器中对虚构函数做了特殊处理,虚构函数的名称全被处理成了destructor,所以在编译器看来,基类和派生类的虚构函数就是发生了重写。
而重写了虚构函数后,构成了多态,而多态调用函数,是和指向的对象有关,此时指向Dog对象,所以析构了Dog,而析构Dog后,编译器自动析构基类。
2.override和final
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
final
修饰虚函数,表示该虚函数不能再被重写

3.重载,隐藏(重定义),重写(覆盖)的对比
重载:两个函数在同一作用域,函数名必须相同。
隐藏:两个函数分别在基类和派生类的作用域,函数名必须相同。
重写:两个函数分别在基类和派生类的作用域,函数名/参数/返回类型相同(协变除外),两个函数必须是虚函数。
三.抽象类
概念
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
纯虚函数:虚函数的后面写上=0 ,则这个函数为纯虚函数。
而派生类不进行重写的话,也是抽象类,也不能实例化出对象,所以纯虚函数可以帮助我们规范派生类必须重写。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
四.多态的原理
虚函数表
先来看看Animal类的大小,我们看到了是4,这里没有成员变量,只有一个函数speak,可大小却是4,大家应该很容易想到指针,即Animal类中存了一个指针_vfptr,这个指针又是什么呢?
_vfptr:v(virtual),f(function),ptr(指针),顾名思义,这就是一个指向虚函数表的指针,这个虚函数表存放了虚函数的地址。
它的内部结构是这样的:
而当派生类继承后会发生什么呢?
我们会发现派生类和基类存的内容怎么不一样呢?基类的虚表不应该传给了派生类吗?
其实是因为,派生类对基类进行了覆盖(重写)。
动态绑定和静态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。
五.单继承和多继承关系的虚函数表
单继承
被重写的虚函数会被覆盖成派生类虚函数的地址,类似与下图中的func2。而派生类中没有重写的函数,如果是虚函数,则会被放进派生类的虚函数表。
多继承
多继承的派生类会有(基类个)虚表,如下图中的派生类就有两个虚表,分别来自Base1和Base2,如果派生类发生了重写,则会覆盖基类的虚表;如果存在没有重写的虚函数,则会被放进第一张虚表(Base1)。
菱形继承,菱形虚拟继承(了解)
菱形继承和多继承类似。
菱形虚拟继承
这样写会报错,原因是Animal的虚表被继承下来,而Dog要重写,Cat要重写,但是就一张虚表,Pet的虚表就不知道放谁的。所以就会报错。
但这个时候,如果Pet也重写speak函数,就不会报错了。原因是,Pet重写后,也不会纠结放Dog还是Cat的虚函数表地址了,而直接放的是Pet的虚表地址。