C++: 如何用C语言实现C++的虚函数机制?

前言

googletest的源码中,看到gtest-matchers.h 中实现的MatcherBase 类自定义了一个 VTable,这种设计实现了一种类似于C++虚函数的机制。C++中的虚函数机制实质上就是通过这种方式实现的,本文用c语言自定义虚函数表VTable实现了一下virtual的功能,来深刻理解其机制。我们通过创建存储函数指针的结构体来模拟这种行为。

C++的运行时多态

如果我们在C++中有一个抽象基类 Shape ,定义了纯虚函数GetArea() 用于计算面积。对于不同的派生于 Shape 的类,面积计算方法会不一样。比如,对于圆形 Circle类,是 Shape 的一种,其特有属性为半径r,面积计算公式为 π·r² ;对于正方形 Square类,其特有属性为边长d,其面积计算公式为

基类Shape 的定义如下:

class Shape { 
public:virtual double GetArea()=0;
};

派生类Circle 继承于 Shape,定义如下:

class Circle: public Shape  {
public:Circle(double r)radius(r){}double GetArea() override {return radius * radius * 3.14;}
private:double radius;
};

通过基类指针指向不同派生类对象,去调用同一个方法,可以实现多态,如下所示:

Shape* shape = new CirCle(5);
shape->GetArea();

那么 virtual 实现多态的底层原理是什么呢?

用C语言简单实现virtual的底层原理

用C语言模拟实现以上C++代码。首先定义一个存储函数指针的结构体VTable,作为 Shape类的虚函数表 ,其中定义了两个函数指针, 分别指向该类计算面积的函数和析构函数,只要目标函数的参数列表和返回类型与函数指针定义相同,其中void*相当于this指针:

struct VTable{double (*GetArea)(void*);void (*Destructor)(void*);
};

然后定义一个基类Shape的结构体,其中包含了一个指向虚函数表VTable 的指针:

struct Shape{VTable* vtable;
};

在派生类 Circle 中,添加了额外的字段 radius,并且包含一个基类的实例,通过这种方式实现继承:

struct Circle{Shape base;double radius;
};

然后定义一个函数GetArea,作为公共调用接口,该函数接收一个Shape指针作为参数,并通过其指向类的虚函数表调用它的面积计算方法:

double GetArea(Shape* shape){return shape->vtable->GetArea(shape);
}

对于Circle类中的面积计算方法,实现如下:

double GetCircleArea(void* obj){Circle* circle = (Circle*)obj;return 3.14 * circle->radius * circle->radius;
}

最后,在程序中初始化Circle类的虚函数表 circle_vtable ,设置GetArea函数和析构函数,分配一块Circle对象大小的内存,将它的vtable绑定到circle_vtable ,初始化radius的值,并通过Shape类型的指针指向Circle对象,调用虚表中的方法:


VTable circle_vtable = {&GetCircleArea,&CircleDestructor};Circle* circle = (Circle*)malloc(sizeof(Circle));
circle->base.vtable = &circle_vtable;
circle->radius = 5;Shape* shape = (Shape*)circle;
printf("Area of circle: %f\n", GetArea(shape));
ShapeDestructor(shape);

输出为:

Area of circle: 78.500000

对于Square 类,也是类似的实现。类设计如下图所示:

完整测试程序地址:https://compiler-explorer.com/z/zbGh7dsh4

自定义VTable的好处

通过virtual实现多态绝大多数时候都够了,那为什么googletest库中要自定义VTable呢?以下是一些好处,供参考,学习一下库开发者的思考角度。

  1. 更好的性能

C++的虚函数机制虽然方便,但是它在某些情况下会带来性能开销。例如,虚函数表的查找需要额外的时间,并且每个对象都需要一个指向虚表的指针,这会增加内存的开销。通过自定义的VTable机制,googletest 可以更好地控制这些开销,可能减少间接调用的开销,提高性能。

  1. 灵活的内存管理

使用自定义的VTable可以更灵活地管理内存。例如,可以将VTable实例放置在特定的内存区域或共享多个对象之间,从而减少内存占用。这种方式也可以使得一些轻量级对象不需要包含虚表指针,从而减小对象的大小。

  1. 跨编译器兼容性

不同的编译器和编译器版本对虚函数的实现可能略有不同,这会导致跨编译器的兼容性问题。通过自定义的VTable机制,googletest 可以避免依赖编译器的实现细节,保证在不同编译器和平台上的一致行为。

  1. 类型擦除和多态性

