前言

虽然不建议多继承,但有些场景还是很适合多继承的。同时,这部分知识虽然实际不会用到,但是我们也可以从中学习到语言的强大,C++作为当时的老大哥,当然因为前沿所以有一大堆坑,我们也可以从C++之父解决这些问题中学习到一些思想
那么,就开始今天的学习

文章目录

  • 前言
  • 一. 多继承的虚函数表
  • 二. 重写的虚函数
  • 三. 动静态绑定
    • 1. 静态绑定
    • 2. 动态绑定
  • 四. 菱形继承的虚表和虚基表
  • 结束语

一. 多继承的虚函数表

我们先构建一个多继承模型

有一个Base1父类和Base2父类,两个父类都有fun1和fun2两个虚函数
Derive公有继承这两个类,但Derive只重写fun1,并且自己新增一个虚函数fun3
问:Derive有几个虚表?新增的虚函数放在哪?

class Base1
{public:virtual void func1(){cout << "Base1::func1()" << endl;}virtual void func2(){cout << "Base1::func2()" << endl;}private:int _b1;
};class Base2
{public:virtual void func1(){cout << "Base2::func1()" << endl;}virtual void func2(){cout << "Base2::func2()" << endl;}private:int _b2;
};class Derive :public Base1,public Base2
{public:virtual void func1(){cout << "Derive::func1()" << endl;}virtual void func3(){cout << "Derive::func3()" << endl;}private:int _d1;
};

首先我们通过监视窗口可以得到第一个问题的答案

我们看到,Derive内部是有两个虚表的,一个继承Base1,一个继承Base2。所有Derive共有两张虚表,但是新增的func3我们还是无法通过监视窗口看到,这时我们同样可以使用上篇博客多态结论证明中使用的打印虚表的函数

