深度解讀《深度探索C++對象模型》之C++虛函數實現分析(三)

来源:https://www.cnblogs.com/isharetech/p/18195731
-Advertisement-
Play Games

本系列深入分析編譯器對於C++虛函數的底層實現,最後分析C++在多態的情況下的性能是否有受影響,多態究竟有多大的性能損失。 ...


“深度解讀《深度探索C++對象模型》”系列已經在CSDN上和我的公眾號上更新完畢,請有需要的同學移步到我的CSDN主頁里去閱讀,主頁地址:https://blog.csdn.net/iShare_Carlos?spm=1010.2135.3001.5421
或者敬請關註我的公眾號:iShare愛分享

前面兩篇請從這裡閱讀:
深度解讀《深度探索C++對象模型》之C++虛函數實現分析(一)
深度解讀《深度探索C++對象模型》之C++虛函數實現分析(二)

虛繼承情況下的虛函數和多態的實現分析

虛繼承如果再加上多重繼承關係,或者具有兩層以上的虛繼承關係,那麼編譯器對於虛函數的支持簡直像進了迷宮一樣讓人眼花繚亂,它們的關係讓人撲朔迷離。其實在實際的應用中很少會出現這樣的設計,也不建議這樣做。我們還是以一個較為常用的只有一層的虛繼承關係的例子來講解對於虛函數的支持,如以下的例子:

#include <cstdio>

class Base {
public:
    virtual ~Base() = default;
    virtual void virtual_func1() { printf("%s\n", __PRETTY_FUNCTION__); }
    virtual void virtual_func2() { printf("%s\n", __PRETTY_FUNCTION__); }
    int b = 0;
};
class Derived: virtual public Base {
public:
    virtual ~Derived() = default;
    void virtual_func2() override { printf("%s\n", __PRETTY_FUNCTION__); }
    virtual void virtual_func3()  { printf("%s\n", __PRETTY_FUNCTION__); }
    int d = 0;
};

int main() {
    Derived* pd = new Derived;
    pd->virtual_func1();
    pd->virtual_func2();
    pd->virtual_func3();
    Base* pb = pd;
    pb->virtual_func1();
    pb->virtual_func2();
    delete pd;
    return 0;
}

上面的代碼中繼承關係雖然只是單一繼承,但由於是虛繼承,所以它不像普通的單繼承那樣,基類的子類部分和對象的起始地址是對齊的,虛函數表也共用同一個,由於虛繼承的關係,虛基類的子類部分是共用的,一般編譯器的實現會把它放到對象佈局的最尾端,即在所有具體繼承的子對象和子類之後,也不和任何子對象共用虛函數表,它自己單獨擁有一個虛函數表。所以上面的代碼編譯器將會產生兩個虛函數表,一個是Derived子類的,一個是Base虛基類的,只不過編譯器把兩個表合併在一起,兩個子對象(Derived和Base)的虛函數表指針被設置指向不同的偏移地址,看看上面代碼對應的彙編代碼中的虛函數表:

vtable for Derived:
    .quad   16
    .quad   0
    .quad   typeinfo for Derived
    .quad   Derived::~Derived() [complete object destructor]
    .quad   Derived::~Derived() [deleting destructor]
    .quad   Derived::virtual_func2()
    .quad   Derived::virtual_func3()
    .quad   -16
    .quad   0
    .quad   -16
    .quad   -16
    .quad   typeinfo for Derived
    .quad   virtual thunk to Derived::~Derived() [complete object destructor]
    .quad   virtual thunk to Derived::~Derived() [deleting destructor]
    .quad   Base::virtual_func1()
    .quad   virtual thunk to Derived::virtual_func2()

