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,其面积计算公式为 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呢?以下是一些好处,供参考,学习一下库开发者的思考角度。
- 更好的性能
C++的虚函数机制虽然方便,但是它在某些情况下会带来性能开销。例如,虚函数表的查找需要额外的时间,并且每个对象都需要一个指向虚表的指针,这会增加内存的开销。通过自定义的VTable机制,googletest
可以更好地控制这些开销,可能减少间接调用的开销,提高性能。
- 灵活的内存管理
使用自定义的VTable可以更灵活地管理内存。例如,可以将VTable实例放置在特定的内存区域或共享多个对象之间,从而减少内存占用。这种方式也可以使得一些轻量级对象不需要包含虚表指针,从而减小对象的大小。
- 跨编译器兼容性
不同的编译器和编译器版本对虚函数的实现可能略有不同,这会导致跨编译器的兼容性问题。通过自定义的VTable机制,googletest
可以避免依赖编译器的实现细节,保证在不同编译器和平台上的一致行为。
- 类型擦除和多态性
自定义的VTable机制可以实现类型擦除和更灵活的多态性。它允许将不同类型的对象统一处理,而不需要它们共享一个公共的基类。这对于模板编程和泛型编程非常有用,因为可以实现基于模板的多态而不需要依赖继承。
- 更好的调试和测试
自定义的VTable可以在调试和测试中提供更多的信息。例如,可以在VTable中包含额外的调试信息或断言,以帮助发现和诊断问题。这种灵活性在某些情况下是C++内置的虚函数机制所无法提供的。
总结
这个例子的实现对很多问题还没有考虑到,不过我认为它已经通过C语言基本展示了C++虚函数的原理。理解以上过程后,再去重新思考以下问题,可能会更清晰。
- C++ virtual运行时多态的实现原理?
- 派生类重写虚函数生效的条件是什么?
- 一个仅有虚析构函数的类大小为多少?
- 纯虚函数=0是什么含义?
- 虚函数为什么会稍慢些?其开销有哪些?
- 为什么构造函数不能是虚函数,而析构函数通常需要是虚函数?
参考
- https://github.com/google/googletest/blob/1d17ea141d2c11b8917d2c7d029f1c4e2b9769b2/googletest/include/gtest/gtest-matchers.h#L316
- https://stackoverflow.com/questions/78655663/why-does-matcherbase-class-in-gtest-matchers-h-define-a-vtable-and-what-is-its
如果你觉得本文对你有帮助,请点个赞,鼓励我持续创作;关注我,一起持续进步!
公众号:七昂的技术之旅
想深入学习C++的同学,可通过以下链接免费获取C++系列书籍。
C++ : 如何用C语言实现C++的虚函数机制?的更多相关文章
- 如何用C#语言构造蜘蛛程序
"蜘蛛"(Spider)是Internet上一种很有用的程序,搜索引擎利用蜘蛛程序将Web页面收集到数据库,企业利用蜘蛛程序监视竞争对手的网站并跟踪变动,个人用户用蜘蛛程序下载We ...
- C 语言的可变参数表函数的设计
在c语言中使用变长参数最常见的就是下面两个函数了: int printf(const char *format, ...); int scanf(const char *format, ...); 那 ...
- javascript语言中的一等公民-函数
简介 在很多传统语言(C/C++/Java/C#等)中,函数都是作为一个二等公民存在,你只能用语言的关键字声明一个函数然后调用它,如果需要把函数作为参数传给另一个函数,或是赋值给一个本地变量,又或是作 ...
- c++语言虚函数实现多态的原理(更新版)
自上一个帖子之间跳过了一篇总结性的帖子,之后再发,今天主要研究了c++语言当中虚函数对多态的实现,感叹于c++设计者的精妙绝伦 c++中虚函数表的作用主要是实现了多态的机制.首先先解释一下多态的概念, ...
- C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现
tfref 前言 C++对象的内存布局 只有数据成员的对象 没有虚函数的对象 拥有仅一个虚函数的对象 拥有多个虚函数的对象 单继承且本身不存在虚函数的继承类的内存布局 本身不存在虚函数(不严谨)但存在 ...
- C语言样式的文件操作函数
使用C语言样式的文件操作函数,需要包含stdio.h头文件. 1.打开文件的函数: //oflag的取值为“w”或“r”,分别表示以写或读的方式打开 FILE* fd = fopen(filename ...
- C语言带参数的main函数
C语言带参数的main函数 #include<stdio.h> int main(int argc,char*argv[]) { int i; ;i<argc;i++) printf ...
- 智能合约语言 Solidity 教程系列3 - 函数类型
Solidity 教程系列第三篇 - Solidity 函数类型介绍. 写在前面 Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊.智能合约有所了解,如果你还不了解,建议你先看以 ...
- C语言中可变参数的函数(三个点,“...”)
C语言中可变参数的函数(三个点,“...”) 本文主要介绍va_start和va_end的使用及原理. 在以前的一篇帖子Format MessageBox 详解中曾使用到va_start和va_end ...
- Go语言学习笔记七: 函数
Go语言学习笔记七: 函数 Go语言有函数还有方法,神奇不.这有点像python了. 函数定义 func function_name( [parameter list] ) [return_types ...
随机推荐
- Vue源码学习(二十):$emit、$on实现原理
好家伙, 0.一个例子 <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset= ...
- 小程序-云数据库的add,get,remove,update
云数据库的使用就是使用简单的原生封装wx.cloud.database().collection("list"),然后就是add,get,remove,update四个方法 初始化 ...
- Three光源Target位置改变光照方向不变的问题及解决方法
0x00 楔子 在 Three.js 中,光源的目标(target)是一种用于指定光源方向的重要元素.在聚光灯中和定向光(DirectionalLight)中都有用到. 有时我们可能会遇到光源目标位置 ...
- 可视化—gojs 超多超实用经验分享(二)
想了想序号还是接上一篇分享的的序号接着写,如果在本文中没有获取需要的答案,可以移步去看看上一篇的分享.gojs 超多超实用经验分享(一) 目录 22. 指定线段连接到节点的某一个特定的接口上 23. ...
- <script> 和 <script setup> 的一些主要差别
<script setup> 是 Vue 3 中的新特性,它是一种简化和更具声明性的语法,用于编写组件的逻辑部分.相比之下,<script> 是 Vue 2 中常用的编写组件逻 ...
- Java 中的一些知识点
Java 中的一些知识点 Java 中的知识点 与C++相关 toString方法 super 与C++相关[了解的不是很多] 在Java程序中:一个方法以 ; 结尾,并且修饰符列表中有 native ...
- 前端使用 Konva 实现可视化设计器(18)- 素材嵌套 - 加载阶段
本章主要实现素材的嵌套(加载阶段)这意味着可以拖入画布的对象,不只是图片素材,还可以是嵌套的图片和图形. 请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,欢迎来提 Issue ...
- Selenium 8个定位元素
selenium 8个定位元素为:id.name.xpath.link_text.class_name.tag_name.css_selector.partial_link_text 1.id元素 浏 ...
- SemanticKernel/C#:使用Ollama中的对话模型与嵌入模型用于本地离线场景
前言 上一篇文章介绍了使用SemanticKernel/C#的RAG简易实践,在上篇文章中我使用的是兼容OpenAI格式的在线API,但实际上会有很多本地离线的场景.今天跟大家介绍一下在Semanti ...
- 【Hibernate】05 缓存与MySQL事务隔离
Cache 什么是缓存? 数据存储到数据库,是从内存中以流的方式写进[输出]到数据库,其效率并不是很高 - 所以在内存中暂存一部分数据,可以不以流的方式读取,效率是非常高的[相对于流来说] Hiber ...