C++ 虚函数、虚函数表剖析

2023-10-27

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数

那么问题来了C++又是如何实现这种技术的呢?C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(又名虚表)

1. 类的虚表

每个包含了虚函数的类都包含一个虚表。

我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

我们来看以下的代码。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

图A的虚表如下图所示

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

2. 虚表指针

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
在这里插入图片描述
上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

3.动态绑定

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};
class B : public A {
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};
class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

类A是基类,类B继承类A,类C又继承类B。类A,类B,类C,其对象模型如下图所示。
在这里插入图片描述
由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都拥有一个虚表指针*__vptr,用来指向自己所属类的虚表。

类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()和A::vfunc2()。

类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。

类C继承于类B,故类C可以调用类B的函数,但由于类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。

虽然图中看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。

非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。

实例讲解

假设我们定义一个类B的对象bObject。由于bObject是类B的一个对象,故bObject包含一个虚表指针,指向类B的虚表。

int main() 
{
    B bObject;
}

现在,我们声明一个类A的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针bObject的虚表指针指向类B的虚表,所以p可以访问到B vtbl。【此处我认为是调用了指针p的虚表指针,而p指向的是类A的虚表,但由于B在初始化时,编译器覆盖了0x401F0位置上已存在的函数,最终导致调用了B中定义的vfunc1h函数。】

int main() 
{
    B bObject;
    A *p = & bObject;
}

当我们使用p来调用vfunc1()函数时,会发生什么现象?

int main() 
{
    B bObject;
    A *p = & bObject;
    p->vfunc1();
}

程序在执行p->vfunc1()时,会发现p是个指针,且调用的函数是虚函数,接下来便会进行以下的步骤。

首先,根据虚表指针p->__vptr来访问对象bObject对应的虚表。虽然指针p是基类A*类型,但是*__vptr也是基类的一部分,所以可以通过p->__vptr可以访问到对象对应的虚表。

然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。对于p->vfunc1()的调用,B vtbl的第一项即是vfunc1对应的条目。

最后,根据虚表中找到的函数指针,调用函数。从上图可以看到,B vtbl的第一项指向B::vfunc1(),所以p->vfunc1()实质会调用B::vfunc1()函数。

如果p指向类A的对象,情况又是怎么样?

int main() 
{
    A aObject;
    A *p = &aObject;
    p->vfunc1();
}

当aObject在创建时,它的虚表指针__vptr已设置为指向A vtbl,这样p->__vptr就指向A vtbl。vfunc1在A vtbl对应在条目指向了A::vfunc1()函数,所以p->vfunc1()实质会调用A::vfunc1()函数。

可以看到,通过使用这些虚函数表,即使使用的是基类的指针来调用函数,也可以达到正确调用运行中实际对象的虚函数

我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。