Derived對象的虛函數表被設置指向上面的第5行的位置,Base虛基類的虛函數表被設置指向第14行的位置,這些事情都是編譯器在預設析構函數中生成的代碼來完成的,具體的分析可以見另外一篇文章《深度解讀<深度探索C++對象模型>之預設構造函數》。因為虛繼承的存在,上面的表中除了支持多態的虛函數和RTTI信息外,還包含了支持虛繼承的信息,主要就是一些正負偏移值,用來在有需要時調整this指針,如第2行的16就是從Derived對象的起始地址調整到Base虛基類子對象的起始地址,第9到12行的-16用於從Base虛基類子對象調整回Derived對象的起始地址。上面部分是主表,下麵部分是次表,主表中是Derived類定義的虛函數:虛析構函數、virtual_func2和virtual_func3兩個虛函數,次表是從Base虛基類繼承而來的虛函數,包括了虛析構函數、virtual_func1和virtual_func2兩個虛函數,其中虛析構函數和virtual_func2虛函數在Derived類中進行了改寫,所以這裡存放的不是真正的虛函數實例的地址,而是指向thunk技術實現的一段彙編代碼,彙編代碼里會跳轉到真實的虛函數實例中執行。

虛繼承下支持虛函數的困難點主要在於兩方面:一個是通過Derived類型的指針調用Base虛基類中的虛函數;另一個是通過Base虛基類類型的指針調用Derived類的虛函數。它們的調用關係跟多重繼承下處理第二及後繼基類的方式很相似,下麵我們以這兩點分別來講解。

  • 通過Derived類型的指針調用Base虛基類中的虛函數

在上面C++代碼中的第20到22行的三行調用中,對virtual_func2和virtual_func3虛函數的調用,因為這兩個虛函數存在於Derived類的虛函數表中,所以對這兩個的調用採用的是常規的調用方法。對virtual_func1虛函數的調用,因為virtual_func1虛函數是從Base虛基類繼承來的且在Derived類中沒有進行改寫,因此它只存在於Base虛基類的虛函數表中,調用它之前先要進行this指針的調整,讓this指針指向Base子對象的起始地址,再通過Base子對象的虛函數表指針來定址到它的虛函數表,並調用對應的虛函數,下麵是它的彙編代碼:

mov     rax, qword ptr [rbp - 16]
mov     rcx, qword ptr [rax]
mov     rcx, qword ptr [rcx - 24]
mov     rdi, rax
add     rdi, rcx
mov     rax, qword ptr [rax + rcx]
call    qword ptr [rax + 16]

[rbp - 16]棧空間存放的是Derived對象的起始地址,對其取值即是虛函數表指針(如不熟悉請參考《深度解讀<深度探索C++對象模型>之C++對象的記憶體佈局》),它指向的是Derived類的虛函數表的起始地址,也即是上表中的第5的位置,[rcx - 24]的意思是往上偏移24位元組並取值,往上偏移24位元組即指向了表的開頭位置,它的值是16,這個值就是上面介紹的用於支持虛繼承調整this指針的作用,然後上面彙編代碼的第4、5行把它加到rdi上,rdi寄存器存放的是Derived對象的起始地址,rdi寄存器(作為this指針)也將作為第7行調用虛函數時的參數。第6行的[rax + rcx]的意思是Derived對象的起始地址加上16偏移值然後取值,它是Base子對象的虛函數表指針(指向上表中的第14行),然後在第7行代碼的調用時再加上16的偏移值即是virtual_func1虛函數對應的地址,即上表中的第16行。

  • 通過Base虛基類類型的指針調用Derived類的虛函數

通過Base虛基類類型的指針調用Derived類的虛析構函數和virtual_func2虛函數,採用的是相同的實現方法,即thunk技術。所以放在一起來講,先來看下它們的彙編代碼:

virtual thunk to Derived::~Derived() [deleting destructor]:	# @virtual thunk to Derived::~Derived() [deleting destructor]
    push    rbp
    mov     rbp, rsp
    mov     qword ptr [rbp - 8], rdi
    mov     rdi, qword ptr [rbp - 8]
    mov     rax, qword ptr [rdi]
    mov     rax, qword ptr [rax - 24]
    add     rdi, rax
    pop     rbp
    jmp     Derived::~Derived() [deleting destructor] # TAILCALL
# 另一個虛析構函數的代碼差不多,這裡省略

virtual thunk to Derived::virtual_func2():	# @virtual thunk to Derived::virtual_func2()
    push    rbp
    mov     rbp, rsp
    mov     qword ptr [rbp - 8], rdi
    mov     rdi, qword ptr [rbp - 8]
    mov     rax, qword ptr [rdi]
    mov     rax, qword ptr [rax - 40]
    add     rdi, rax
    pop     rbp
    jmp     Derived::virtual_func2()    # TAILCALL

