多态及其实现原理

  • 一、多态的概念
    • 概念
    • 构成条件
  • 二、虚函数的重写
    • 重写的定义
    • 重写的特殊情况
    • override和final关键字
    • 区分重写、重载、重定义
    • 抽象类的概念
  • 三、多态的实现原理
    • 父类对象模型
    • 补充:生成默认构造方法的场景
    • 子类对象模型
    • 多态的调用原理
    • 多继承的虚函数表
  • 四、继承与多态中的常见问题

注:编译环境为VS 2022,指针大小为4字节

一、多态的概念

概念

多态,指完成某个行为,不同的对象去完成时会产生出不同的状态。如:定一个一Animal类,类中包含动物的叫声这种方法,分别定义Dog和Cat类继承自动物类,那么Dog和Cat类中也会包含叫声这种方法,但是他们具体实现是不同的,因为每种动物的声音都不相同,这便是一种多态。

多态的分类

  • 静态多态,也称为静态绑定或者早绑定,是指函数在编译期间就已经确定了函数的行为。函数重载、函数模板等都属于静态多态。
  • 动态多态,即动态绑定或者晚绑定,指程序在运行时才可以确定函数的行为。本文主要分析的是动态多态。

构成条件

  • 在继承体系下,父类中包含虚函数
  • 子类中对父类的虚函数进行重写
  • 通过父类的指针或者引用调用虚函数

多态的体现:不同的类对象调用同一函数,会产生不同的行为

二、虚函数的重写

重写的定义

虚函数:virtual关键字修饰的函数

子类中有一个跟父类完全相同的虚函数,即返回值类型函数名形参列表都完全相同,则可以说子类重写了父类的虚函数。
Student类中重写了BuyTicket方法:

注意:只要父类中函数用virtual修饰即可,子类可以不加,且虚函数的重写与权限无关。

重写的特殊情况

  • 协变——返回值类型不同
    父类的虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用。
  • 析构函数重写——父类与子类析构函数名字不同
    如果父类的析构函数为虚函数,子类的析构函数只要定义了,都能与父类的析构函数构成重写。可以理解为编译器对析构函数的名字做了特殊处理,编译后析构函数的名字统一处理成destructor。

override和final关键字

这两个关键字的主要作用都是帮助用户检测是否构成重写

  • final
    修饰虚函数,表示虚函数不可被重写;另外final也可以修饰类,表示该类不能被继承
  • override
    修饰虚函数,检查子类虚函数是否重写了父类的虚函数,如果没有构成重写则会报错

区分重写、重载、重定义

抽象类的概念

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也称接口类),抽象类不能实例化对象。抽象类被集成以后如果没有对虚函数进行重写,则继承的类也是抽象类。一般情况下,抽象类必须被继承,且必须对虚函数进行重写,否则定义为抽象类则没有实际意义。
Shape类:

class Shape
{public:// 纯虚函数virtual double GetArea() = 0;virtual double GetCircumference() = 0;
};

三、多态的实现原理

父类对象模型

给出一个Base类,一个Derived类继承Base类

class Base
{public:virtual void Func1(){cout << "Base::Fun1c()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}virtual void Func3(){cout << "Base::Func3()" << endl;}
public:int  _a;
};
class Derived:public Base
{public:int _b;
};

父类对象模型:

总结:

  • 类中定义了虚函数以后,定义对象时,编译器会为对象创建一张虚表,并将一个指向这张虚表的指针保存在对象的前四个字节,无论定义几个虚函数,对象都只比多四个字节大小。这个指针称为函数虚表指针
  • 虚表地址是在构造对象时进行填充的,构造函数如果显式实现,编译器会对用户实现的函数进行修改,增加给对象前四个字节存放虚表地址的语句。
  • 虚表本质就是一个函数指针数组,按照声明顺序依次存放虚函数的地址

补充:生成默认构造方法的场景

在学习类与对象时我们知道构造函数是是类的默认成员函数,如果用户没有显式定义,编译器会默认生成,但是实际上并不是在所有情况下编译器都会生成默认的构造函数,编译器只会在需要的时候生成构造函数
四种生成默认构造方法的场景

  • B类中包含有A类的对象,B类没有显式定义构造函数,A类定义了无参或者全缺省的构造方法,则编译器会给B类生成默认的构造方法。
    分析:因为A类有无参或者全缺省的构造方法,需要在B类中调用A类的构造方法对A类成员进行初始化,所以需要生成B类的构造方法,在其初始化列表中调用A类构造方法。
    如果A类没有显式定义构造函数,则不会生成B类构造方法,默认赋随机值;如果A类定义的构造方法不是无参或者全缺省的,则需要在初始化列表中对A类对象初始化:

  • 继承中,B继承A,A中定义了无参或者全缺省的构造方法,B未显式定义,则编译器会给B类生成默认的构造方法。将B中继承自A的部分初始化。