参考:
C++ 虚函数表剖析.
c++虚函数详解(你肯定懂了).

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++ 虚函数、虚函数表剖析 的相关文章

  • 向进度条添加百分比文本 C#

    我有一个方法可以显示进程栏何时正在执行以及何时成功完成 我工作得很好 但我想添加一个百分比 如果完成 则显示 100 如果卡在某个地方 则显示更少 我在网上做了一些研究 但我无法适应我正在寻找的解决方案 这是我的代码 private voi
  • 在 C++ 中使用 matlab 结构(matlab 函数调用的返回值)(由 matlab 编译器生成的库)

    你好 我有一个相当简单的 matlab 函数 例如 function MYSTRUCT myfunc MYSTRUCT prop1 test MYSTRUCT prop2 foo MYSTRUCT prop3 42 end 我用 matla
  • 注销租约抛出 InvalidOperationException

    我有一个使用插件的应用程序 我在另一个应用程序域中加载插件 我使用 RemoteHandle 类http www pocketsilicon com post Things That Make My Life Hell Part 1 App
  • Directory.Delete 之后 Directory.Exists 有时返回 true ?

    我有非常奇怪的行为 我有 Directory Delete tempFolder true if Directory Exists tempFolder 有时 Directory Exists 返回 true 为什么 可能是资源管理器打开了
  • 为什么 int8_t 和用户通过 cin 输入显示奇怪的结果[重复]

    这个问题在这里已经有答案了 一小段代码让我发疯 但希望你能阻止我跳出窗外 看这里 include
  • 如何让 Swagger 插件在自托管服务堆栈中工作

    我已经用 github 上提供的示例重新提出了这个问题 并为任何想要自己运行代码的人提供了一个下拉框下载链接 Swagger 无法在自托管 ServiceStack 服务上工作 https stackoverflow com questio
  • 计算 Richtextbox 中所有单词的最有效方法是什么?

    我正在编写一个文本编辑器 需要提供实时字数统计 现在我正在使用这个扩展方法 public static int WordCount this string s s s TrimEnd if String IsNullOrEmpty s re
  • 提交后禁用按钮

    当用户提交付款表单并且发布表单的代码导致 Firefox 中出现重复发布时 我试图禁用按钮 去掉代码就不会出现这个问题 在firefox以外的任何浏览器中也不会出现这个问题 知道如何防止双重帖子吗 System Text StringBui
  • C中的malloc内存分配方案

    我在 C 中尝试使用 malloc 发现 malloc 在分配了一些内存后浪费了一些空间 下面是我用来测试 malloc 的一段代码 include
  • 复制目录内容

    我想将目录 tmp1 的内容复制到另一个目录 tmp2 tmp1 可能包含文件和其他目录 我想使用C C 复制tmp1的内容 包括模式 如果 tmp1 包含目录树 我想递归复制它们 最简单的解决方案是什么 我找到了一个解决方案来打开目录并读
  • 如何创建包含 IPv4 地址的文本框? [复制]

    这个问题在这里已经有答案了 如何制作一个这样的文本框 我想所有的用户都见过这个并且知道它的功能 您可以使用带有 Mask 的 MaskedTestBox000 000 000 000 欲了解更多信息 请参阅文档 http msdn micr
  • 使用接口有什么好处?

    使用接口有什么用 我听说它用来代替多重继承 并且还可以用它来完成数据隐藏 还有其他优点吗 哪些地方使用了接口 程序员如何识别需要该接口 有什么区别explicit interface implementation and implicit
  • 如何使用 LINQ2SQL 连接两个不同上下文的表?

    我的应用程序中有 2 个数据上下文 不同的数据库 并且需要能够通过上下文 B 中的表的右连接来查询上下文 A 中的表 我该如何在 LINQ2SQL 中执行此操作 Why 我们正在使用 SaaS 产品来跟踪我们的时间 项目等 并希望向该产品发
  • 在一个平台上,对于所有数据类型,所有数据指针的大小是否相同? [复制]

    这个问题在这里已经有答案了 Are char int long 甚至long long 大小相同 在给定平台上 不能保证它们的大小相同 尽管在我有使用经验的平台上它们通常是相同的 C 2011 在线草稿 http www open std
  • Qt - ubuntu中的串口名称

    我在 Ubuntu 上查找串行端口名称时遇到问题 如您所知 为了在 Windows 上读取串口 我们可以使用以下代码 serial gt setPortName com3 但是当我在 Ubuntu 上编译这段代码时 我无法使用这段代码 se
  • 使用自定义堆的类似 malloc 的函数

    如果我希望使用自定义预分配堆构造类似 malloc 的功能 那么 C 中最好的方法是什么 我的具体问题是 我有一个可映射 类似内存 的设备 已将其放入我的地址空间中 但我需要获得一种更灵活的方式来使用该内存来存储将随着时间的推移分配和释放的
  • C# HashSet 只读解决方法

    这是示例代码 static class Store private static List
  • 无法接收 UDP Windows RT

    我正在为 Windows 8 RT 编写一个 Windows Store Metro Modern RT 应用程序 需要在端口 49030 上接收 UDP 数据包 但我似乎无法接收任何数据包 我已按照使用教程进行操作DatagramSock
  • 当从finally中抛出异常时,Catch块不会被评估

    出现这个问题的原因是之前在 NET 4 0 中运行的代码在 NET 4 5 中因未处理的异常而失败 部分原因是 try finallys 如果您想了解详细信息 请阅读更多内容微软连接 https connect microsoft com
  • 从列表中选择项目以求和

    我有一个包含数值的项目列表 我需要使用这些项目求和 我需要你的帮助来构建这样的算法 下面是一个用 C 编写的示例 描述了我的问题 int sum 21 List

