C++是如何实现多态的

结论:C++通过虚函数来实现多态的,根本原因是派生类和基类的虚函数表的不同

构成多态的必要条件有如下3点:

  • 存在继承关系
  • 基类存在虚函数,且派生类有相同原型的函数遮蔽它
  • 存在基类类型的指针指向派生类对象,且该指针调用了存在遮蔽关系的虚函数

如下图,就是一个简单的多态的例子:(实验环境:vs2019)

大猪眉头一皱,觉得事情并不简单。

针对上述实验结果,有个疑点:

  • 基类指针是如何知道要调用派生类的f函数,而不是基类的f函数的呢?

这其中的奥秘,还是要从内存模型开始探索。谈到内存模型,那么我们先探讨最简单的——这个对象有多大吧(或者说这个对象占几个字节的内存吧)。

如上图所示,A的大小在32位编译模式下是4字节,在64位编译模式下是8字节,由此可见,A内存模型里面肯定有个指针!(原因:https://blog.csdn.net/qq_18138105/article/details/105209406)

那么继续深入分析对象的内存模型,这个指针到底指向什么呢?

实际上,这个指针指向一个数组,数组中的每个元素都是虚函数的入口地址,这个数组也就是虚函数表(存在于C++内存模型中的常量区)!由于虚函数表和对象在内存上是分开存储的(虚函数在C++内存模型中的代码区,对象在C++内存模型中的堆区,指向对象的指针在C++内存模型的栈区),因此,就需要在对象中需要安插一个指向这个虚函数表的指针!

我们再看一个例子:

各个对象的内存模型如下(我用cocos creator画的):

如上图所示,对象内存和虚函数表内存分开的,对象的*vfptr指向对应的虚函数表。(注意:和很多博客画的图不同,为了直观!我是把处于内存高地址的放上面,处于内存低地址的放下面)

仔细观察派生类B的虚函数表

  • 派生类如果存在对基类有遮蔽关系的虚函数,则在虚函数表中则取派生类的这个虚函数的入口地址,如&B::f
  • 对于未被派生类遮蔽的基类的虚函数,派生类的虚函数表则取基类的这个虚函数的入口地址,如&A::g
  • 派生类新增的虚函数,依次往虚函数表后面加,如&B::h

因此,我们通过指针调用虚函数时,先根据指针找到对象里的vfptr来定位到虚函数表,然后通过虚函数在虚函数表中的索引值来得到虚函数的入口地址。

比如 a->f(), 实际上编译器会这么处理 (*(*(a+0)+0))(a)

  • a+0 是 a对象 vfptr 的地址
  • *(a+0)是 vfptr的值, 又 vfptr 是指向虚函数表的指针,因此 *(a+0) 也是虚函数表的首地址
  • 由于 A::f 函数在虚函数表中的索引是0,因此 (*(a+0)+0)就是获取 A::f 函数的入口地址
  • 知道了A::f 函数的地址,*(*(a+0)+0) 就是对 A::f 的调用
  • 把a对象的指针传入 A::f, 就是a作为 A::f 的 this指针!

同理,调用 b->f() ,也是一样的,只不过访问的是 B的虚函数表,最后调用的是 B::f, 而不是 A::f, 这就解释了 “基类指针是如何知道要调用派生类的f函数,而不是基类的f函数” 的问题,就是因为虚函数表的不同

疑问虽然已经解决了,但是我们还是要继续细探究竟!经过下面的实验,得出的 结果和内存模型完全相符!

#include <iostream>
using namespace std;class A {public:A() : a(100) {}virtual void f() {cout << "A::f" << endl;}virtual void g() {cout << "A::g" << endl;}
protected:int a;
};class B : public A {public:B() : b(50) {}virtual void f() {cout << "B::f" << endl;}virtual void h() {cout << "B::h" << endl;}
protected:int b;
};// 定义一个 参数为 A*类型 返回值是 void 的 函数指针 Fun
typedef void (*Fun)(A*);// 指针值类型(64位编译模式下是long long, 32位编译模式下是int)
#ifdef _WIN64
#define ptr_value long long
#else
#define ptr_value int
#endif
// 根据对象指针和偏移量 获取 指针值类型的指针
#define ptr(obj, offset) ((ptr_value*)obj+offset)int main() {A* a = new A;A* b = new B;// 接下来探究 a对象 和 b对象 的 内存模型 //ptr_value a_vfptr_value = *ptr(a, 0); // a_vfptr的值 即 b的虚函数表的首地址((Fun)*ptr(a_vfptr_value, 0))(a); // A::f((Fun)*ptr(a_vfptr_value, 1))(a); // A::g    ptr_value a_a = *ptr(a, 1); // a对象 的 int a成员cout << (int)a_a << endl; // 100ptr_value b_vfptr_value = *ptr(b, 0); // b_vfptr的值 即 b的虚函数表的首地址((Fun)*ptr(b_vfptr_value, 0))(b); // B::f((Fun)*ptr(b_vfptr_value, 1))(b); // A::g    ((Fun)*ptr(b_vfptr_value, 2))(b); // B::hptr_value b_a = *ptr(b, 1); // b对象 的 int a成员cout << (int)b_a << endl; // 100ptr_value b_b = *ptr(b, 2); // b对象 的 int b成员cout << (int)b_b << endl; // 50return 0;
}

