C++多态(上)

Posted 风起、风落

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++多态(上)相关的知识,希望对你有一定的参考价值。

文章目录

1. 多态概念

就是多种形态, 具体为 去完成某个行为,当不同对象完成时会产生不同的状态


如买票这种行为,普通人是全价买票,而学生是半价买票,军人则是优先买票


又或者支付宝扫码领红包,不同的人扫到的钱是不一样的


2.多态的定义和实现

1.多态的构成条件

多态的条件:
1.虚函数的重写----三同 (函数名、参数、返回值相同)
2.父类的指针或者引用去调用

无法使用父类对象调用

当把函数的参数从父类的引用改成父类本身时,发现不能实现多态了(具体为什么后面解释)


不满足多态条件的调用

不满足多态的虚函数重写的条件
则看调用者的类型,调用这个类型的成员函数
此时属于Person类,所以两次都会调用父类的BuyTicket函数

2. 虚函数

被virtual修饰的类成员函数被称为虚函数


虚函数的重写/覆盖

派生类中有一个跟基类完全相同的虚函数(即两者的返回值类型、函数名字、参数列表都相同),称子类的虚函数重写了基类的虚函数


此时的preson中的BuyTicket函数与 Student类中的BuyTicket的函数构成重写/覆盖


传递不同的对象调用不同的函数
传父类调用的是父类的虚函数
传子类调用的是子类的虚函数

person &p=st; 由于stuent类属于person类的子类,所以p是stuent类中属于父类那一部分的别名
传递父类对象ps,调用 Person类中的BuYTicket函数
传递子类对象st,调用 Student类中的BuYTicket函数


虚函数重写的例外

1. 子类的虚函数可以不加virtual

子类重写了虚函数,重写体现了接口继承
子类把函数的声明 继承下来,重写的是函数的实现,
所以不写也可以,因为它继承父类的接口,重写实现,满足多态的条件


2.协变
返回值不同,但必须是父子关系的指针或者引用

协变的实际作用不是很大,可能在某些特殊场景可以用到

例题


父子类的func函数构成多态
1.虚函数的重写
虽然子类并没有写,但是由于例外,子类的函数会继承父类的函数的virtual
2.调用父类的指针或者引用
在父类的test函数中 存在隐藏的this指针,该this指针类型为 A*

使用父类的指针去调用,所以满足多态条件


子类的对象传给父类的指针,实际上相当于 父类的指针指向子类中父类的那一部分
由于满足多态,指向谁调用谁,所以调用的是子类的func函数


子类把父类的函数的声明 继承下来,重写的是函数的实现
所以实际上父类的缺省val值会把子类的缺省val值覆盖掉
子类的func函数 就变为val为1,输出B->1

变形题

结果为B->0


由于不满足父类的指针或者引用去调用
所以不构成多态
那虚函数的重写也就不能生效,所以在子类中的func函数中的缺省val值以及为0
不构成多态,则看调用者的类型,调用这个类型的成员函数
所以调用子类的func函数,最终输出 B->0

3.C++11 override 和final

final

修饰虚函数,表示该虚函数不能被重写


overrride

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

4.重载 、覆盖(重写)、隐藏(重定义)的对比

5. 抽象类

在虚函数后面写上=0,这个函数被称为纯虚函数,包含纯虚函数的类叫做抽象类,抽象类不能实例化对象


若创建一个子类继承抽象类,则该子类也包含纯虚函数,子类也会变成抽象类,所以子类创建对象也会报错

6. 多态原理

虚函数表

常考笔试题 sizeof(Base) 大小是多少?


若以结构体的内存对齐考虑,则大小应为8字节,但是实际上为12字节



_vfptr代表虚函数表指针,加上虚函数指针,内存对齐后字节大小为12

多态的原理


BuyTicket不是虚函数重写时,不构成多态,生成的汇编指令
不构成多态,p.BuyTicket() 调用时,就看p的类型 ,此时p的类型为Person,所以传的是 Person::BuyTicket的地址


构成多态时,多态调用转换成的汇编指令



p.BuyTicket()是不知道自己要调用那个的,通过查看传递过来的对象做出判断,
若为父类对象,则p->BuyTicket在mike的虚表中找到虚函数 Person::BuyTicket
若为子类对象,则p->BuyTicket在iohnson的虚表中找到虚函数 Student::BuyTicket

虚函数表 本质是一个虚函数指针数组

为什么叫做覆盖?