  • 虚拟继承中,B类虚拟继承子A类,B类未显式定义构造方法,编译器会给B类生成默认的构造方法,目的是:给B类对象的前4个字节填充虚基表地址

  • 类中包含虚函数,未显示定义构造方法,则编译器会自动生成构造方法,为对象的前4个字节填充虚表地址

子类对象模型

子类虚表构建规则

  1. 将父类虚表内容拷贝一份放到子类虚表中,注意父类和子类用的不是同一张虚表,仍以上面的Base和Derived类为例

    可以看出,两个虚表指针的地址不同,但虚表中保存的虚函数的地址都相同
  2. 如果子类中将父类的虚函数进行了重写,则用子类的虚函数地址替换虚函数表中相同偏移量的虚函数的地址。
  3. 子类中增加的虚函数按照其在类当中的声明次序放在虚表的最后
    子类中增加了两个虚函数:

    但是由于VS监视窗口中无法显式新增加的子类,而内存窗口只能显式虚函数的地址,无法确认是哪个函数,所以这里通过打印的方式进行验证。

通过上图中程序的方式打印出了子类对象中虚函数的分布情况,在这里VFP是一个函数指针类型,前面加typedef表示为函数指针类型,如果不加,则是函数指针变量。
所以是用VFP*接收指向第一个虚函数指针的指针,p与*p的类型:

所以最终的结论是:子类新增的虚函数按照其在类中的声明次序放在虚函数表的最后。

子类对象的构造过程
构造子类对象时,在初始化列表中先调用父类的构造函数,此时对象的前4个字节保存的虚表指针指向父类的虚表,之后构造子类自己的虚表,虚表再指针指向子类的虚表。

总结

  1. 虚表的本质是函数指针数组,在编译时生成
  2. 虚函数的重写也叫覆盖,指的是虚表中虚函数的覆盖,重写是语法层的叫法,覆盖是原理层的叫法
  3. 对象中保存的是虚表指针,虚表中保存的是虚函数指针,虚函数和普通函数一样保存在代码段,在VS中虚表也保存在代码区
  4. 同一个类的对象共用同一张虚表,父类和子类各自拥有各自的虚表。

多态的调用原理

父类对象,函数调用时的汇编代码:

普通函数调用时直接传递函数的地址,这个地址在编译期间就确定了,虚函数则要经过虚表指针寻址等步骤。从上面的汇编代码也可以看出动态多态的晚绑定的特点,在编译期间普通函数的调用已经确定了要调用的具体函数,虚函数则无法确定,只有等程序运行起来,形参b是具体哪个对象确定了以后,才能确定要调用的函数的地址
上面是传递父类对象时的调用情况,子类对象调用时的汇编代码与父类对象相同,区别就是子类对象有自己的虚表,最终调用的是子类需表中的函数。
总结多态的原理:
创建对象时,编译器会给包含虚函数的类对象创建一张虚表,并将虚表地址填充在对象的前4个字节,子类对象会拷贝父类对象的虚表,然后再对自己重写的虚函数进行替换,并在虚表中添加子类新增的虚函数;函数调用时,编译器会先从对象的前4个字节获取该对象虚表的地址,然后在虚表中获取虚函数地址进行函数调用;由于每个类对象都有属于该类的一张虚表,且虚函数一般都进行了重写,即函数名与父类相同,但函数执行的内容不同,最终产生的结果就是,不同类的对象调用同一函数产生不同的结果,由此形成了多态。

多继承的虚函数表

给出两个父类Base1和Base2,Derived子类继承自两个父类

通多监视窗口查看子类对象的模型:

多态中多继承的子类对象模型与多继承的模型原理相同,但是VS的监视窗口无法查看子类新增的虚函数在需表中的位置,按照之前但继承中打印虚表中函数的原理进行打印:

最终得到的结果:

可以看出,子类中增加的虚函数保存在上面的虚表中。
多继承子类对象模型及对象虚表:

四、继承与多态中的常见问题

  1. 析构函数可以设置为虚函数吗?
    可以,在继承体系中,最好将父类的析构函数设置为虚函数;如果子类中涉及到资源管理,则必须将父类的析构函数设置为虚函数,这样父类和子类中的析构函数便会构成重写(重写的特殊情况),形成多态,通过父类指针指向子类对象时,delete父类对象的指针也会调用子类的析构函数
    子类中涉及资源管理,调用父类析构函数析构子类对象,则会有内存泄漏,如图:

  2. 构造函数可以设置为虚函数吗?
    不能,虚函数是放在虚表中的,虚表指针是在构造方法的初始化列表中进行填充的,通过虚表指针才能找到虚函数,但是不调用构造方法就没有虚表指针,二者矛盾。即如果构造方法是虚函数,那么调用构造方法就要通过虚表指针,但是虚表指针是要通过调用构造方法才能填充的

