之前的文章中我们介绍了多态的原理以及虚表的知识,本文中我们将继续对多态进行更加深入的学习。

单继承中的虚函数表

虚函数表(虚表)的本质是一个函数指针数组,虚函数的地址会进入虚表。首先我们来看一下这样的一段代码:

class Base {public:Base(): _b(10) {++_b;}virtual void Func1() {cout << "Base::Func1()" << endl;}virtual void Func2() {cout << "Base::Func2()" << endl;}void Func3() {cout << "Base::Func3()" << endl;}private:int _b = 1;
};class Derive : public Base {public:virtual void Func1() {cout << "Derive::Func1()" << endl;}virtual void Func4() {cout << "Derive::Func4()" << endl;}private:int _d = 2;
};int main() {Base b;Derive d;return 0;
}

在这段代码中Derive公有继承了Base,在Derive中虚函数重写了Func1函数,又写了一个Func4函数,然后我们对这段代码进行进行调试,发现了一个很神奇的现象,虽然Func4也是虚函数,但是它并没有显示在调试窗口里面,这是就会怀疑,这个表并没有展示完全,再通过内存窗口进行查看会发现有三个很相近的地址,这很有可能就是在Derive中的三个虚函数。下面我们便试图将虚函数表中的虚函数进行打印,这样可以更好的进行观察。(在vs系列的编译器中,编译器会在虚表的末尾添加上nullptr,g++没有)

vs下虚表的打印

编写一个打印虚表的函数得到的结果可以看出我们的猜测没有错误,在内存中的确实是Derive的虚表:

下面就是打印虚表使用的函数:

// 用程序打印虚表
typedef void(*VF_PTR)(); // 声明一个函数指针,函数指针与一般的typedef不同,不能够写成这样的形式 typedef void(*)() VF_PTR//void PrintVFTable(VF_PTR table[])
void PrintVFTable(VF_PTR* table){for (int i = 0; table[i] != nullptr; ++i){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}int main(){Base b;Derive d;// 有两种方式:// 第一种方式有缺陷只能在32位的系统下使用,若是转换到了64位的系统就需要使用一种占据八个字节的指针// 第二种方式就是直接使用*(VF_PTR**)的方法//PrintVFTable((VF_PTR*)*(int*)&b);//PrintVFTable((VF_PTR*)*(int*)&d);PrintVFTable(*(VF_PTR**)&b);PrintVFTable(*(VF_PTR**)&d);return 0;
}

其余的一些问题

从上述中会有这样的问题:虚表是在什么时候生成? -- 编译;对象中的虚表指针是在什么时候初始化的? -- 构造函数的初始化列表;这从下图中即可看出走出构造函数之后虚表指针就有了值。

虚表存在哪里?下面我们来打印几个地址来进行判断:可以推断出虚表是存放在常量区的

动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

多继承中的虚函数表

下面我们再来看一段代码:

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;
};int main(){Derive d;return 0;
}

在上述的代码中我们重写了func1,添加了虚函数func3,进行调试会发现:Derive类中的func3函数也是虚函数但是与之前一样并没有不能在内存窗口中查看到,那么它到底存储在哪里?是在base1的虚表中,还是base2的虚表中,又或者是两张虚表中都会存在?于是我们便可以使用前面打印虚表的方法来进行查看。于是编写函数进行打印,可以看到虚函数func3存放在了第一张虚表中:

Derive d;
PrintVFTable((VF_PTR*)(*(int*)&d));
PrintVFTable((VF_PTR*)(*(int*)((char*)&d+sizeof(Base1)))); // 这里先要将&dDerive*类型的地址转换为char*的地址才能加上Base1的大小,在与之前一样转换为int*类型再解引用转换成VF_PTR*类型即可
//Base2* ptr2 = &d; // 简便的方法是我们可以对Derive类进行切片获得的自然就是Base2的地址。
//PrintVFTable((VF_PTR*)(*(int*)(ptr2)));

func1函数已经进行了多态的重写,但是在两张虚表中的地址却不相同,这点让人感到非常的奇怪。

下面我们来从汇编的角度来看一下为什么:可以看到Base1的指针直接就指向了func1函数,而Base2的指针需要转好几层才能指向func1函数

这里有一句关键指令sub ecx,8,ecx中一般存放的是this指针,这里做的就是修正this指针的位置,这里减少的8就是Base1的大小。再结合上述的信息就可以得到因为在继承中先继承的是Base1,后继承的是Base2,ptr1调用的时候直接就是虚表的地址,而ptr2调用的时候前面还有一个Base1大小的空间因此无法直接访问,需要先对this指针进行相关处理。下面我们将base1的大小进行修改,再次查看:当对Base1添加一个int类型的成员,我们会看到ecx后面跟的数变成了0Ch也就是12,即虚表指针加上两个int对象的大小。

如果继承的顺序发生了改变,同样修正的对象也会发生改变:

菱形继承中虚拟继承的问题

