C++编程揭秘:虚表机制与ABI兼容性的实例剖析

前言:
假设你的应用程序引用的一个库某天更新了,虽然 API 和调用方式基本没变,但你需要重新编译你的应用程序才能使用这个库,那么一般说这个库是源码兼容(Source compatible);反之,如果不需要重新编译应用程序就能使用新版本的库,那么说这个库跟它之前的版本是二进制兼容的(Binary compatible)。
👉👉👉
而影响ABI兼容中,最重要的部分涉及到虚表机制,这块我们重点来谈下它们之间的关系。

文章目录

      • 虚表生成
      • 子类的虚表
      • C++ 类虚表中函数顺序规则
      • 搞清楚虚表有什么用?
      • 导出DLL注意事项
      • C++ 虚析构函数在虚表中位置说明
      • 更多ABI 相关文章

虚表生成

C++的类只要有一个虚函数,就会生成一张虚表:

class A
{
};class B
{
public:virtual void vfunc1();
}
sizeof(A) = 1	// 空类1个字节用于地址定位
sizeof(B) = 4	// 有虚表指针,占sizeof(void*)字节

子类的虚表

Visual Studio 可以使用自带的命令行工具查看类的内存布局。在 Visual Studio 2022 中是如下工具:

在这里插入图片描述

命令是:cl /d1 reportSingleClassLayout<ClassName> xxx.cpp

例如:cl /d1 reportSingleClassLayoutA demo.cpp 即,在 demo.cpp 中查看 class A 的内存布局。

class A
{
public:virtual void vfunc1();
private:int a;
};class B
{
public:virtual void vfunc2();
private:int b;
};class C1 : public A
{
public:virtual void vfunc3();
private:int c;
};class C2 : public A, public B
{
public:virtual void vfunc3();
private:int c;
};

class C1 的内存布局是:

class C1        size(12):+---0      | +--- (base class A)0      | | {vfptr}4      | | a| +---8      | c+---C1::$vftable@:| &C1_meta|  00      | &A::vfunc11      | &C1::vfunc3

class C2 的内存布局是:

class C2        size(20):+---0      | +--- (base class A)0      | | {vfptr}4      | | a| +---8      | +--- (base class B)8      | | {vfptr}
12      | | b| +---
16      | c+---C2::$vftable@A@:| &C2_meta|  00      | &A::vfunc11      | &C2::vfunc3C2::$vftable@B@:| -80      | &B::vfunc2

C++ 类虚表中函数顺序规则

  1. 从基类开始,按照申明顺序每遇到一个不是重写的虚函数,就记录在表中
  2. 如果有重载,则提前重载的虚函数
  3. 依次循环遍历子类,如果遇到重写,则替换相应的虚函数

举例:

class A
{
public:virtual void vfunc1() = 0;virtual void vfunc2() = 0;virtual void vfunc1(int x) = 0;virtual void vfunc3() = 0;void vfunc4();void vfunc4(int x);virtual void vfunc1(int x, int y) = 0;
};class B : public A
{
public:virtual void vfunc1(int x) = 0;virtual void vfunc4() = 0;void vfunc5();virtual void vfunc2(int x) = 0;
}

请问B的虚表是应该是什么样的?

  1. 遍历A中的虚函数

    void A::vfunc1();
    

    由于 vfunc1 有两个重载,按照第 2 条规则,依次提前重载函数:

    void A::vfunc1();
    void A::vfunc1(int x);
    void A::vfunc1(int x, int y);
    
  2. 继续遍历A中的虚函数

    void A::vfunc1();
    void A::vfunc1(int x);
    void A::vfunc1(int x, int y);
    void A::vfunc2();
    void A::vfunc3();
    
  3. 由于B 重写了 Avoid vfunc1(int x) 函数,所以将表中对应的函数替换

    void A::vfunc1();
    void B::vfunc1(int x);
    void A::vfunc1(int x, int y);
    void A::vfunc2();
    void A::vfunc3();
    
  4. 添加 B::vfunc4() 到虚表中

    void A::vfunc1();
    void B::vfunc1(int x);
    void A::vfunc1(int x, int y);
    void A::vfunc2();
    void A::vfunc3();
    void B::vfunc4();
    
  5. 由于 B::vfunc2(int x) 没有重写A中的函数,按照规则 1 添加到虚表中

    void A::vfunc1();
    void B::vfunc1(int x);
    void A::vfunc1(int x, int y);
    void A::vfunc2();
    void A::vfunc3();
    void B::vfunc4();
    void B::vfunc2(int x);
    

在这里插入图片描述
在这里插入图片描述

搞清楚虚表有什么用?

答:为了ABI兼容

举例:

某工程师写了这样一个 SDK:

// awesome.h
class IAwesomeSDK
{
public:virtual void foo() = 0;virtual void bar(int x) = 0;
};extern "C" {// 创建SDK实例
IAwesomeSDK *createAwesomeInstance();// 销毁SDK实例
void destroyAwesomeInstance();} // extern "C"// 二次开发用户这样对其进行使用:// demo.cpp
int main(int argc, char **argv)
{IAwesomeSDK *sdk = createAwesomeInstance();sdk->foo();sdk->bar();destroyAwesomeInstance();return 0;
}

