C++ 中的多态
@TOC
零、前言
C++有五大特性:对象,封装,继承,抽象和多态。而本章则将学习讲解 C++中的多态
一、多态的概念和定义
概念:
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
示例:买票
普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票
定义:
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为
多态构成条件:
必须通过基类的指针或者引用调用虚函数
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
示图:
二、虚函数
1、概念和定义
虚函数语法:
被 virtual 修饰的类成员函数称为虚函数
示例:
虚函数重写:
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数
示例:
结果:
2、虚函数重写的特例
协变
类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变(基类与派生类虚函数返回值类型不同)
示例:
结果:
派生类虚函数不加 virtual
在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字也可以构成重写
原因:
继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性)
示例:
结果:
注意:
该种写法不是很规范,不建议这样使用
如果基类的虚函数不加 virtual,派生类的虚函数加 virtual,这种情况是不构成虚函数的
析构函数的重写
我们知道,基类指针和引用可以指向基类和派生对象,由此通过指针和引用释放对象时需要实现析构的多态,但基类与派生类析构函数名字不同
为了解决,编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor
示例:
结果:
3、C++11 override 和 final
引入:
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的(编译器会按需实例化,只有实例化才会进行检查)
为此 C++11 提供了 override 和 final 两个关键字,可以帮助用户检测是否重写
final
修饰虚函数,表示该虚函数不能再被重写
示例:
结果:
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
示例:
结果:
4、重载/重写/重定义对比
对比示图:
三、抽象类
概念:
在虚函数的后面写上 =0 ,则这个函数为纯虚函数
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
示例:
结果:
接口继承和实现继承:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口(如果不实现多态,不要把函数定义成虚函数)
注意:
虽然函数重写需要接口一样,但是对于参数的缺省值没有检查,如果基类虚函数和派生类重写函数的缺省值不同,按照基类虚函数的接口来走,也就是说不用管派生类重写函数的接口
四、多态的原理
1、虚函数表
例题:
结果:
解释:
b 对象是 8bytes,除了_b 成员,还多一个 _vfptr 放在对象的前面
注意:
_vfptr 一般存放在变量前(存放位置与平台有关)
对象中的这个指针我们叫做虚函数表指针(v 代表 virtual,f 代表 function)
一个含有虚函数的类中都至少都有一个虚函数表指针(因为虚函数的地址要被放到虚函数表中,虚函数表也称虚表)
示例:
示图:
说明:
d 对象由两部分构成,一部分是父类继承下来的成员(虚表指针也就),存在部分的另一部分是自己的成员
对于派生类 d 对象,因为 Func1 完成了重写,所以 d 的虚表中存的是重写的 Derive::Func1(将继承的虚函数进行重写,而对应的在虚函数表上进行覆盖成自己的虚函数地址也叫作覆盖)(重写是语法的叫法,覆盖是原理层的叫法)
Func2 继承下来后是虚函数,所以放进了虚表,Func3 也继承下来了,但是不是虚函数,所以不会放进虚表
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 nullptr(用来表示结束)(可能根据编译器而定)
派生类的虚表生成总结:
先将基类中的虚表内容拷贝一份到派生类虚表中
如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
注意:
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是函数的地址存到了虚表中,另外对象中存的是虚表指针,而虚表存在代码段中的(在 vs 平台下)(可以打印对象的地址与常量,变量进行比较)
2、多态的原理
示例:
示图:
说明:
p 是指向 mike 对象时,p->BuyTicket 在 mike 的虚表中找到虚函数是 Person::BuyTicket
p 是指向 johnson 对象时,p->BuyTicket 在 johson 的虚表中找到虚函数是 Student::BuyTicket
这样就实现出了不同对象去完成同一行为时,展现出不同的形态
要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到该对象中取找的(所以多态的条件包括使用基类对象指针或引用去调用,而不是基类对象);不满足多态的函数调用是在编译时确认好的
示图:
汇编:
3、动态绑定与静态绑定
概念:
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态(如函数重载)
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
4、多继承虚函数表
注:我们想看多继承的虚函数表是怎么样的,但是内容窗口是有点问题的(可以认为是编译器设计没考虑到),没办法看到实际的情况,所以这里我们设计特殊的虚函数(返回值,参数相同)并使用打印虚函数表函数进行查看地址和调用验证
示例:
示图:
说明:
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
为什么对于两个虚函数 func1 的覆盖地址会不一样?
这里覆盖的地址进行了一次包装,每个地址都指向一个 jump,而两个 jump 存的是同一个虚函数地址
对于菱形继承/菱形虚拟继承:
实际中不建议设计出菱形继承及菱形虚拟继承:一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗
大概结果:示例: B 继承 A C 继承 A D 继承 B,C
五、继承和多态常见的面试问题
什么是多态?
简单来说,多态是不同对象同种行为产生不同状态
即在不同继承关系的类对象,去调用同一函数,产生了不同的行为
什么是重载、重写(覆盖)、重定义(隐藏)?
重载:在同一作用域中,函数名相同和参数不同构成重载
重写:在基类和继承类域中,对于虚函数,函数名,参数和返回值相同(特例除外),构成重写
重定义:在基类和继承类域中,函数名相同,不构成重载就构成重定义
多态的实现原理?
对于虚函数类会在对象的成员变量中生成虚函数表指针,指向的虚函数表中储 了该对象的虚函数地址
对于派生类会继承基类的虚函数表,如果派生类重写了虚函数,则会对继承的虚函数表中对应的函数地址进行覆盖成派生类对象的虚函数
当对象调用虚函数时会先找到虚函数表,通过虚函数表找到对应的虚函数,由此构成多态
inline 函数可以是虚函数吗?
可以,不过编译器会忽略 inline 属性,这个函数就不再是 inline,因为虚函数要放到虚表中去
静态成员可以是虚函数吗?
不能,因为静态成员函数没有 this 指针,不能通过对象指针找到其虚函数表,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表
构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的,如果构造函数为虚函数,会先在对象中的虚函数查找对应虚函数,而此时虚函数表没有生成(动态多态是运行时绑定)
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数
当基类指针指向 new 出来的基类对象或 new 出来的派生类对象时,需要使用指针进行释放对象,此时需要析构函数为虚函数,保证指针指向的对象成功释放
对象访问普通函数快还是虚函数更快?
如果是普通对象,是一样快的
如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找
虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段(对象构造时)就生成的,一般情况下存在代码段(常量区)的
C++菱形继承的问题?虚继承的原理?
菱形继承存在数据冗余和二义性的问题
虚继承会让继承的父类在成员变量中生成虚基表指针,指向虚基表会储存其继承的基类成员变量距离其成员变量的距离,通过该距离找到其基类成员变量,而两个继承的父类中的虚基表指向同一份父类的父类成员变量
注意:不要把虚函数表和虚基表搞混了
什么是抽象类?抽象类的作用?
抽象类用来表示现实中一些抽象的事物类型,抽象类不具有实例,派生类只是继承其虚函数接口,它强制派生类重写虚函数,否则派生类也是抽象函数
抽象类体现出了接口继承关系 ,以及用来更好的表示现实中一些抽象的事物类型
版权声明: 本文为 InfoQ 作者【可口也可樂】的原创文章。
原文链接:【http://xie.infoq.cn/article/3b305522dbc8bb393d736e853】。文章转载请联系作者。
评论