目录
一、多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票。不同的对象会产生不同的结果,这就是多态的一种表现。
二、多态的定义及实现
1、多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。
🌟那么在继承中要构成多态还有两个条件:
⭕必须通过基类的指针或者引用调用虚函数
⭕被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
①虚函数
虚函数:即被 virtual 修饰的类成员函数称为虚函数。
②虚函数的重写
虚函数的重写,可也以称作覆盖,是指派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
当派生类和父类都加上 virtual 时(最规范)
当父类有 virtual 而派生类没有 virtual 时
在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
当父类没有 virtual 而派生类有 virtual 时
不仅结果是错的,而且会报错。这是因为不满足多态的条件,所以是看调用者的类型,调用这个类型的成员函数。Func函数的参数是 Person* ,所以只会调用 Person 的成员函数。
【重写例外】
① 协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。总结是返回值可以不同,但必须是父子关系指针或者引用。也可以是单独的父子类。
② 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成 destructor 。
满足多态,按照参数类型调用函数。子类的指针调用了 test ,继承了父类的test。缺省参数虽然不同但是依旧构成重写,因为
2、override 和 final
① override
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错,构成重写成功运行。下图是成功运行:
②final
修饰虚函数,使虚函数不能再被重写。
修饰类不能被继承
关键字没有出现之前,如果不想类被继承,大佬们都是这样设计的:将不想被继承的类的构造函数或者析构函数变成私有,那么任何函数都访问不到它的构造或析构,那么就无法继承下去。
3、重载、重写、隐藏的对比
重写(覆盖):基类与派生类中具有相同的虚函数,派生类中的同名虚函数构成重写。不过要求三同(返回类型,函数名字,参数类型都相同)。
隐藏(重定义):基类和派生类中具有相同的同名函数,派生类中即可构成隐藏。
重载:要求在同一作用域内,才可以构成重载,同名函数参数也要相同,就构成重载。
三、抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
如果需要实例化对象,子类必须重写虚函数
四、动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
五、单继承和多继承关系的虚函数表
1、单继承中的虚函数表
观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这 两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印 出虚表中的函数。
这个其实是编译器的问题,它把fun3 fun4 隐藏起来了,正常来说fun3 fun4 是派生类的虚函数,会放在派生类的虚函数表中最后面,但发现虚表中并没有。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。如下图,可以看到打印出来的虚表,Derive 继承覆盖了fun1,继承了func2。【虚函数表的打印】
2、多继承中的虚函数表
Derive 继承了 Base1和 Base2,所以会形成两个虚表。此时我们也可以看到派生类的虚函数因为编译器又没有显示完整,但实际上是完整继承的,与单继承一样。
❓为什么派生类重写的虚函数地址不一样
派生类继承了两个基类,所以派生类有两个虚表,派生类中对虚函数 fun1进行了重写,那么重写后的虚函数地址会覆盖在原来的虚表上。这些都是没有问题的。有问题的是继承下来的Base1的虚表中重写的fun1地址和Base2的虚表中重写的fun1的地址竟然不一样。派生类中只有一个fun1,理论上这两个重写虚函数地址应该相同啊。但是这里面并不相同,而且当我们去调用这两地址时,就会发现竟然都调用了派生类中的虚函数fun1。这是什么原理呢?为了便于理解,我们打印出这两个虚表。
【解答】
虽然地址不一样,但是最终调用的函数都是同一个。想要调用派生类的函数,即需要传 this 指针去调用Derive d.fun1();本质上是用this指针去调用fun1的,而this指针是Derive类型的,一开始是指向Derive对象的起始位置。ptr1和ptr2调的都是派生类的函数fun1,ptr1恰好指向子类对象的开始,和this指针重合,但ptr2在对象的中间,需要修正this指针的位置。
❓派生类的虚函数放在哪一个虚表中
【解答】只会放在第一个虚表中。