如果保证新发布的动态库可以兼容之前的程序(集成DLL的程序不需要重新编译,就可以使用新DLL),那么动态库中添加功能需要注意:

  1. 只能在类最后添加新的虚函数

    class IAwesomeSDK
    {
    public:virtual void feature1() = 0;		// 错误virtual void foo() = 0;virtual void bar(int x) = 0;
    };
    
  2. 添加的新函数可以与旧函数重名(重载)

    class IAwesomeSDK
    {
    public:virtual void foo() = 0;virtual void bar(int x) = 0;virtual void bar() = 0;				// 错误
    };
    
  3. 可以修改旧函数的签名(参数,返回值,限定符等)

    class IAwesomeSDK
    {
    public:virtual void foo(int x = 0) = 0;	// 错误virtual void bar(int x) = 0;
    };
    
  4. 可以重新排序旧函数

    class IAwesomeSDK
    {
    public:virtual void bar(int x) = 0;		// 错误virtual void foo() = 0;				// 错误
    };
    

这时你要添加一个新功能,还希望旧程序可以不重新编译替换新DLL,你可以这么做:

class IAwesomeSDK
{
public:virtual void foo() = 0;virtual void bar(int x) = 0;virtual void feature() = 0;			// 正确
};

导出DLL注意事项

  1. 申请和释放内存保持在同一模块。

  2. 最好不要在接口处使用STL库,除非编译器选项一致、STL实现一致、系统平台一致。

class IAwesomeSDK
{
public:virtual void foo() = 0;virtual void bar(int x) = 0;virtual std::string feature() = 0;			// 错误,模块内申请,模块外释放
};

C++ 虚析构函数在虚表中位置说明

先说结论:如果类中含有虚析构函数,其受约束和普通虚函数一致:

  1. 从基类开始,按照申明顺序每遇到一个不是重写的虚函数,就记录在表中
  2. 如果有重载,则提前重载的虚函数
  3. 依次循环遍历子类,如果遇到重写,则替换相应的虚函数

数据测试如下:(环境:Visual Studio 2022,默认配置)

  • 测试项1:没有虚析构函数时,虚表中的排布情况如下图:
    在这里插入图片描述

    在这里插入图片描述

  • 测试项2:虚析构函数位于类首时:
    在这里插入图片描述

    在这里插入图片描述

  • 测试项3:虚析构函数位于有重载的虚函数前面时:
    在这里插入图片描述

    在这里插入图片描述

  • 测试项4:虚析构函数位于非重载的虚函数前面时:
    在这里插入图片描述

    在这里插入图片描述

更多ABI 相关文章

  • 【1】C++ 编程必看!超万字深度解析API与ABI兼容性的关键问题

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

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

相关文章

C语言指针相关知识(第五篇章)(非常详细版)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、sizeof和strlen对比二、数组之间的比较&#xff08;依据strlen和sizeof来呈现&#xff09;&#xff08;一&#xff09;、一维整型数组&#xff08;二&#…

Value-Based Reinforcement Learning(2)

Temporal Difference &#xff08;TD&#xff09; Learning 上节已经提到了如果我们有DQN&#xff0c;那么agent就知道每一步动作如何做了&#xff0c;那么DQN如何训练那&#xff1f;这里面使用TD算法。 简略分析&#xff1a; 是的估计 是的估计 所以&#xff1a; Deep Re…

对vue3/core源码ref.ts文件API的认识过程

对toRef()API的认识的过程: 最开始认识toRef()是从vue3源码中的ref.ts看见的,右侧GPT已经举了例子 然后根据例子,在控制台输出ref对象是什么样子的: 这就是ref对象了,我们根据对象中有没有__v_isRef来判断是不是一个ref对象,当对象存在且__v_isRef true的时候他就判定为是一个…

Linux-组管理和权限管理

1 Liunx组的基本介绍&#xff1a; 在Linux中的每个用户必须属于一个组&#xff0c;不能独立于组外。在Linux中每个文件都有所有者、所在组、其他组的概念 所有者所在组其它组改变用户所在的组 2 文件/目录的所有者 一般文件的创建者&#xff0c;谁创建了该文件&#xff0c;就…

从程序被SQL注入来MyBatis 再谈 #{} 与 ${} 的区别

缘由 最近在的一个项目上面&#xff0c;发现有人在给我搞 SQL 注入&#xff0c;我真的想说我那么点资源测试用的阿里云服务器&#xff0c;个人估计哈&#xff0c;估计能抗住他的请求。狗头.png 系统上面的截图 数据库截图 说句实在的&#xff0c;看到这个之后我立马就是在…

游戏找不到d3dcompiler_43.dll怎么办,教你5种可靠的修复方法

在电脑使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“找不到d3dcompiler43.dll”。这个问题通常出现在游戏或者图形处理软件中&#xff0c;它会导致程序无法正常运行。为了解决这个问题&#xff0c;我经过多次尝试和总结&#xff0c;找到了以下五…

idea2023的git从dev分支合并到主分支master