随机推荐

  • 内联函数总结

    定义 它们看起来像函数 运作起来像函数 比宏 macro 要好得多 使用时还不需要承担函数调用的负担 当内联一个函数时 编译器可以对函数体执行特定环境下的优化工作 这样的优化对 正常 的函数调用时不可能的 规则 inline关键字必须和函数
  • vue+vue-matomo实现埋点

    安装 npm install vue matomo save main js import createApp from vue import manotoUse from utils manotoUse import router fro
  • linux 符号连接文件,Linux 硬链接和软链接(符号链接)

    什么是目录 Linux 文件系统是树状结构的 根目录下存在一系列子目录 目录里边有文件或者子目录 但问题在于 目录是什么 文件又是什么 文件是 数据 属性 比如名字 创建时间 所有者之类 目录是 一个列表 列表中的每一项是 inode gt
  • 婴幼儿奶酪怎么选

    总原则 天然奶酪 高钙 少钠 无添加剂 奶酪 分天然奶酪 和再制奶酪 天然奶酪成分非常简单 是主要以牛奶为原材料 没有经过深加工的原生奶酪 再制奶酪配料表复杂 是天然奶酪加上水 以及添加剂后的产物 营养价值降低不少 天然奶酪 并且高钙低钠是
  • Ubuntu18.04 “A start job is running for Raise network interface(5min 1s)”

    在启动Ubuntu时出现如下情况 5min过后 虽然会进入ubuntu的用户登陆界面 但是登陆成功后 输入ifconfig命令 如下图 会发现ens33中没有ip地址 因此xshell无法与该虚拟机进行远程连接 经过分析 我采用的连接方式为
  • java swing 天天酷跑游戏 功能完善 完整代码 下载即可以运行

    今天天气不错 利用一段时间给大家分享一个天天酷跑的游戏435 该游戏也属于一个比较优秀的作品 整个系统界面漂亮 有完整得源码 希望大家可以喜欢 喜欢的帮忙点赞和关注 一起编程 一起进步 开发环境 开发语言为Java 开发环境Eclipse或
  • 使用 styled-components 定义组件样式

    每个项目产品都要加埋点 加500行埋点是不是会占用你一两天的时间而且很容易犯错 想只用一小时准确加完这500行埋点剩下一天喝茶聊天么 来试试这520web工具 高效加埋点 目前我们公司100号前端都在用 因为很好用 所以很自然普及开来了 推
  • django-xadmin自定义widget插件(自定义详情页字段的显示样式)

    有时候我们想要修改xadmin详情页字段的显示方式 比如django默认的ImageField在后台显示的是image的url 我们更希望看到image的缩略图 再比如django将多对多字段显示为多选的下拉框或者左右选择栏的方式 向图片展
  • axios+qs发送ajax请求获取接口数据

    axios qs发送ajax请求获取接口数据 一 html页面中需要引入axios和qs库 如下 二 后台接口如下 1 getDemo1接口 测试传参id 必须 和 name 不必须 CrossOrigin RestController p
  • sort排序

    文章目录 一 Arrays sort 1 1 基本数据类型排序 1 2 引用对象类型排序 二 Collections sort Java中常用的数组或集合排序的方法有两个 一个是java util Arrays中的静态方法Arrays so
  • 案列 : 提供index.html页面,页面中有一个省份 下拉列表 2. 当 页面加载完成后 发送ajax请求,加载所有省份

    一 首先明确需求 案例需求 1 提供index html页面 页面中有一个省份 下拉列表 2 当 页面加载完成后 发送ajax请求 加载所有省份 二 数据库编写 CREATE DATABASE NBA 创建数据库 USE NBA 使用数据库
  • 深度学习第一篇论文——半监督学习Mean Teacher 的学习

    最近一个月刚接触深度学习 导师给了一篇论文 mean teacher 让我先理解然后跑论文里面的代码 这个过程中我出现了很多问题 借这篇blog记录下来 也是鼓励自己接着学下去 Mean Teacher 的论文地址 https arxiv
  • jsp之cookie的基本操作&&实现用户登录cookie记录用户信息

    jsp状态管理 因为http的无状态性 所以可以用session或cookie技术保存用户信息 而cookie可以记录信息可以判定注册用户是否已经登录网站 购物车的应用 浏览记录 但是有安全风险 创建cookie Cookie cookie
  • keil调试查看ROM或RAM

    Ctrl F5或点击调试按钮进入调试界面 在工具栏上点击Memory Windows 则右下角出现Memory1的页面 默认出现的是ROM的查看界面 在Address一栏输入十六进制的地址即可查看ROM里面的数值 点击Memory Wind
  • 动态修改JavaBean中的注解的参数值

    我这里有一个需求需要修改Person类中的一个属性上的注解的值进行修改 例如 public class Person private int age ApiParam access lala private String name get
  • 怎么做验收测试?

    本文是本系列文章的第四篇 也是最后一篇 主要讲述我们在Lyft面对越来越多的开发人员和服务时 如何扩展开发实践 第一部分 开发和测试环境的历史 第二部分 加快本地开发的一些优化 第三部分 预发布环境通过重载形式来扩展服务网格 第四部分 怎么
  • halcon给图像添加不同颜色的透明遮罩(叠加透明ROI)

    目录 前言 方法 1 给单通道图像添加透明遮罩 2 给RGB图像添加透明遮罩 参考链接 前言 最近想给图片叠加上透明region方便展示 以前一直用overpaint region算子搭配add image就行 这次用单通道图竟然叠加出来的
  • typec耳机知识介绍

    数字耳机和模拟耳机 模拟耳机即我们的常见的3 5mm接口的耳机 包括左右声道 地或者mic 如左图 数字耳机 右图 包含一个usb声卡 DAC ADC amp 模拟耳机 当数字耳机接入到手机 otg 或者电脑后 手机或者电脑识别到了usb设
  • 如何在本地测试ajax时间,为什么在本地测试ajax反应很慢

    在本地测试ajax接收分页数据 要反应1s左右 以上 但是放到服务器上就100ms到 900多ms之间 有什么好的办法解决ajax获取速度慢吗 已经把分页数据换成json了 没多大改善 数据大小显示是在2 9kb 耗时1 03s 处理逻辑就
  • C++ 虚函数、虚函数表剖析

    C 中的虚函数的作用主要是实现了多态的机制 关于多态 简而言之就是用父类型别的指针指向其子类的实例 然后通过父类的指针调用实际子类的成员函数 那么问题来了C 又是如何实现这种技术的呢 C 使用了一种动态绑定的技术 这个技术的核心是虚函数表