【C++】多态学习总结

一. 多态的分类

多态就是调用一个函数时,展现出多种形态。比如买火车票这件事,普通人是全价,学生是半价,这就是一种多态。

多态分为静态的多态和动态的多态:

1. 静态的多态

静态的意思是编译时由编译器决定具体调用哪个函数,函数重载和模板就是一种静态的多态。
在这里插入图片描述

2. 动态的多态

动态指的是运行时才确定到底调用哪个函数。

条件:要同时满足两个条件。

  • 子类继承父类,完成虚函数重写
  • 父类的指针或引用去调用这个重写的虚函数。

效果:调用函数跟对象有关,指向那个对象就调用谁的虚函数。

  • 父类的指针或引用指向父类对象,调用父类的虚函数。
  • 父类的指针或引用指向子类对象,调用子类的虚函数。

二. 多态的相关概念介绍及其实现

我们假设一个场景:Student 继承了 Person 类。Person 类对象买票全价,Student 类对象买票半价。

1. 虚函数

虚函数:被 virtual 修饰的类的成员函数称为虚函数,直接在函数的返回值类型前加上关键字 virtual 即可。
在这里插入图片描述

2. 虚函数的重写(覆盖)

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

在这里插入图片描述

【Note1】 子类中重写的虚函数可以不加 virtual 关键字

在重写基类虚函数时,派生类的虚函数不加 virtual 关键字,这样也可以构成重写,因为在基类中已经确定了该函数是虚函数,自然它的虚函数属性也被派生类继承了下来。但是该种写法不规范,不建议这样使用。
在这里插入图片描述

【Note2】 协变(基类与派生类虚函数返回值类型可以不同)

有个特殊的例外:派生类重写基类虚函数时,与基类虚函数返回值类型可以不同。即基类和派生类的虚函数返回具有继承关系的自己或其它类对象的指针或引用,此时依然可以构成多态,这种情况称为协变。
在这里插入图片描述

【Note3】 建议析构函数也定义成虚函数

当一个基类的指针指向 new 出来的派生类对象时,为了保证 delete 基类指针时能够去调用派生类析构函数(也就是实现多态),最好把析构函数也定义为虚函数,以避免内存泄漏。
在这里插入图片描述
另外虽然基类与派生类析构函数名字不同,这看起来违背了重写的规则,其实不然,最终编译时编译器会对析构函数的名称做特殊处理:把所以对象的析构函数的名称统一处理成 Destructor。

3. 多态的构成条件

在继承中要构成多态还有两个条件

  1. 派生类重写基类的虚函数。
  2. 通过基类的指针或者引用去调用虚函数。

在这里插入图片描述

4. C++11 引入的 override 和 final 关键字

从上面可以看出,C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,比如:函数名字母次序写反,导致无法构成重写,这种错误在编译期间是不会报错的,只有在程序运行时没有得到预期结果后 debug 找出,因此:C++11提供了 override 和 final 两个关键字来帮助程序员检测是否重写正确。

override关键字

这个关键字用来修饰派生类中需要重写的基类的虚函数,强制要求派生类去重写该虚函数。
在这里插入图片描述

final关键字

这个关键字用来修饰基类中不想要被子类重写的虚函数,用来限制不让子类重写该函数。
在这里插入图片描述

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

在这里插入图片描述


三. 抽象类

1. 什么是抽象类?

在认识抽象类之前必须先了解什么是纯虚函数。

纯虚函数:在虚函数声明的最后加上 =0 ,这个函数就叫做纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类)。
在这里插入图片描述
抽象类:包含纯虚函数的类叫做抽象类(也叫接口类)
在这里插入图片描述

2. 为什么要有抽象类?

抽象类特点:不能实例化出对象。

  • 可以更好的去表示现实世界中那些没有实例对象的抽象类型,比如:植物、人、动物、车等等。
  • 体现了接口继承。强制子类去重写基类的虚函数(不重写的话,子类也是抽象类,不能实例化出对象)

在这里插入图片描述

2. 实现继承和接口继承比较

在这里插入图片描述


四. 多态的实现原理

1. 虚函数表

算一算 32 位平台下 sizeof(Base) 等于多少?

在这里插入图片描述

通过计算结果是 8 字节,除了成员变量 _a 的 4 个字节外,还有一个虚函数指针(_vfptr)也是四个字节。

说说这个虚函数指针,它指向的是一个函数指针数组,这个函数指针数组我们叫虚函数表(简称虚表),它的元素是虚函数的地址。
在这里插入图片描述

那派生类也有虚表吗?有的话和基类的虚表是同一个吗?我们看看下面两个对象的数据模型:
在这里插入图片描述