自定义的VTable机制可以实现类型擦除和更灵活的多态性。它允许将不同类型的对象统一处理,而不需要它们共享一个公共的基类。这对于模板编程和泛型编程非常有用,因为可以实现基于模板的多态而不需要依赖继承。

  1. 更好的调试和测试

自定义的VTable可以在调试和测试中提供更多的信息。例如,可以在VTable中包含额外的调试信息或断言,以帮助发现和诊断问题。这种灵活性在某些情况下是C++内置的虚函数机制所无法提供的。

总结

这个例子的实现对很多问题还没有考虑到,不过我认为它已经通过C语言基本展示了C++虚函数的原理。理解以上过程后,再去重新思考以下问题,可能会更清晰。

  1. C++ virtual运行时多态的实现原理?
  2. 派生类重写虚函数生效的条件是什么?
  3. 一个仅有虚析构函数的类大小为多少?
  4. 纯虚函数=0是什么含义?
  5. 虚函数为什么会稍慢些?其开销有哪些?
  6. 为什么构造函数不能是虚函数,而析构函数通常需要是虚函数?

参考

  1. https://github.com/google/googletest/blob/1d17ea141d2c11b8917d2c7d029f1c4e2b9769b2/googletest/include/gtest/gtest-matchers.h#L316
  2. https://stackoverflow.com/questions/78655663/why-does-matcherbase-class-in-gtest-matchers-h-define-a-vtable-and-what-is-its

如果你觉得本文对你有帮助,请点个赞,鼓励我持续创作;关注我,一起持续进步!

公众号:七昂的技术之旅

关注公众号,送你一份C++系列电子书。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/863832.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Workerman在线客服系统源码,附搭建教程

源码介绍: Workerman在线客服系统源码。 workerman是一个高性能的PHP socket 服务器框架,workerman基于PHP多进程以及libevent事件轮询库,PHP开发者只要实现一两个接口,便可以开发出自己的网络应用,例如Rpc服务、聊天…

240630_昇思学习打卡-Day12-Transformer中的Multiple-Head Attention

240630_昇思学习打卡-Day12-Transformer中的Multiple-Head Attention 以下为观看大佬课程及查阅资料总结所得,附大佬视频链接:Transformer中Self-Attention以及Multi-Head Attention详解_哔哩哔哩_bilibili,强烈建议先去看大佬视频&#xff…

python解锁图片相似度的神奇力量

在这个信息爆炸的时代,图片成为了我们传递信息、表达情感和记录生活的重要方式。然而,面对海量的图片资源,如何快速准确地找到相似的图片,成为了一个亟待解决的问题。现在,让我们为您揭开图片相似度的神秘面纱,带您领略这一创新技术的魅力! 图片相似度技术,就像是一位…

docker harbor仓库搭建,主从库复制

背景:需要主机安装docker-ce和docer-compose #1.安装相关依赖. yum install -y yum-utils device-mapper-persistent-data lvm2 #2.下载官方的docker yum源文件 yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo …

10款好用不火的PC软件,真的超好用!

AI视频生成:小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频https://aitools.jurilu.com/市场上有很多软件,除了那些常见的大众化软件,还有很多不为人知的小众软件,它们的作用非常强大,简洁…

cJSON源码解析之add_item_to_object函数

文章目录 前言add_item_to_object函数是干什么的add_item_to_object代码解析函数实现函数原理解析开头的代码constant_key参数的作用最后的if判断 add_item_to_array函数 总结 前言 在我们的日常编程中,JSON已经成为了一种非常常见的数据交换格式。在C语言中&#…

UI Toolkit系统学习

UI Toolkit 此文章用于学习UnityUI系统,手头的项目做完会来完善 官方文档 Unity上方菜单栏点击Window->UI Toolkit->Samples可以看UI Toolkit中的很多样例 使用 UI Toolkit 和 UI Builder 制作物品编辑器 在文件夹中右键->Create->UI Toolkit->Edi…

leetCode-hot100-动态规划专题

