1、多态
多态就是去完成具体某个行为,当不同的对象去完成时会产生不同的状态。
1.1、分类
- 静态多态(静态绑定、静态联编、早绑定):在编译期间,就可以确定函数的行为,即:具体调用哪个函数。(函数重载、模板)
函数重载:在编译期间,编译器会对函数实参类型进行推演,然后根据推演的结果选择合适的函数进行调用,如果有完全匹配的,直接调用;否则进行隐式类型转化,如果可以转化则调用,否则报错。 - 动态多态(动态绑定、动态联编、晚绑定):在程序运行时,根据基类指针或者引用指向不同类的对象,调用对应的虚函数,在程序运行时,确定函数具体的行为。
1.2、实现条件
- 基类中必须要有虚函数,子类必须对基类中的虚函数进行重写;
- 虚函数调用:必须通过基类的指针或者引用来调用虚函数。
如果两个条件已经完全满足了,在程序运行时,根据基类的指针或者引用指向不同类的对象选择对应类的虚函数进行调用。
2、重写
2.1、构成重写条件
- 一定是子类对基类的虚函数进行重写;
- 子类和基类虚函数的原型必须要一致:返回值类型,函数名字,参数列表必须一样
例外:
- 协变:基类虚函数返回基类的指针或者引用 / 子类虚函数返回子类的指针或者引用
- 析构函数:如果基类中的析构函数是虚函数,只要子类的析构函数给出,则必然构成重写。
2.2、为什么要将析构函数设置为虚函数?
在继承体系中,最好将基类的析构函数设置为虚函数。
当基类的指针指向子类对象时:
如果析构函数不为虚函数,则只会调用基类的析构函数,不会调用子类的析构函数。
如果析构函数为虚函数时,因为基类指针指向子类对象,实现了多态。则会先调用子类的析构函数,编译器会在子类析构函数最后一条有效语句之后添加调用基类的析构函数,然后去调用基类的析构函数。
基类虚函数可以与子类虚函数的访问权限不同,但是一般情况下都会将基类虚函数的访问权限设置为public。
3、override 和 final
- override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
- final:修饰虚函数,表示该虚函数不能再被继承。
4、抽象类
纯虚函数:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
抽象类:包含纯虚函数的类叫做抽象类。
抽象类的特性:
- 不能实例化对象;
- 子类需要对抽象类中的纯虚函数进行重写,否则子类也是抽象类。
5、多态的实现原理
5.1、虚函数表
如果类中包含有虚函数,编译器会给对象模型最前面增加4个字节,这个4个字节存储的是虚表指针,这个指针指向的是一张虚函数表。虚函数是通过一张虚函数表来实现的。
5.2、基类和子类虚表的构建过程
基类:需表中放置的是虚函数,按照虚函数在类中声明的先后次序,一次添加到虚表中。
子类:
- 将基类虚表中内容拷贝一份放置到子类虚表中;
- 如果子类重写了基类某个虚函数,则使用子类虚函数替换虚表中相同偏移量位置的基类虚函数。
- 将子类新添加的虚函数按照其在子类中声明的先后次序一次添加到虚表的最后。
5.3、虚函数表的注意事项
- 虚函数表本质上是一个指针数组,这个数组最后面放了一个nullptr,这个就相当于结束符,跟字符串的”\0“一样。
- 虚表存的是虚函数指针;
虚函数跟普通函数一样,存在代码段;
对象中存的是虚表指针;
虚表存在代码段,是不可修改的。
5.4、虚函数的调用原理
- 从对象前4个字节中获取虚表的地址;
- 传递this指针和其它参数;
- 从虚表中找对应虚函数的地址;
- 调用该虚函数。
6、多态常见面试题
1、inline函数可以是虚函数吗?
不能,因为inline函数没有地址,无法把地址放到虚表中。
2、静态函数可以是虚函数吗?
不能,因为静态函数没有this指针,使用类型::成员函数的调用方式无法访问到虚表中,所以静态成员函数无法放到虚表中。
3、构造函数可以是虚函数吗?
不可以,因为对象中的虚表指针是在构造函数初始化列表阶段进行初始化的。如果构造函数是虚函数,构造函数需要先从对象的前4个字节找到虚表指针(虚表指针是在构造函数创建对象时放进对象中的),所以无法获取到虚表指针,就无法调用构造函数。
4、对象访问普通函数快还是虚函数快?
如果是普通对象的话,一样快。
如果是指针对象或者是引用对象,则调用普通函数更快,因为构成多态,运行调用虚函数需要到虚表中进行查找。
5、虚函数表在什么阶段生成的?存在哪?
虚函数表是在编译阶段就生成的;一般情况下存在代码段(常量区)中。