通过上面的对象模型图,我们发现了以下几个值得注意的地方:

  1. 首先派生类对象 d 中也有一个虚表指针,d 对象由两部分构成,一部分是基类继承下来的成员(包括基类的虚表),另一部分是派生类自己的成员。
    在这里插入图片描述

  2. 虽然说是继承了父类的虚表,但不是照搬这么简单。总结一下派生类的虚表生成过程:
    a、先将基类中的虚表内容拷贝一份到派生类虚表中 。
    b、如果派生类重写了基类中某个虚函数,用派生类自己的虚函数地址覆盖虚表中基类虚函数的 地址。
    c、派生类自己定义的虚函数增加到派生类虚表的最后位置(上图中没有看到是vs编译器隐藏了他们)。
    在这里插入图片描述

  3. 既然派生类虚表内容是根据基类虚表深拷贝过来的,那么基类和派生类的虚表指针(_vfptr)所存储的值也就不一样了,它们是两个不同的地址,分别指向两张虚表。
    在这里插入图片描述

  4. Fun3()是派生类自定义的普通函数,对于普通函数不论是派生类自定义出来的还是基类继承下来的,他都不会被放入虚表。

2. 关于虚表的几点补充

补充1:虚表的元素是一个个虚函数的地址,最后放空指针作为结束标志

在这里插入图片描述

补充2:一个类只要有虚函数就一定会有自己的虚表,包括后面它派生出来的类也会深拷贝它的虚表内容

在这里插入图片描述

补充3:同一个类定义出来的对象共用同一张虚表

在这里插入图片描述

问题1:对象中虚表指针在什么阶段初始化?虚函数又在什么阶段生成的呢?

答:虚表指针在初始化列表中初始化(由操作系统帮我们完成),虚函数在编译阶段确定地址。

问题2:虚函数放到虚表里面的,这句话对吗?

答:这句话不准确,虚表里面放的是虚函数地址,虚函数和普通函数一样,编译完成后都放在代码段。

3. 再次理解虚表指针、虚表、虚函数

在这里插入图片描述

4. 多态的实现原理

前面说过要实现多态,有两个条件,一个子类重写基类的虚函数,另一个是要求基类的指针或引用调用虚函数 why?

问题1:为什么子类要重写基类的虚函数?

还是对下面这两个类进行分析
在这里插入图片描述

我们进行下面这个操作,实现多态:
在这里插入图片描述

我们可以看到

  1. 观察下图的红色箭头我们看到,p是指向Person对象p时,p->BuyTicket在对象虚表中找到虚函数是Person::BuyTicket。
  2. 观察下图的蓝色箭头我们看到,p是指向Student对象s时,p->BuyTicke在对象虚表中找到虚函数是Student::BuyTicket。
  3. 满足多态以后的函数调用,不是在编译时确定的,而是运行时到指向的对象中的虚表里找对应的虚函数地址来调用。所以指向父类对象,调用的就是父类的虚函数;指向子类对象,调用的就是子类的虚函数。即构成多态,指向谁就调用谁的虚函数。
  4. 如果不构成多态,那么这里调用的就是编译时编译器确定的调用那个函数,主要是看p的类型来决定。即不构成多态,对象类型是什么就调用哪个函数。
    在这里插入图片描述

问题2:为何要父类的指针或引用去调用虚函数,父类的对象不可以码?

不能,首先我们要明白子类赋值给基类的对象、指针、引用这些都叫做切片,但它们的实现原理是不同的:

  • 对象的切片:子类赋值给基类的对象时只会拷贝(调用基类的拷贝构造)子类中基类那一部分的成员过去,不会把子类的_vfptr拷贝过去,因为虚表是类的所有对象共用的、默认生成的。
  • 引用、指针的切片:子类赋值给基类的引用或指针时,基类直接指向或引用子类中基类的那一部分,当然也包括子类的_vfptr。相当于是一种浅拷贝。

五. 多继承关系的虚函数表

前面我们一直搞的是单继承,现在我们看看多继承(不考虑菱形继承和菱形虚拟继承)

1. 单继承中的虚函数表

派生类自己定义的虚函数在派生类的虚表里不会显示出来,只会显示基类的,这是编译器的监视窗口故意隐藏了这两个函数

我们看下面两个类,通过监视窗口我们观察到派生类的虚表里确实重写了基类的Fun1(),但隐藏了自己定义的虚函数Fun2()。
在这里插入图片描述
我们可以打印虚表来验证编译器确实隐藏了派生类自己定义的虚函数。

思路
取出b、d对象的头4bytes,就是虚表的指针,然后遍历虚函数的地址。前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
在这里插入图片描述

验证结果
可以看到编译器确实是隐藏了派生类自定义的虚函数
在这里插入图片描述

2. 多继承中的虚函数表

依然打印的虚函数表,看看多继承中派生类的虚函数表示什么样。
在这里插入图片描述
既然是多继承,那派生类应该继承了两个虚表。我们分别打印这两个虚表看看。
在这里插入图片描述
打印结果发现,多继承中派生类自己定义的未重写的虚函数放在第一张虚函数表中。
在这里插入图片描述

3. 总结

单继承

  1. 派生类自己定义的会虚函数会加到它的虚函数表的最后。

多继承

  1. 多继承会按照继承基类的顺序在派生类中生成多个虚函数表。
  2. 派生类的自己定义的未重写的虚函数放在第一个继承的基类那部分的虚函数表中。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值