typedef void(*VF_PTR)();
void Print(VF_PTR table[])
{for (int i = 0; table[i]; i++){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}

这里不再解释,感兴趣的可以查看上篇博客。

但是迎面而来的是一个问题,打印第一张虚表,我们可以直接获取对象的前四个字节,但是第二张虚表呢?是不是需要偏移呢?偏移多少呢?怎么偏印呢?

第一张虚表我们可以直接访问前四个字节得到虚表指针

Print((VF_PTR*)(*(int*)(&d)));

第二章虚表,我们就需要偏移指针获得了,偏移多少呢?偏移一个Base1的大小,注意我们需要先强转成char*,这样访问范围是1字节,加上Base1的大小才是正确的偏移量

Print((VF_PTR*)(*(int*)(  (char*)&d  +sizeof(Base1) )));


我们看到,新增的虚函数,是加到第一张虚表的后面的

另外,我们除了手动让指针偏移以外,我们还可以这样操作。父类指针指向子类,会进行切片,会自动指向子类的父类部分,这样就自动实现指针的偏移了。

二. 重写的虚函数

但是仔细观察一下,我们会发现一个问题

这里我们通过虚表打印出了函数指针指向的虚函数地址,同时取出函数指针,并调用。这里func1调用的也是子类重写的虚函数,但是这两个虚函数的地址却不一样
这是否代表有两份重写的func1虚函数呢?
答案是,没有,只有一份
以下通过汇编角度解释


从这两次调用func1的汇编指令,我们发现,最终都会跳转到同一个地址的函数,所以重写的func1函数确实只有一份,但为什么func2的地址不一样?又为什么中间还多一步的跳转呢?

我们观察得到,Base2的汇编跳转,是为了多执行一个指令

ecx是this指针的寄存器
这一步汇编指令的作用是将当前的this指针往上移动8个字节。
因为满足了多态,父类指针指向子类对象,但调用的是子类的函数,所以this指针要指向子类对象的首地址,但是父类指针会发生切片,指针指向的是子类的父类部分,所以要偏转回到子类对象的首地址。
那为什么Base1不需要偏转回去呢?这是因为Base1是先继承的,其地址就在子类的第一部分,所以Base1的父类指针刚好指向子类的首地址。

三. 动静态绑定

1. 静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。比如:函数重载
函数模板本质也是函数重载,为什么函数模板不能分离编译,就是因为需要在编译时确定参数类型,不然不会生成函数的地址,运行时就会产生链接错误

2. 动态绑定

动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

四. 菱形继承的虚表和虚基表

此部分为补充知识,可以不掌握,因为菱形继承本来就不建议实现,在此基础上的多态更是不会使用,这样的模型也复杂,所以可以不掌握。

我们构建这样一些类


A里面有一个虚函数,B和C虚继承A,重写A的虚函数,同时还有自己新增的虚函数,D再继承B和C

class A
{public:virtual void func1(){}
public:int _a;
};class B : virtual public A
{public:virtual void func1(){}virtual void func2(){}
public:int _b;
};class C : virtual public A
{public:virtual void func1(){}virtual void func3(){}
public:int _c;
};class D : public B, public C
{public:virtual void func1(){}
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

根据菱形虚拟继承的知识,为了解决数据冗余和二义性,所以在B和C中会创建虚基表,存储偏移量,同时因为虚继承,多态的实现,A,B,C,D中都有虚表
接下来,我们通过内存窗口查看 d 对象的具体存储结构

A,B,C的第一个地址都是虚表指针
B,C的第二个地址是虚基表指针

因为B和C同时重写了A的虚函数,所以B和C各自原本的虚表应该是自己的虚函数的函数指针,但因为都虚继承的A的虚函数,所以函数指针并不是在B和C各自的虚表,A的内容会被放在对象的最下面,变为公共部分,所以只能有一个重写的虚函数,这时候用哪个都不合适,所以此时要求D必须也重写A的虚函数

所以此时,B和C的虚表内,其实只有自己新增的虚函数的函数指针

而B和C的虚基表是这样的

第一个是ff ff ff fc 代表-4,也是偏移量,刚好移到虚表的位置
第二个偏移量,是偏移到A这个公共部分的。

结束语

本篇文章是对多态的一些比较偏的知识的笔记,掌握要求不深,感谢您的阅读

如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。

【C++】多继承的多态相关推荐

  1. Go 学习笔记(36)— 基于Go方法的面向对象(封装、继承、多态)

    Go 面向对象编程的三大特性:封装.继承和多态. 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式 继承:使得子类具有父类的属性和方法或者重新定义.追加属性和方法等 多态:不同对象中同种行为的不 ...

  2. Python中的继承和多态

    本文以生活中的基础现象为切入点,主要介绍了Python基础中继承和多态,包括单继承.多继承的语法.多态常见的 "鸭子类型". 以及如何重写父类的方法都做了详细的讲解. 一.继承的介 ...

  3. python 继承和多态

    python 继承和多态 在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类.父类或超类(Ba ...

  4. java 封装 继承 堕胎_JAVA封装、继承、多态

    封装 1.概念: 将类的某些信息隐藏在类的内部,不允许外部程序访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问. 2.好处: a.只能通过规定的方法访问数据 b.隐藏类的实例细节,方便修改和实 ...

  5. Python面对对象编程——结合面试谈谈封装、继承、多态,相关习题详解

    1.面向对象的三大特征 封装:属性和方法放到类内部,通过对象访问属性或者方法,隐藏功能的实现细节.当然还可以设置访问权限; 继承:子类需要复用父类里面的属性或者方法,当然子类还可以提供自己的属性和方法 ...

  6. Java基础-OOP特性之封装、继承、多态、抽象

    为什么80%的码农都做不了架构师?>>>    //要习惯将程序(功能.代码)模块化 //OOP四大特性:封装.继承.多态.抽象 //OOP的核心:封装一个类,把数据抽象出来,再把方 ...

  7. 第10章 接口、继承与多态----抽象类和接口

    一.抽象类 在解决实际问题时,一般将父类定义为抽象类,需要使用这个父类进行继承与多态处理.回想继承和多态原理,继承树中越是在上方的类越抽象,如:鸽子类继承鸟类.鸟类继承动物类等.在多态机制中,并不需要 ...

  8. Java进阶篇(一)——接口、继承与多态

    前几篇是Java的入门篇,主要是了解一下Java语言的相关知识,从本篇开始是Java的进阶篇,这部分内容可以帮助大家用Java开发一些小型应用程序,或者一些小游戏等等. 本篇的主题是接口.继承与多态, ...

  9. java继承和多态的实验报告_JAVA,继承和多态实验报告

    实验项目名称 : 继承和多态 ( 所属课程 : Java 语言程序设计 ) 院 系: 专业班级: 姓 名: 学号: 实验地点: 指导老师: 本实验项目成绩: 教师签字: 日期: 1.实验目的 (1)掌 ...

  10. 封装 继承 多态_Java基础知识——封装、继承、多态

    1 基本概括 2 详解 封装: 是将类的某些信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问 封装的优点:只能通过规定的方式来访问数据:隐藏类的实现细节: ...

最新文章

  1. 人脸识别种族偏见:黑黄错误率比白人高100倍 | 美官方机构横评189种算法
  2. (转)Linux grep
  3. PL/SQL 使用文档——表注释、显示乱码
  4. 如何计算像素当量_基于非线性标定的桥梁裂缝精确视频测量技术研究
  5. Docker 解决容器时间与主机时间不一致的问题三种解决方案
  6. 前端学习(1985)vue之电商管理系统电商系统之本地分支放到git上面保存
  7. java程序员经常使用的Intellij Idea插件
  8. SVN下载安装与使用
  9. pypyodbc 连接Access数据库常见报错整理
  10. html浮动垂直居中对齐,css如何设置垂直居中对齐?
  11. 解决Windows10 14393版本迅雷崩溃问题
  12. 同步时序逻辑电路分析——数电第六章学习
  13. EndnoteX9简介及基本教程使用说明
  14. 禁止Win系统自动唤醒
  15. Combining Deep Learning with Information Retrieval to Localize Buggy Files for Bug Reports
  16. C# 实现视频监控系统(附源码)
  17. 大数据告诉你中老年人上网爱干什么
  18. 火狐查看html页面大小,利用火狐浏览器测试自适应网页
  19. rust 格式化输出
  20. JS含有部分相同属性的两个对象快速赋值法

热门文章

  1. 工程师、程序员、码农有什么区别?
  2. ERP-非财务人员的财务培训教(二)------如何评价公司/部门经营业绩收藏
  3. 计算机excel二级试题及答案,2016年计算机二级excel题目及答案
  4. 微带线和带状线的区别
  5. 32岁的我裸辞了,八年Java老鸟,只因薪水被应届生倒挂,在闭关三个月后拿到阿里Offer,定级P7
  6. restlet_JAX-RS 的一个列子
  7. 【Python开发】FastAPI 07:Depends 依赖注入
  8. 智慧城市总体解决方案和建设思路
  9. 图像处理中的模糊算法
  10. 软件项目管理:项目调研