首页 > 编程语言 >C++ : 如何用C语言实现C++的虚函数机制?

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

时间:2024-06-30 21:31:21浏览次数:1  
标签:VTable 函数 C++ C语言 Shape Circle circle

前言

在 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++的同学,可通过以下链接免费获取C++系列书籍。

百度链接 | 谷歌链接

标签:VTable,函数,C++,C语言,Shape,Circle,circle
From: https://www.cnblogs.com/qiangz/p/18276995

相关文章

  • C语言力扣刷题11——打家劫舍1——[线性动态规划]
    力扣刷题11——打家劫舍1和2——[线性动态规划]一、博客声明二、题目描述三、解题思路1、线性动态规划 a、什么是动态规划2、思路说明四、解题代码(附注释)一、博客声明  找工作逃不过刷题,为了更好的督促自己学习以及理解力扣大佬们的解题思路,开辟这个系列来记录......
  • c++高精度计算-大整数相乘
    例题-信奥赛1307:【例1.3】高精度乘法题目描述:输入两个高精度正整数M和N(M和N均小于100位)。求这两个高精度数的积。输入:输入两个高精度正整数M和N。输出:求这两个高精度数的积。输入样例:363输出样例:108 做题思路:学习乘法的朋友大概对加减法都有一定的了解,我就......
  • 【C语言】--操作符详解
    ......
  • 【保姆级教程+配置源码】在VScode配置C/C++环境
    目录一、下载VScode1.在官网直接下载安装即可2.安装中文插件二、下载C语言编译器MinGW-W64三、配置编译器环境变量1.解压下载的压缩包,复制该文件夹下bin目录所在地址2.在电脑搜索环境变量并打开3.点击环境变量→选择系统变量里的Path→点击编辑按钮4.点击新建5......
  • item7 Moving to Modern C++**
    第3章移步现代C++CHAPTER3MovingtoModernC++说起知名的特性,C++11/14有一大堆可以吹的东西,auto,智能指针(smartpointer),移动语义(movesemantics),lambda,并发(concurrency)——每个都是如此的重要,这章将覆盖这些内容。掌握这些特性是必要的,要想成为高效率的现代C++程序员需......
  • 【C++】 ——【模板初阶】——基础详解
    目录1.泛型编程1.1泛型编程的概念1.2泛型编程的历史与发展1.3泛型编程的优势1.4泛型编程的挑战2.函数模板2.1函数模板概念2.2函数模板格式2.3函数模板的原理2.4函数模板的实例化2.5模板参数的匹配原则2.6函数模板的特化2.7函数模板的使用注意事项2.......
  • C语言教程-11-字符串
    title:C语言教程-11-字符串tags:[C]categories:C语言教程description:最重要的交互信息-字符串及其存储,操作提要:本章要讲解字符串的内容.同时会使用到前面基本输入输出一章中讲解的各种输入输出函数和数组,若不了解请自行复习.注意:本章需要的前置知识为:1.......
  • NzN的C++之路--拷贝构造函数&&赋值运算符重载
    目录Part1拷贝构造函数一、概念二、特征Part2赋值运算符重载一、运算符重载二、赋值运算符重载三、前置++和后置++重载Part3const成员Part4 取地址及const取地址操作符重载 Part1拷贝构造函数一、概念        拷贝构造函数:只有单个形参,该形参......
  • C语言 | Leetcode C语言题解之第188题买卖股票的最佳时机IV
    题目:题解:intmaxProfit(intk,int*prices,intpricesSize){intn=pricesSize;if(n==0){return0;}k=fmin(k,n/2);intbuy[k+1],sell[k+1];memset(buy,0,sizeof(buy));memset(sell,0,sizeof(sell));......
  • C语言 | Leetcode C语言题解之第187题重复的DNA序列
    题目:题解:#defineMAXSIZE769/*选取一个质数即可*/typedefstructNode{charstring[101];intindex;structNode*next;//保存链表表头}List;typedefstruct{List*hashHead[MAXSIZE];//定义哈希数组的大小}MyHashMap;List*......