目录
一、多态的概念
多态通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态。
就比如在火车站买票这个行为:对于不同的用户,有不同的购票方式,比如学生可以买学生票,享受半价优惠;军人可以享受优先购票的权利;而普通人则只能排队购买全价票。这就是一个典型的多态问题,多态就是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
构成多态的条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写;若不满足重写,则是隐藏关系3.父类指针或者引用去调用虚函数
二、虚函数重写
虚函数即被virtual修饰的类成员函数
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl;}//虚函数 };
2.1虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数(即虚函数+三同)。
2.2虚函数重写特例
1.协变(基类与派生类虚函数返回值类型不同):
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变2. 析构函数的重写(基类与派生类析构函数的名字不同):
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。
注意:只有派生类的析构函数重写了基类的析构函数,delete对象调用析构函
数,才能构成多态,才能保证指针指向的对象正确的调用析构函数。
2.3关键字override和final
1. final:
修饰虚函数,表示该虚函数不能再被重写
修饰类,表示该类不能被继承
2.override:
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。(只是一个检查的作用)
2.4重载、覆盖、隐藏的对比
三、抽象类
3.1概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car { public: virtual void Drive() = 0;//抽象类定义接口 };
3.2接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
四、虚表
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
对于如上代码,sizeof(Base)的大小应该是多少呢?如果你不知道虚表的话,那肯定会说等于4。但实际上sizeof(Base)=8,为什么?
因为除了_b成员,还多一个__vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
1.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
2.派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
3. 本小节之前(5.2小节)买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。
五、多态常问面试题
1.什么是多态?
答:通俗来说就是多种形态,不同的对象去做同一件事情,得到的结果不一样。
2.什么是重载、重写(覆盖)、重定义(隐藏)?
答:
重载:在同一作用域内,函数名和参数相同。
重写:分别在基类和派生类的作用域,函数名、参数、返回值必须相同(除两个特例外),且必须是虚函数。
重定义:分别在基类和派生类的作用域,函数名相同。不构成重写的基类和派生类同名函数就是重定义。
3. 多态的实现原理?
答:
1. 用virtual关键字声明的函数叫做虚函数,虚函数肯定是类的成员函数。
2. 存在虚函数的类都有一个一维的虚函数表叫做虚表。当类总声明虚函数时,编译器会在类中生成一个虚函数表。
3. 类的对象有一个指向虚表开始的虚指针,使调用虚函数时,能够找到正确函数。虚表和类是对应的,虚表指针和对象是对应的。
4.inline函数可以是虚函数吗?
答:可以,但是inline只是一个建议,当一个函数是虚函数后,多态调用中inline失效了,因为虚函数要放到虚表中去。
5.静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用 “类型::成员函数” 的调用方式。虚函数是为了实现多态,多态运行时去虚表找决议。static成员函数都是在编译时决议,所以他是虚函数没有价值。
6.构造函数可以是虚函数吗?
答:不可以,虚函数是为了实现多态,运行时去虚表找对应虚函数进行调用,对象中虚表指针都是构造初始化列表阶段才初始化的。构造函数虚函数时没有意义的。
7.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:(1)可以,并且最好把基类的析构函数定义成虚函数。
8.拷贝构造和operator=可以是虚函数吗?
答: 拷贝构造不可以,拷贝构造也是构造函数。
operator=可以,但是没有实际性的意义。
9.对象访问普通函数快还是虚函数更快?
答:如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
10. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
11. C++菱形继承的问题?虚继承的原理?
答:菱形继承存在数据冗余和二义性,而虚继承就是解决菱形继承中数据冗余和二义性的问题。
12.. 什么是抽象类?抽象类的作用?
答:(1)包含纯虚函数的类叫做抽象类(也叫接口类)。纯虚函数:在虚函数的后面写上 =0 。
(2)抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。