动态规划 动态规划定义动态规划的核心思想动态规划的基本特征动态规划的基本思路例题322.零钱兑换53.最大子数组和72.编辑距离139.单词拆分62.不同路径63.不同路径Ⅱ64.最小路径和70.爬楼梯121.买卖股票的最佳时机152.乘积最大子数组 动态规划定义 动态规划(Dynami…

【训练篇】MLU370-M8 完成 qwen1.5-7b-chat-lora训练及推理

文章目录 前言一、平台环境配置二、环境 or 模型准备1.模型下载2.环境准备2.1 modelscope2.2 transformers2.3 accelerate2.4 deepspeed2.5 peft2.6 环境代码修改 3训练代码准备4 代码修改 三,训练后推理验证四.推理效果展示1.微调前2.微调后 前言 本期我们采用魔塔…

distance delayed sound

distance delayed sound 在本章中,我们将讨论在游戏音频中使用距离延迟的重要性。我们将首先通过一个常见的例子——闪电和雷鸣,来展示这种重要性并解释距离延迟音频的基础知识。我们将讨论计算速度、距离和时间的数学和方程式,以确定距离延迟…

数据倾斜优化:Hive性能提升的核心

文章目录 1. 定义2. 数据倾斜2.1 Map2.2 Join2.3 Reduce 3. 写在最后 1. 定义 数据倾斜,也称为Data Skew,是在分布式计算环境中,由于数据分布不均匀导致某些任务处理的数据量远大于其他任务,从而形成性能瓶颈的现象。这种情况在H…

PotPlayer安装及高分辨率设置

第1步: 下载安装PotPlayer软件 PotPlayer链接:https://pan.baidu.com/s/1hW168dJrLBonUnpLI6F3qQ 提取码:z8xd 第2步: 下载插件,选择系统对应的位数进行运行,该文件不能删除,删除后将失效。 …

【强化学习的数学原理】课程笔记--2(贝尔曼最优公式,值迭代与策略迭代)

目录 贝尔曼最优公式最优 Policy求解贝尔曼最优公式求解最大 State Value v ∗ v^* v∗根据 v ∗ v^* v∗ 求解贪婪形式的最佳 Policy π ∗ \pi^* π∗一些证明过程 一些影响 π ∗ \pi^* π∗ 的因素如何让 π ∗ \pi^* π∗ 不 “绕弯路” γ \gamma γ 的影响reward 的…

2024/6/30周报

文章目录 摘要ABSTRACT文献阅读题目问题本文贡献方法LSTMTCN模型总体架构 实验实验结果 深度学习TCN-LSTM代码运行结果 总结 摘要 本周阅读了一篇关于TCN和LSTM进行光伏功率预测的文章,本文提出了一种利用LSTM-TCN预测光伏功率的新模型。它由长短期记忆和时间卷积网…

ThreadPoolExecutor基于ctl变量的声明周期管理

个人博客 ThreadPoolExecutor基于ctl变量的声明周期管理 | iwts’s blog 总集 想要完整了解下ThreadPoolExecutor?可以参考: 基于源码详解ThreadPoolExecutor实现原理 | iwts’s blog ctl字段的应用 线程池内部使用一个变量ctl维护两个值&#xff…

树莓派开发之文件传输

文章目录 一、简介使用U盘传输文件使用SD卡传输文件使用Xftp 7传输文件 二、 总结 一、简介 在树莓派开发中经常会用到文件传输,下面介绍几种树莓派文件传输的几种方法。 使用U盘传输文件 (1)复制所需传输文件到U盘 (2&#…

C++:typeid4种cast转换

typeid typeid typeid是C标准库中提供的一种运算符,它用于获取类型的信息。它主要用于类型检查和动态类型识别。当你对一个变量或对象使用typeid运算符时,它会返回一个指向std::type_info类型的指针,这个信息包含了关于该类型名称、大小、基…

Pikachu靶场--Sql Inject

参考借鉴 pikachu靶场练习(详细,完整,适合新手阅读)-CSDN博客 数字型注入(post) 这种类型的SQL注入利用在用户输入处插入数值,而不是字符串。攻击者试图通过输入数字来修改SQL查询的逻辑,以执行恶意操作。…

Unity Shader 极坐标

Unity Shader 极坐标 前言项目简单极坐标极坐标变体之方形极坐标变体之圆形拉花 鸣谢 前言 极坐标记录 项目 简单极坐标 极坐标变体之方形 极坐标变体之圆形 拉花 鸣谢 【菲兹杂货铺】【Unity Shader教程】极坐标实现以及极坐标的两种变体

【87 backtrader期权策略】基于50ETF期权的covered-call-strategy

前段时间有读者希望能够实现一个期权策略的模板,这段时间通过akshare下载了期权的数据,并进行了清洗,写了一个最简单的期权策略,供大家参考。 策略逻辑: 这是151 trading strategies中的一个期权策略。 买入50ETF基金,手续费按照万分之二计算,一直持有卖出一个最远期的…