    C++中的多态——理解虚函数表及多态实现原理相关推荐

    1. 初入c++(六)虚函数实现多态,虚析构函数,虚函数表和多态实现机制,纯虚函数。

      1.c++多态的概念以及用途. 1.1虚函数实现多态 通过基类指针只能够访问派生类的成员变量,不能够访问派生类的成员函数. 解决问题的办法:使用虚函数(virtual function),只需要在函数 ...

    2. C 虚函数表及多态内部原理详解

      C 中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态" ...

    3. C++多态之虚函数表详解及代码示例

      引言 C++相对其他面向对象语言来说,之所以灵活.高效.很大程度的占比在于其多态技术和模板技术.C++虚函数表是支撑C++多态的重要技术,它是C++动态绑定技术的核心. 如果对多态还不了解的小伙伴,可 ...

    4. C++虚函数表和多态

      这个视频上解释的很好,C++在调用可重写的虚函数时,通过访问虚函数表来进行,这个UP主通过解释编译后的代码把多态分析的很清楚. https://www.bilibili.com/video/BV15g ...

    5. C++多态:多态实现原理剖析,虚函数表,评价多态,常见问答与实战【C++多态】(55)

      虚函数表 一般继承(无虚函数覆写) 一般继承( 有虚函数覆写) 静态代码发生了什么 评价多态 常见问答与实战 问答 为什么虚函数必须是类的成员函数? 为什么类的静态成员函数不能为虚函数? 为什么构造函 ...

    6. C++多态的原理(虚函数指针和虚函数表)

      C++多态的原理 (虚函数指针和虚函数表) 1.虚函数指针和虚函数表 2.继承中的虚函数表 2.1单继承中的虚函数表 2.2多继承中的虚函数表 3.多态的原理 4.总结 1.虚函数指针和虚函数表 以下 ...

    7. C++类的虚函数表和虚函数在内存中的位置

      C++类的虚函数表和虚函数在内存中的位置 C++类的虚函数表和虚函数在内存中的位置 虚函数表和虚函数在内存中的位置说明 参考 C++类的虚函数表和虚函数在内存中的位置 虚函数表指针是虚函数表所在位置的 ...

    8. 图解C++虚函数 虚函数表

      图解C++虚函数 2016年07月02日 17:47:17 海枫 阅读数:5181 标签: 虚函数c++g++对象模型C++虚函数更多 个人分类: C/C++/linux 版权声明:本文为博主原创文章 ...

    9. C++ COM编程之接口背后的虚函数表

      前言 学习C++的人,肯定都知道多态机制:多态就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.对于多态机制是如何实现的,你有没有想过呢?而COM中的接口就将这一机制运用 ...

    最新文章

    1. iPhone全球技术巡讲北京站,WWDRChina 2009 Beijing
    2. 产品经理经验谈:从产品经理、用户、产品的角度重新认知产品运营
    3. 重磅发布!阿里云云效《阿里巴巴DevOps实践指南》
    4. VBS基础篇 - Dictionary对象
    5. DB2 多表空间 重定向 还原
    6. 运行中的linux备份系统盘,怎样使用ghost对linux进行系统备份?
    7. SLAM方向国内有哪些优秀公司?
    8. 飞盘比赛(入门oj Problem 5961)
    9. 腾讯首款区块链AR游戏上线《一起来捉妖》,风物志里的奇珍异兽
    10. html5文字云在线制作,一键生成高大上的文字云,这5个工具值得推荐。
    11. Python QT5文件对话框总是错误代码-1073740791 (0xC0000409)
    12. Web与排版学上的字体问题【转】
    13. Oracle 数据库学习
    14. TOPMOST窗口属性失效的一种场景
    15. CDOJ 1144 Big Brother 二分图匹配
    16. CFS调度器负载计算
    17. 推荐一款优秀的简历模板
    18. Ajax和Git-自我总结
    19. ST北生(600556)关于资产重组进展情况的公告
    20. Java常用到的6个加密技术,先收藏,总会用得到

    热门文章

    1. python名词解释总结
    2. 【深度优先搜索-中等】1034. 边界着色
    3. Java编程性能调优-01|字符串性能优化不容小觑,百M内存轻松存储几十G数据
    4. JavaScript-初识ajax、ajax封装、及json对象使用(上)
    5. 自学方法|明确学习的出发点【可能的阶梯】
    6. 我觉得做运营月薪8000比做程序员月薪10000+好多了
    7. 业内人士透露:百联与阿里“联姻”,三个没想到!
    8. 统计学三大相关系数之肯德尔(kendall)相关性系数
    9. 如何安装vuecli3
    10. 常用的几款富文本编辑器