菱形虚拟继承过于复杂 ,这里不过多的讲解,在这说明之前我们在菱形虚拟继承中留下的一个问题,之前虚基表中的偏移量的第一位全是0,但是在这里可以发现并不是0,而变成了fcffffff,这个数转换成十进制就是-4,可以理解为到首地址的偏移量或者到虚标的地址的偏移量。

C++ - 多态(2) | 虚表的打印、单继承与多继承的虚表相关推荐

  1. 单继承与多继承的虚函数表

    我们大部分时候使用的多态虚表实在public继承的基础上实现的 这句话可以怎么说了? 自己简单的思考逻辑认为基类中的成员或者成员函数子类中都会重新拷贝一份,也就是如果派生类中没有定义基类的某虚函数重写 ...

  2. C++对象模型:单继承,多继承,虚继承

    什么是对象模型 有两个概念可以解释C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各种支持的底层实现机制. 类中成员分类 数据成员分为静态和非静态,成员函数有静态非静态以及虚函数 cla ...

  3. python中的单继承,多继承和mro顺序

    python作为一门动态语言,是和c++一样支持面向对象编程的.相对对象编程有三大特性,分别是继承,封装和多态.今天我们重点讲解的是,python语言中的单继承和多继承. 继承概念: 如果一个类继承了 ...

  4. iOS严谨单例写法/可继承单例

    单例模式在iOS开发中可能算是最常用的模式之一了,但是由于OC本身的语言特性,想要写一个正确的单例模式相对来说比较麻烦. 今天就来说一说, 单例创建的方式和严谨的单例写法及可继承单例编写. 基本单例的 ...

  5. day25 python学习 继承,钻石继承 多态

    ---恢复内容开始--- 通过一个列子认识父类和子类中,子类的如何实现对父类默认属性调用,同时拥有自己的属性,如何在子类中调用父类的方法,class Ainmal:country='afdas'def ...

  6. Python 在子类中调用父类方法详解(单继承、多层继承、多重继承)

    Python 在子类中调用父类方法详解(单继承.多层继承.多重继承)   by:授客 QQ:1033553122   测试环境: win7 64位 Python版本:Python 3.3.5 代码实践 ...

  7. 9-2:C++多态之纯虚函数和抽象类以及接口继承和实现继承

    文章目录 (1)纯虚函数和抽象类的概念 (2)抽象类的意义 (3)接口继承与实现继承 (1)纯虚函数和抽象类的概念 如果一个类的虚函数后面写上=0,同时不写它的实现,那么这样的虚函数称之为纯虚函数,包 ...

  8. Python OOP:继承、单继承、多继承、__mro__、子类重写父类同名属性和方法、子类调用父类同名属性和方法、多层继承、super()、私有(实例)属性和方法、获取修改私有属性值、私有类属性

    一.继承 Python⾯向对象的继承指的是多个类之间的所属关系,即⼦类默认继承⽗类的所有属性和⽅法. 继承作用:继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展. 在P ...

  9. python_day6_面向对象的介绍/构造函数/类变量和实例变量/析构函数/私有属性和私有方法/继承、多继承和继承实例/多态

    python_面向对象的介绍/构造函数/类变量和实例变量/析构函数/私有属性和私有方法/继承.多继承和继承实例/多态 在这里得感谢,老师Alex金角大王(路飞学城IT) Python(给兄弟们挂个

最新文章

  1. vue国际化-vue-i18n的配置
  2. 为什么plotly被称为“有史以来最牛逼”可视化神器?
  3. python里none什么意思_python中stream=None什么意思?
  4. 【Henu ACM Round#17 D】Hexagons!
  5. Android 原生通知Notification 写法
  6. 【剑指offer】数字在排序数组中出现的次数
  7. 计算机大一笔试题,大学计算机基础(大一) 笔试题库
  8. Java实现黑客帝国代码雨(待机屏保)
  9. php怎么自动识别车牌号,车牌号自动识别系统怎么录入,很多你不知道的潜规则...
  10. 贪心算法 --- 例题2.哈夫曼编码问题
  11. Word添加脚注自定义标记
  12. 我的Foobar2000定制版本
  13. 名师出高徒!请关注领英上这十位活跃的大神
  14. 从安防行业网络化态势 看门禁市场发展风向
  15. 钉钉机器人V1使用说明
  16. 【译】浏览器如何工作:在现代web浏览器场景的之下
  17. 安装vim的最新版本
  18. 初学STM32,使用HAL库点灯全过程
  19. Ward Cunningham创建模式共享社区
  20. 如何运营闪闪壁纸号,快速吸粉连爆,抓住图文风口?

热门文章

  1. 大数据智能分析解决方案
  2. 【计算机网络:自顶向下方法】(一)计算机网络和英特网
  3. 小米嵌入式软件工程师笔试题目解析
  4. 阿里图标库icon字体使用详细步骤
  5. 等保知识|测评高风险项详解:安全管理部分
  6. 盲盒系统搭建——玩转盲盒系统
  7. SAP Router是个啥
  8. raspberry pi 用树莓派来听落网电台
  9. 助力服装智造!这家企业携手美创实现全流程数据安全保障
  10. 金投网煤炭数据爬取-精进版