1.本地项目切换到主分支master 右键项目-git-Branches 依次点击项目-Remote-Origin-master-CheckOut 现在你的idea中的这个项目就是远程master分支的代码了。 2.合并dev分支到master 右击项目-git-Merge 选择origin-dev 点击Merge按钮&#xff0c;此时只是合并到本地的maste…

每日一题---有效的括号问题

文章目录 前言1.题目以及分析2.参考代码 前言 前面我们学习了栈的相关操作&#xff0c;现在我们做一道题&#xff0c;进行巩固 Leetcode—有效的括号 1.题目以及分析 这道题就可以使用栈进行操作&#xff0c;因为把最左边的括号当成栈底&#xff0c;最右边的是栈顶&#xff0c…

【每日刷题】Day49

【每日刷题】Day49 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 110. 平衡二叉树 - 力扣&#xff08;LeetCode&#xff09; 2. 501. 二叉搜索树中的众数 - 力扣&…

基于YOLOv8的车牌检测与识别(CCPD2020数据集)

前言 本篇博客主要记录在autodl服务器中基于yolov8实现车牌检测与识别&#xff0c;以下记录实现全过程~ yolov8源码&#xff1a;GitHub - ultralytics/ultralytics: NEW - YOLOv8 &#x1f680; in PyTorch > ONNX > OpenVINO > CoreML > TFLite 一、环境配置 …

27【Aseprite 作图】盆栽——拆解

1 橘子画法拆解 (1)浅色3 1 0;深色0 2 3 就可以构成一个橘子 (2)浅色 2 1;深色1 0 (小个橘子) (3)浅色 2 1 0;深色1 2 3 2 树根部分 (1)底部画一条横线 (2)上一行 左空2 右空1 【代表底部重心先在右】 (3)再上一行,左空1,右空1 (4)再上一行,左突出1,…

省市区(输入code) 转相应省市区工具类(两种方式)

方式一 通过调用接口&#xff08;时间高达1s&#xff09; package cn.iocoder.yudao.module.supplier.utils;import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element;import java.io.BufferedReader; import java.io.InputStreamReader; i…

Java 泛型基础

目录 1. 为什么使用泛型 2. 泛型的使用方式 2.1. 泛型类 2.2. 泛型接口 2.3. 泛型方法 3. 泛型涉及的符号 3.1. 类型通配符"?" 3.2. 占位符 T/K/V/E 3.3. 占位符T和通配符&#xff1f;的区别。 4. 泛型不变性 5. 泛型编译时擦除 1. 为什么使用泛型 Java 为…

基于深度学习的入侵检测系统综述文献概述

好长时间不发博客了&#xff0c;不是因为我摆烂了&#xff0c;是我换研究方向了&#xff0c;以后我就要搞科研了。使用博客记录我的科研故事&#xff0c;邀诸君共同见证我的科研之路。 1、研究方向的背景是什么&#xff1f; &#xff08;1&#xff09;互联网发展迅速&#xff…

基于ssm的蛋糕商城系统java项目jsp项目javaweb

文章目录 蛋糕商城系统一、项目演示二、项目介绍三、系统部分功能截图四、部分代码展示五、底部获取项目源码&#xff08;9.9&#xffe5;带走&#xff09; 蛋糕商城系统 一、项目演示 蛋糕商城管理系统 二、项目介绍 系统角色 : 管理员、用户 一&#xff0c;管理员 管理员有…

Mixed-precision计算原理(FP32+FP16)

原文&#xff1a; https://lightning.ai/pages/community/tutorial/accelerating-large-language-models-with-mixed-precision-techniques/ This approach allows for efficient training while maintaining the accuracy and stability of the neural network. In more det…

【排序算法】选择排序以及需要注意的问题

选择排序的基本思想&#xff1a;每一次从待排序的数据元素中选出最小&#xff08;或最大&#xff09;的一个元素&#xff0c;存放在序列的起始位置&#xff0c;直到全部待排序的数据元素排完 。 第一种实现方法&#xff1a; void SelectSort(int* arr, int n) {for (int j 0…

【kubernetes】探索k8s集群中金丝雀发布后续 + 声明式资源管理yaml

目录 一、K8S常见的发布方式 1.1蓝绿发布 1.2灰度发布&#xff08;金丝雀发布&#xff09; 1.3滚动发布 二、金丝雀发布 三、声明式管理方法 3.1YAML 语法格式 3.1.1查看 api 资源版本标签 3.1.2查看资源简写 3.2YAML文件详解 3.2.1Deployment.yaml 3.2.2Pod.yaml …

C++系列-C/C++内存管理方式

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” C/C内存分布 在这篇文章开始之前&#xff0c;我们先以一道题目来进行引入&#xff1a; int glovalvar 1; static int staticGlovalvar 1; void Test() {static int staticva…

Java进阶学习笔记27——StringBuilder、StringBuffer

StringBuilder&#xff1a; StringBuilder代表可变字符串对象&#xff0c;相当于一个容器&#xff0c;它里面装的字符串是可以改变的&#xff0c;就是用来操作字符串的。 好处&#xff1a; StringBuilder比String更适合做字符串的修改操作&#xff0c;效率会更高&#xff0c;…