目录
一、概念、格式
不同的对象去做同一件事情,有不同的结果,多态
同一件性质事件、行为的多个形态
二、虚函数重写
什么叫虚函数?函数前加了一个virtual关键字
只能修饰局部函数,修饰全局函数无意义
多态必须在继承体系下成立、传的是谁就调用的谁
1、特征
1)父子类完成虚函数重写(函数名、参数(不看缺省值)、返回值都相同)
2)父亲的指针或者引用去调用虚函数
2、虚函数重写的协变
什么是协变?
就是字符类的虚函数返回值可以不一样
是重写的一个特殊情况
就叫做协变
3、析构函数的重写
析构函数建议写成虚函数,加关键字virtual构成重写
加了关键字之后,会在底层将析构函数名都改成destruct,这样才满足重写的条件(函数名相同)
统一改名字的目的,就是为了构成重写,达到传入谁,就调用谁的析构的目的
不加关键字,就构成继承结构下的隐藏,如果要调用,需要指定域访问
就达不到谁调用,调谁的效果
也就是不指定调用,就不会调用析构函数
4、派生类可不写vitual但不建议
为什么?
虚函数的重写,重写的是函数体的实现
实际上,是把基类函数声明继承下来,和派生类的函数实现进行组合
也就是说,是否构成重载和派生类的函数声明无关
子类的函数重写是实现,是父类的函数声明加上子类函数定义的组合
重写的是函数定义,而不是函数声明,所以加不加无所谓,因为也不看
同时,继承并不是真正的从父类拷贝一份代码下来
而是找的时候依旧到父类去找对应的函数
所以,事实上,父类的函数实现,this指针依旧是父类对象
子类中把父类视为一个自定义类型
普通调用:看对象的类型是什么
多态调用:谁调用,就指向谁
为更好的理解上述所论,可以看下面这一段代码:答案选B(父类声明+子类函数实现)
5、重载、继承隐藏、多态重写
多态需要在继承的基础上构成
隐藏(重定义)只要是函数名相同即构成(无关参数)
重写(覆盖)要求函数名、参数、返回值相同,且必须是虚函数(协变除外)
三、override和final
用final修饰的类叫做最终类,不能被继承
格式:写在类名后面
class A final{...}
override用在子类的虚函数
作用:检查是否完成重写,检查语法等
例如父类写vittual了没有等等,重写的时候才需要voerride
格式:写在函数名后面
virtual void func()override {...}
四、虚函数表指针
1、虚函数概念
也称虚表指针(只要有虚函数就会有虚表指针)
干嘛的?用来实现多态
有虚函数的类,在开辟空间时,会多开辟一个指针
这个指针指向一个空间,叫做虚函数表
这个空间会存所有的虚函数(就是加virtual关键字的函数)的地址
按照声明顺序来存放地址
因此,
虚表的本质是一个函数指针数组
内部存的只是虚函数的地址
而虚函数真正存的地方,跟普通函数一样,在代码段
虚表存在哪里呢?
首先虚表不是存在对象里的
对象存的只是虚表的指针
虚表存在常量区,也就是所谓代码段
总结:
存在虚函数,多开辟一个虚表指针
虚函数存在常量区(即代码段)
虚表存在常量区(即代码段)
虚表是在构造函数初始化列表部分进行初始化的
五、多态如何实现?(核心原理)
多态:传父类调父类,传子类调子类
在编译的时候,子类和父类如果构成多态
构成各自的虚表怎么构成?
1、父类的虚表是如何构成的?
生成一个虚表指针
这个虚表存放父类所有的虚函数地址
以此,构成父类的虚表
2、子类的虚表是如何构成的?
拷贝父类的虚表,如果子类有新的虚函数,加上去;
如果子类对父类的虚函数进行了重写,就覆盖原来的父类虚函数
如此,构成子类自己新的虚表
所以,事实上
也可以这样说:
子类的虚表其实不是自己生成的
而是继承的父类的
只是,多的以及重写虚函数要进行添加和覆盖
3、各自的虚表实现多态的访问
这样,在进行调用的时候,
你传父类的对象,调用的是父类的虚表,父类的虚表存放的是父类的虚函数地址
你传子类的对象,调用的是子类的虚表,子类的虚表存放的是子类+父类的虚函数地址
4、为什么重写也叫做覆盖?
子类继承父类的虚函数就是把父类的所有虚函数全部拷贝下
也就是直接拷贝父类的虚表
但是子类会把子类自己重写的虚函数覆盖父类的虚函数
所以,重写也叫做覆盖
如果子类没有重写,那就不会覆盖,此时如果调用,调用的依旧是父类的虚函数
子类对象模型内部会有一个父类的虚表,加上自己的成员变量
即,子类也只有一个虚表
从父类继承而来的,改造过的虚表
父类没有虚表,无从继承,自己才会生成
注意:
同类型对象共用一张虚表
不同类型对象有各自的虚表
六、多继承和菱形继承的的虚表
如果子类继承了多个父类(不是菱形继承)
在子类的对象模型内,就会有多个父类的对象模型
什么叫做对象模型:类对象的成员变量+虚表
为什么要有多个父类对象的模型?
因为多态的调用是父类的指针和引用
子类对象赋值给父类对象时
需要进行切片
假如没有,那么对父类对象赋值的时候
就没有办法进行区分到底继承的父类有哪些东西
所以,从这里来看
切片,就是为解决多态而存在的
派生类的虚函数放在第一个继承的基类的虚表内
菱形虚拟继承(了解一下)
以及虚基表如何解决数据冗余和二义性的问题
继承是白盒
七、抽象类
1、格式
虚函数不实现,只有声明
且在虚函数后赋值0
这个叫做纯虚函数
class A
{
public:
virtual void func() = 0;
};
2、特点
不能实例化出对象
意义:强制派生类完成虚函数重写
如果不重写,就继承了父类的纯虚函数,那个自己本身也就有了一个纯虚函数
那么自己也会变成一个抽象类,无法被继承
可以理解为:虚函数的继承是一种接口继承
普通函数的继承是实现继承
八、继承的虚基表和多态的虚函数表
继承中的虚基表:存的是基类部分的偏移量,解决菱形继承的数据冗余和二义性的问题
多态中的虚函数表:存的是基类和派生类各自的虚函数,解决基类和派生类对象目标函数访问准确的问题
总结:
多态:要求父类指针/引用调用,虚函数重写
进而实现:编译时,
父类有父类的虚表
子类有子类的虚表
进而实现:运行时,
指向父类,到父类的虚表中找,调用父类虚表中的的虚函数
指向子类,到子类的虚表中找,调用子类虚表中的的虚函数
九、面试相关
对多态有一个系统的知识构架,经得住问,知道是什么,为什么,怎么样,即可做到问不到,如此,学问才算有些许火候。
1、什么多态?
多态,简单的来说,就是同一个事情,不同的人做,有不同的行为以及结果
例如,买票,儿童免票,少儿半价,成人全票,残疾人特殊票等等
2、什么是重载、重写(覆盖)、重定义(隐藏)?
重载:
重载是函数的重载,是在同一个域内实现的
构成的条件是函数名相同,但是参数不同,返回值不同并不构成重载
而代码最终运行都是指令
原理是在编译的时候,把参数作为函数名字编码的一个参量
由此,既可做到虽函数名相同,但是不同的参数有不同的函数编码
重写与重定义:
重写是在多态的情况才存在
而多态是在继承的基础上才构成
重写的构成条件是基类和派生类的函数名相同、参数相同(不看缺省值,只是形参),返回值相同
同时,在返回值不同的特殊情况叫做协变
重写的存在,就是为了构成多态,让派生类拥有自己的特点、方法
而重定义是在继承体系下才会存在
重定义的构成条件是基类和派生类的函数名相同,不论参数、返回值相同与否
简单而言,不构成重写,就是重定义
3、多态的实现原理?
核心是虚表指针的机制
只要有虚函数,在编译时就会生成一个虚表指针
这个虚表指针指向一个数组空间
这个数组空间存放该类以及继承而来的所有虚函数的地址
虚表指针和数组空间都存在常量区,即所谓代码段
因此,这样的机制,使得:
父类有父类的虚表
子类有子类的虚表
如此,
父类对象访问,去父类的虚表找函数
子类对象访问,去子类的虚表找函数
就达到了多态的目的
同时,子类的虚函数是从父类那里继承过来的
子类多余的加进去,重写的九覆盖,所以重写也叫做覆盖
多继承的情况呢?
继承两个父类,存两个父类的虚表,自己的虚表存在第一个继承的父类的虚表内
继承三个父类,存三个父类的虚表,自己的虚表存在第一个继承的父类的虚表内
...
如此类推
为什么要存多个虚表呢?
不是冗余吗?
不,这是为了赋值方便,为了切片
这样
son赋值给father1,就切father1的虚表过去
son赋值给father2,就切father2的虚表过去
...
如此类推
4、inline可以是虚函数吗?
可以
但是编译器会忽略inline属性
这个函数不再是inline(内联)
因为虚函数要放到虚表内
5、静态成员可以是虚函数吗?
不可以
因为静态成员没有this指针
没有this指针
无法放进虚函数表
不在虚函数表,即成员函数的调用方式无法访问
所以,导致对象无法调用
对象无法调用,那么多态就没有意义
6、构造函数可以是虚表函数吗?
不能
因为虚函数表是在构造函数的初始化列表阶段进行初始化的
如果构造函数也搞成虚函数,也放进了虚函数表
那么,虚函数表到哪里进行初始化?
7、析构函数可以是虚函数吗?什么场景下,析构函数是虚函数?
可以
上文有,自己查看
8、对象访问普通函数快还是虚函数快?
如果是一般对象,一样快
但如果是指针/引用,那么普通函数快
因为虚函数的调用还要多一步
就是到虚函数表查找
9、虚函数表在什么阶段生成?存在哪里?
在编译阶段,在构造函数的初始化列表阶段
一般存在代码段,即所谓常量区
10、C++菱形继承是什么?原理是什么?
上文有,自己查看
11、什么是抽象类?作用是什么?
抽象类:虚函数不写实现,函数名赋值0
意义:强制派生类重写虚函数
体现接口继承关系