父对象和子对象都调用BuYTicket函数时,由于构成多态,要进行虚函数重写,所以子类的虚函数指针数组是由父类的虚函数指针数组拷贝过来的,再向其中填入新的地址,造成覆盖
而没有被重写的Func函数则没有被覆盖

为什么父类对象不可以实现多态?

若为父类的对象,则不可以实现多态


查看汇编时,直接就去调用Peson::BuYTicket的地址


若为指针或者引用,将子类中属于父类那一部分切出来,
使指针指向属于父类那一部分,或者作为属于父类那一部分的别名
子类的虚表还是子类的


若为父类对象,就会把子类中属于父类的那一部分拷贝给父类,
有可能把子类的虚表也拷贝给父类
若拷贝成功,则父类对象的虚表就不知道是父类的虚表还是子类的虚表了

C++多态(静多态和动多态)

如今的C++已经是个多重泛型编程语言(multiparadigm programming lauguage),一个同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)的语言。 这些能力和弹性使C++成为一个无可匹敌的工具,但也可能引发使用者的某些迷惑,比如多态。在这几种编程泛型中,面向对象编程、泛型编程以及很新的元编程形式都支持多态的概念,但又有所不同。 C++支持多种形式的多态,从表现的形式来看,有虚函数、模板、重载等,从绑定时间来看,可以分成静态多态和动态多态,也称为编译期多态和运行期多态。

 
泛型编程和元编程通常都是以模板形式实现的,因此本文主要介绍基于面向对象的动态多态和基于模板编程的静态多态两种形式。另外其实宏也可以认为是实现静态多态的一种方式,实现原理就是全文替换,但C++语言本身就不喜欢宏,这里也忽略了“宏多态”。
 
动态多态的设计思想:对于相关的对象类型,确定它们之间的一个共同功能集,然后在基类中,把这些共同的功能声明为多个公共的虚函数接口。各个子类重写这些虚函数,以完成具体的功能。客户端的代码(操作函数)通过指向基类的引用或指针来操作这些对象,对虚函数的调用会自动绑定到实际提供的子类对象上去。
静态多态的设计思想:对于相关的对象类型,直接实现它们各自的定义,不需要共有基类,甚至可以没有任何关系。只需要各个具体类的实现中要求相同的接口声明,这里的接口称之为隐式接口。客户端把操作这些对象的函数定义为模板,当需要操作什么类型的对象时,直接对模板指定该类型实参即可(或通过实参演绎获得)。
相对于面向对象编程中,以显式接口和运行期多态(虚函数)实现动态多态,在模板编程及泛型编程中,是以隐式接口和编译器多态来实现静态多态。
静态多态本质上就是模板的具现化。静态多态中的接口调用也叫做隐式接口,相对于显示接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成,隐式接口通常由有效表达式组成。
 
动态多态和静态多态的优缺点比较:
静态多态
优点:
由于静多态是在编译期完成的,因此效率较高,编译器也可以进行优化;
有很强的适配性和松耦合性,比如可以通过偏特化、全特化来处理特殊类型;
最重要一点是静态多态通过模板编程为C++带来了泛型设计的概念,比如强大的STL库。
缺点:
由于是模板来实现静态多态,因此模板的不足也就是静多态的劣势,比如调试困难、编译耗时、代码膨胀、编译器支持的兼容性
不能够处理异质对象集合
动态多态
优点:
OO设计,对是客观世界的直觉认识;
实现与接口分离,可复用
处理同一继承体系下异质对象集合的强大威力
缺点:
运行期绑定,导致一定程度的运行时开销;
编译器无法对虚函数进行优化
笨重的类继承体系,对接口的修改影响整个类层次;
不同点
本质不同,静态多态在编译期决定,由模板具现完成,而动态多态在运行期决定,由继承、虚函数实现;
动态多态中接口是显式的,以函数签名为中心,多态通过虚函数在运行期实现,静态多台中接口是隐式的,以有效表达式为中心,多态通过模板具现在编译期完成
相同点
都能够实现多态性,静态多态/编译期多态、动态多态/运行期多态;
都能够使接口和实现相分离,一个是模板定义接口,类型参数定义实现,一个是基类虚函数定义接口,继承类负责实现;
 
 
 
待更新!!!
 
可以看看文章:浅谈C++多态性

以上是关于C++多态(上)的主要内容,如果未能解决你的问题,请参考以下文章

C++编译期多态与运行期多态

c++中为啥要用new 函数()实现多态?

C++语言特点

C++中的多态

C++常用的音频工具库

C++常用的音频工具库