大家好,我是苏貝,本篇博客带大家了解C++的多态,如果你觉得我写的还不错的话,可以给我一个赞👍吗,感谢❤️
目录
- [4. 多态的原理](#4. 多态的原理)
-
- [4.1 虚函数表](#4.1 虚函数表)
- [4.2 多态的原理](#4.2 多态的原理)
- [4.3 动态绑定与静态绑定](#4.3 动态绑定与静态绑定)
- [5. 单继承和多继承关系的虚函数表](#5. 单继承和多继承关系的虚函数表)
-
- [5.1 单继承中的虚函数表](#5.1 单继承中的虚函数表)
- [5.2 多继承中的虚函数表](#5.2 多继承中的虚函数表)
- [5.3 菱形继承/菱形虚拟继承(了解)](#5.3 菱形继承/菱形虚拟继承(了解))
4. 多态的原理
4.1 虚函数表
问:下面代码的结果是什么?
答案是8,为什么?类Base里只有1个int类型的变量,它占4个字节,结果是8,那说明还有4个字节是因为虚函数存在的,是什么?看下图
通过观察测试我们发现b对象中除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分
虚函数表里存放的是虚函数的地址
我们再来多一些虚函数
通过观察和测试,我们发现了以下几点问题:
1、 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,父类的虚表指针也继承了,另一部分是自己的成员
2、 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3、 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表
4、 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
总结一下派生类的虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
上面说的都是不同类的对象的虚函数表,那如果是同类的对象,它们的虚函数表是什么情况呢?
同类的对象共用一个虚函数表
4.2 多态的原理
上面分析这个半天了,那么多态的原理到底是什么?还记得下面的Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket
1、 观察上图的红色箭头我们看到,p是指向pt对象时,p->BuyTicket在pt的虚表中找到的虚函数是Person::BuyTicket。
2、 观察上图的蓝色箭头我们看到,p是指向st对象时,p->BuyTicket在st的虚表中找到的虚函数是Student::BuyTicket。
3、 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
满足多态以后的函数调用需要的函数地址,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时需要的函数地址是编译时确认好的。
现在我们来打印一下对象d的虚表
根据上面的学习,我们知道,虚表是一个函数指针数组,
但是按照上图的函数指针数组的格式就太麻烦了,因此我们typedef一下
再写一个打印虚表的函数
最后只需要将虚表指针传进Print函数,即需要将d的第一个4字节的内容(虚表指针)作为参数
我们可以将d强转成int类型的吗?不能,因为只有相关联的类型才能相互转换
![](https://i-blog.csdnimg.cn/direct/fd6dcf7a02ed47ffafe24a68538f3cc8.png)
所以我们先&d,再取出前4个字节的地址,即(int*)&d,再得到前4个字节地址的内容,即*((int*)&d),最后在将它强转成VFPTR*即可
![](https://i-blog.csdnimg.cn/direct/241ff24b4bbc47e5b0625655e3e5ec03.png)
我们还想知道这3个函数地址分别对应哪个函数怎么办?调用对应函数
![](https://i-blog.csdnimg.cn/direct/edbfe740252e4f1dab50600521621336.png)
![](https://i-blog.csdnimg.cn/direct/a8e9b9c831f24529a801d89dd85728e9.png)
这里还有一个很容易混淆的问题:虚函数存在于哪个区域? 虚表存在于哪个区域?
答:虚表存的是虚函数指针,不是虚函数,虚函数和普通成员函数一样的,都是存在于代码段(即常量区)的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下也是存在于代码段的
通过结果我们发现,A和B的虚表地址离位于常量区的"hhhhh"更近,所以大致能证明虚表存在于常量区(即代码段)
4.3 动态绑定与静态绑定
1、 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
2、 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
5. 单继承和多继承关系的虚函数表
需要注意的是在单继承和多继承关系中的虚函数表,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的
5.1 单继承中的虚函数表
![](https://i-blog.csdnimg.cn/direct/c9a6df847a4f4ad18c8ef9f328d0ff67.png)
观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?
![](https://i-blog.csdnimg.cn/direct/8f459013f582457b89b3d307e3152fcb.png)
1、 打开内存窗口
2、 使用代码打印出虚表中的函数。(在11.4的第2点多态的原理的最后有详细介绍)
结论:
如果子类有虚函数,继承的父类有虚函数表指针,那就将子类的虚函数地址放到第一个有虚函数表指针的父类的虚函数表中。
如果继承的父类没有虚函数表指针,那就子类自己创建一个虚函数表并存储虚表的地址
5.2 多继承中的虚函数表
![](https://i-blog.csdnimg.cn/direct/3634f90cd80448ce95c56de9f6399a7d.png)
问:上面程序的答案是什么?
答案:20,为什么?
![](https://i-blog.csdnimg.cn/direct/2776a3909bb7417ebdb564bd6be98637.png)
Derive本身没有虚表指针吗?没有,因为它继承的基类有虚函数表,所以Derive将func3的函数地址放到了第一个虚表指针所指向的虚表中
下面通过打印虚函数表来证明
如果出现上图的情况,那就先清理解决方案,再重新生成解决方案
再运行程序就正常了
观察上图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
5.3 菱形继承/菱形虚拟继承(了解)
1、菱形继承:
![](https://i-blog.csdnimg.cn/direct/6c674a53ed33453cbd04ecbe118f4313.png)
菱形继承的对象模型和多继承相似,如果派生类有不是继承的虚函数A,如果继承的基类有虚表指针,那么将虚函数A放到第一个有虚表指针的基类的虚表中。
![](https://i-blog.csdnimg.cn/direct/cc06fa56443d4514a57d53963cda9781.png)
2、菱形虚拟继承
虚基表:在菱形继承那部分有讲到
好了,那么本篇博客就到此结束了,如果你觉得本篇博客对你有些帮助,可以给个大大的赞👍吗,感谢看到这里,我们下篇博客见❤️