通過Base類型的指針來調用Derived類的虛析構函數的場景是:Base類型的指針指向Derived的對象,然後調用了delete函數釋放這個對象,這時調用的是在Base子對象的虛函數表中的虛析構函數,它是thunk技術實現的一段彙編代碼。virtual_func2虛函數定義在Derived類中,又是對Base虛基類中的virtual_func2虛函數的改寫,所以存在於兩個虛函數表中,但實際的函數實例只有一個,在Base虛基類的虛函數表中存放的是thunk技術實現的一段彙編代碼。

上面的兩個函數都是thunk技術生成的彙編代碼,代碼的內容基本一樣,只是在最後一行跳轉到不同的函數中去執行。首先將this指針(保存在rdi寄存器中,這時指向Base子對象的地址)保存到[rbp - 8]的棧空間中,然後取值並保存到rax寄存器中,這裡取到的值是Base子對象中的虛函數表指針,即指向上表中第14行的位置,然後減去24(或40)的偏移量並取值,這兩處的值都是-16,然後加上rdi中,rdi保存的是Base子對象的地址,向下偏移16位元組後回到Derived對象的起始地址,然後跳轉到相應的函數中去執行。

“深度解讀《深度探索C++對象模型》”系列已經在CSDN上和我的公眾號上更新完畢,請有需要的同學移步到我的CSDN主頁里去閱讀,主頁地址:https://blog.csdn.net/iShare_Carlos?spm=1010.2135.3001.5421
或者敬請關註我的公眾號:iShare愛分享


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 請註意以下繼承體系中各class的constructors寫法: 1 class CPoint 2 { 3 public: 4 CPoint(float x=0.0) 5 :_x(x){} 6 7 float x() {return _x;} 8 void x(float xval){_x=xval ...
  • NumPy 分割數組 NumPy 提供了 np.array_split() 函數來分割數組,將一個數組拆分成多個較小的子數組。 基本用法 語法: np.array_split(array, indices_or_sections, axis=None) array: 要分割的 NumPy 數組。 i ...
  • REST(Representational State Transfer),表現形式狀態轉換,它是一種軟體架構風格 當我們想表示一個網路資源的時候,可以使用兩種方式: 傳統風格資源描述形式 http://localhost/user/getById?id=1 查詢id為1的用戶信息 http://l ...
  • title: Django 自定義管理命令:從入門到高級 date: 2024/5/16 18:34:29 updated: 2024/5/16 18:34:29 categories: 後端開發 tags: Django 自定義命令 入門教程 高級技巧 命令創建 命令使用 自定義管理 第 1 章 ...
  • 目錄簡介工作流程核心架構核心模塊介紹DataX調度流程支持的數據實踐下載環境執行流程引用 簡介 DataX是一個數據同步工具,可以將數據從一個地方讀取出來並以極快的速度寫入另外一個地方。常見的如將mysql中的數據同步到另外一個mysql中,或者另外一個mongodb中。 工作流程 read:設置一 ...
  • 本文記錄我在對接位元組旗下產品火山雲旗下雲游戲產品 OpenApi 介面文檔時遇到的坑,希望能幫助大家(火山雲旗下雲游戲產品的文檔坑很多,我算是從零到一都踩了一遍,特此記錄,希望大家引以為鑒)。 1. 文檔問題 很經典的開局一張圖,對接全靠問, 這裡給大家強調下,當要跟第三方產品對接時,一定要確認拿到 ...
  • 抽象類與介面的理解、設計思路與實際用途 在面向對象的編程中,抽象類和介面是兩個非常重要的概念,它們為開發者提供了創建可重用、可擴展和可維護代碼的基礎。下麵我們將從理解、設計思路和實際用途三個方面來探討抽象類和介面。 1. 抽象類(Abstract Class) 理解: 抽象類是一種不能被實例化的類, ...
  • 正文 今天是做櫃員的第一天,準確來說是半天。雖然沒什麼業務,不過還是有些手足無措。主要是真上陣了還是有些恐慌吧。 交接手續真的非常麻煩。 聽他們說,不久之後他們要去插秧什麼的,據說是黨日活動,我真心覺得有些麻,這都搞的什麼麽蛾子。前陣子還說要買扶貧戶的產品,為了完成任務,一個人攤下來得有 660 塊 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...