因此,只要理解了虚函数表,C++的多态自然就迎刃而解了。

C++是如何实现多态的相关推荐

  1. 重拳出击之《JVM》面试官版 (初哥勿看)

    <fonr color = black>JVM发展史,虚拟机发展史模块 java技术体系包括了几个组成部分? javaME.SE.EE分别是什么? 都说JDK7版本是第一个里程碑版本,为什 ...

  2. Python Day26:多态、封装、内置函数:__str__、__del__、反射(反省)、动态导入模块...

    ## 多态 ```python OOP中标准解释:多个不同类型对象,可以响应同一个方法,并产生不同结果,即为多态 多态好处:只要知道基类使用方法即可,不需要关心具体哪一个类的对象实现的,以不变应万变, ...

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

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

  4. C#关于面对象多态例子

    //主的喂狗 class Program     {         static void Main(string[] args)         {             //我们来模拟一个主人 ...

  5. java为什么序列化不一致_java – 为什么Jackson多态序列化在列表中不起作用?

    杰克逊正在做一些真正奇怪的事情,我找不到任何解释.我正在进行多态序列化,当一个对象独立时它可以很好地工作.但是,如果将相同的对象放入列表并对列表进行序列化,则会删除类型信息. 它丢失类型信息的事实将导 ...

  6. 【C++】多态(早期绑定、后期绑定)、抽象类(纯虚函数)、虚析构函数

    我们都知道面向对象编程的三大特征是封装.继承.多态,今天我们就来说一下其中之一的多态. 概念: 多态: 多态字面意思就是多种形态,C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同 ...

  7. Go 分布式学习利器(13)-- Go语言的多态

    文章目录 1. 基本的多态实现 2. 空接口与断言 3. Go接口的最佳实践 1. 基本的多态实现 我们知道C++中实现多态是通过虚函数表 和 继承来 实现的. 类似如下代码: class Progr ...

  8. (1)访问控制 (2)final关键字 (3)对象创建的过程 (4)多态

    1.访问控制(笔试题) 1.1 常用的访问控制符 public - 公有的 protected - 保护的 啥也不写 - 默认的 private - 私有的 1.2 访问控制符的比较 访问控制符 访问 ...

  9. 多态---父指针指向子类对象(父类引用指向子类对象)

    我们都知道,面向对象程序设计中的类有三大特性:继承,封装,多态,这个也是介绍类的时候,必须提到的话题,那么今天就来看一下OC中类的三大特性: 一.封装 封装就是对类中的一些字段,方法进行保护,不被外界 ...

  10. C++拾趣——使用多态减少泛型带来的代码膨胀

    泛型编程是C++语言中一种非常重要的技术,它可以让我们大大减少相似代码编写量.有时候,我和同事提及该技术时,称它是"一种让编译器帮我们写代码的技术".(转载请指明出于breakso ...

最新文章

  1. 图像篡改痕迹检测:Adobe双流Faster R-CNN网络
  2. 500页开放书搞定概率图建模,图灵奖得主Judea Pearl推荐(附链接)
  3. java 打包apk_Android APK打包流程
  4. 站在巨人的肩膀,2020我在使用和涉及到的开源项目
  5. restful和rest_HATEOAS的RESTful服务:JVM上的REST API和超媒体
  6. 百面机器学习!算法工程师面试宝典!| 码书
  7. Uncaught ReferenceError: jie is not defined
  8. IDEA 不提示报错 和有波浪线
  9. 合理安排计算顺序避免溢出
  10. 【Spring-IOC】bean扫描器ClassPathBeanDefinitionScanner详解
  11. 高考为什么考计算机信息,信息技术是否应该进入高考?
  12. HTML:设置背景颜色和图片
  13. IntelliJ IDEA运行内存设置
  14. 日语N2听力常用词汇
  15. MongoDB填充因子和更新优化
  16. 【车牌识别】RGB颜色模型车牌识别【含GUI Matlab源码 888期】
  17. 千亿市场规模的物流SaaS平台,是发生在云端的物流信息化的二次革命
  18. 硬盘内部硬件结构和工作原理详解
  19. 定位推送分享轻社交网络平台《足迹》——数据库设计
  20. java利用Scanner获取键盘输入

热门文章

  1. Spring Setter依赖注入示例
  2. 多个公证员提高网络吞吐量
  3. 认识CUBA平台的CLI
  4. java核心面试_Java核心面试问题
  5. 使用Java创建DynamoDB表
  6. SpringBoot:使用JdbcTemplate
  7. jpa动态扩展sql_扩展您的JPA POJO
  8. proxy aspectj_使用AspectJ,Javassist和Java Proxy进行代码注入的实用介绍
  9. 使用Spring Roo进行概念验证
  10. 使用JPA和Hibernate有效删除数据