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.pswp.cn/web/16167.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/16167.shtml
英文地址,請注明出處:http://en.pswp.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;就…

Docker in Docker(DinD)原理與實踐

隨著云計算和容器化技術的快速發展&#xff0c;Docker作為開源的應用容器引擎&#xff0c;已經成為企業部署和管理應用程序的首選工具。然而&#xff0c;在某些場景下&#xff0c;我們可能需要在Docker容器內部再運行一個Docker環境&#xff0c;即Docker in Docker&#xff08;…

002 CentOS 7.9 redis-7.2.5安裝及配置

https://github.com/redis/redis https://redis.io/insight/#insight-form 安裝及配置 在CentOS 7.9上安裝和配置Redis 7.2.5版本&#xff0c;可以遵循以下詳細步驟&#xff1a; 一、準備工作 確保安裝包已準備好&#xff1a; 確認您已經下載了redis-7.2.5.tar.gz安裝包&a…

從程序被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 一、環境配置 …

python學習:基礎語句

目錄 條件語句 循環語句 for 循環 while 循環 break continue 條件語句 Python提供了 if、elif、else 來進行邏輯判斷。格式如下&#xff1a; Pythonif 判斷條件1: 執行語句1... elif 判斷條件2: 執行語句2... elif 判斷條件3: 執行語句3... else: 執行語句4…

C# 集合(六) —— 自定義集合Collection類

總目錄 C# 語法總目錄 集合六 Collection 1. 自定義集合Collection其他 1. 自定義集合Collection Collection可以對添加刪除元素或者添加刪除屬性進行事件響應。 class Person {public string name;public int age;public Person(){this.name "";this.age 0;}pub…

ubuntu 硬盤轉移

我插了兩個 文件系統&#xff1a; ubuntu 硬盤轉移&#xff1a; sudo dd if/dev/sdX1 of/dev/sdY1 bs128K convnoerror,sync statusprogressdd 的意思是DiskToDisk&#xff0c;if 是輸入文件系統&#xff0c;of是輸出文件系統。 bs是每次傳遞的數據大小。 注意&#xff1a;接…

mysql-主從同步原理

AB復制(重點) 一、什么是主從復制? 1、主從同步也叫AB復制&#xff0c;是用來建立一個和主數據庫完全一樣的數據庫環境&#xff0c;稱為從數據庫&#xff1b;主數據庫一般是準實時的業務數據庫。 2、主從復制的作用 1.做數據的熱備&#xff0c;作為后備數據庫&#xff0c;…

如何用MySQL的SQL語句來讀寫硬盤目錄文件

1.先確保創建表&#xff0c;例如起名Temp CREATE TABLE temp ( id int(11) NOT NULL AUTO_INCREMENT, image mediumblob, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT7 DEFAULT CHARSETutf8; 注意這里的image字段用mediumblog&#xff0c;就可以避免出現data too …

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 為…