C++程序員光速入門匯編(三):虛函數(shù)表與虛函數(shù)調(diào)用

關(guān)鍵概念

下面會說到虛函數(shù)相關(guān)的一些重要細節(jié)。了解這些概念對于理解C++中的虛函數(shù)調(diào)用和動態(tài)類型檢查非常重要。雖然這些細節(jié)在匯編層面不一定直接可見,但它們確實影響了編譯器如何生成匯編代碼來處理虛函數(shù)調(diào)用。

虛函數(shù)表的位置

虛函數(shù)表(vtable)是一個靜態(tài)的數(shù)據(jù)結(jié)構(gòu),通常位于程序的數(shù)據(jù)段,由編譯器生成,對C++程序員透明。每個具有虛函數(shù)的類都有一個與之關(guān)聯(lián)的虛函數(shù)表。虛函數(shù)表中的每個條目都是一個指向虛函數(shù)實現(xiàn)的指針。派生類可以繼承、重寫或添加虛函數(shù)。當派生類繼承基類的虛函數(shù)時,它們在虛函數(shù)表中具有相同的偏移量。當派生類重寫虛函數(shù)時,它會在其虛函數(shù)表中用新的函數(shù)實現(xiàn)替換基類函數(shù)的指針。當派生類添加新的虛函數(shù)時,這些函數(shù)將被添加到虛函數(shù)表的末尾。

虛函數(shù)表指針的位置

每個具有虛函數(shù)的對象都有一個指向其類的虛函數(shù)表的指針。同樣,虛函數(shù)表指針由編譯器生成,對C++程序員透明。這個指針通常作為對象的第一個成員變量存儲,位于對象內(nèi)存布局的起始位置。在C++中,我們可以通過指向?qū)ο蟮闹羔榿碓L問虛函數(shù)表指針。例如,如果pAnimal是一個指向Animal對象的指針,我們可以通過(void**pAnimal)[0]來訪問其虛函數(shù)表的第一個條目。

虛函數(shù)地址在虛函數(shù)表中的位置和偏移

虛函數(shù)在虛函數(shù)表中的位置是由編譯器決定的。通常,編譯器會根據(jù)虛函數(shù)在類中的聲明順序來分配偏移量。假設(shè)有一個基類Animal,它具有兩個虛函數(shù):makeSound()eat()。makeSound()的虛函數(shù)表偏移量可能是0,而eat()的偏移量可能是1。當一個派生類(如Dog)重寫了makeSound()函數(shù)時,它的虛函數(shù)表中makeSound()的偏移量仍然是0,但指向了Dog::makeSound的實現(xiàn)。如果pAnimal是一個指向Animal對象的指針,我們可以通過(void**pAnimal)[1]訪問eat函數(shù)在虛函數(shù)表中的條目。關(guān)于虛函數(shù)表和虛函數(shù)表指針的位置,以及虛函數(shù)地址在虛函數(shù)表中的位置和偏移,會在后續(xù)的MASM匯編代碼有所體現(xiàn)。

同一個基類派生的子類的虛函數(shù)表是否共享

虛函數(shù)表不會在派生類之間共享。每個具有虛函數(shù)的類都有一個與之關(guān)聯(lián)的虛函數(shù)表。雖然派生類可以繼承、重寫或添加虛函數(shù),但是它們的虛函數(shù)表是不同的。在某些情況下,如果派生類沒有重寫任何虛函數(shù),編譯器可能會優(yōu)化并共享虛函數(shù)表,但這取決于編譯器的實現(xiàn)。

多繼承和虛擬繼承

虛擬繼承和多繼承可能會使虛函數(shù)調(diào)用變得更為復(fù)雜,因為編譯器需要生成額外的代碼來處理這些情況。在多繼承的情況下,派生類從多個基類繼承,因此會有多個虛函數(shù)表。派生類的對象將包含多個指向每個基類虛函數(shù)表的指針。虛函數(shù)表指針的布局和順序取決于派生類繼承基類的順序。

在虛擬繼承的情況下,虛擬基類將被放在對象內(nèi)存布局的末尾。這意味著在虛擬繼承的情況下,虛擬基類的虛函數(shù)表指針可能不位于派生類對象內(nèi)存布局的起始位置。虛擬繼承旨在解決菱形繼承問題,即多個子類繼承同一個基類,導(dǎo)致基類的多個實例被包含在最終的派生類中。虛擬繼承確保所有派生類共享單個虛擬基類的實例,從而消除了菱形繼承問題。

動態(tài)類型檢查和dynamic_cast

在C++中,RTTI(Run-Time Type Information)會為包含虛函數(shù)的類提供運行時類型信息。當類具有虛函數(shù)時,編譯器為類生成RTTI,這些信息可用于dynamic_casttypeid操作符。因此,在這種情況下,dynamic_cast可以用于安全地將基類指針轉(zhuǎn)換為派生類指針。

然而,對于沒有虛函數(shù)的類,編譯器通常不會生成RTTI。這意味著在這種情況下,dynamic_cast不能用于類型轉(zhuǎn)換,因為缺乏必要的運行時類型信息。如果你嘗試對沒有虛函數(shù)的類使用dynamic_cast,編譯器會報錯。這就是為什么我們需要使用static_cast進行類型轉(zhuǎn)換。static_cast是在編譯時進行的類型轉(zhuǎn)換,它不依賴于RTTI。這意味著static_cast可以用于沒有虛函數(shù)的類之間的類型轉(zhuǎn)換。然而,這種轉(zhuǎn)換是不安全的,因為它不會在運行時檢查類型的兼容性。如果類型不匹配,可能會導(dǎo)致未定義行為。

虛析構(gòu)函數(shù)

虛析構(gòu)函數(shù)是一個特殊的虛函數(shù),用于確保派生類對象在刪除時能夠正確地調(diào)用其析構(gòu)函數(shù)。當通過基類指針刪除派生類對象時,如果基類的析構(gòu)函數(shù)不是虛函數(shù),那么只有基類的析構(gòu)函數(shù)會被調(diào)用。為了避免這種情況,我們需要將基類的析構(gòu)函數(shù)聲明為虛函數(shù)。這將確保在刪除派生類對象時,適當?shù)奈鰳?gòu)函數(shù)會被調(diào)用,從而正確地釋放資源。在MASM匯編程序中,可以看到派生類的虛析構(gòu)函數(shù)內(nèi)部,調(diào)用基類的虛析構(gòu)函數(shù),完成整個析構(gòu)過程。這將在后續(xù)的代碼示例中得到體現(xiàn)。

程序示例

同樣,下面會列出一段C++程序,以及可能生成的對應(yīng)的MASM匯編程序。

#include <iostream>

class Base {
public:
    virtual void foo() {
        std::cout << "Base::foo()" << std::endl;
    }
    virtual ~Base() {
        std::cout << "Base destructor" << std::endl;
    }
};

class Derived : public Base {
public:
    void foo() override {
        std::cout << "Derived::foo()" << std::endl;
    }
    virtual ~Derived() {
        std::cout << "Derived destructor" << std::endl;
    }
};

int main() {
    Base *b = new Derived();
    b->foo();
    delete b;
    return 0;
}

該C++程序的輸出將是:

Derived::foo()
Derived destructor
Base destructor

由于C++編譯器會生成大量的匯編代碼,包括一些庫函數(shù)和系統(tǒng)調(diào)用,因此在這里只提供一個簡化版的MASM匯編程序。這個簡化版的匯編程序只包含虛函數(shù)調(diào)用和虛析構(gòu)函數(shù)調(diào)用的關(guān)鍵部分。

; 省略其他導(dǎo)入和庫函數(shù)聲明

; 數(shù)據(jù)段定義
.data
    ; 虛函數(shù)表
    _vftable_Base   dd OFFSET _Base_foo, OFFSET _Base_destructor
    _vftable_Derived dd OFFSET _Derived_foo, OFFSET _Derived_destructor

; 代碼段定義
.code
; 派生類的虛函數(shù)
_Derived_foo PROC
    ; 輸出 "Derived::foo()"
    ; 省略具體實現(xiàn)
    ret
_Derived_foo ENDP

; 派生類的虛析構(gòu)函數(shù)
_Derived_destructor PROC
    ; 輸出 "Derived destructor"
    ; 省略具體實現(xiàn)

    ; 調(diào)用基類的析構(gòu)函數(shù)
    call _Base_destructor

    ret
_Derived_destructor ENDP

; 基類的虛函數(shù)
_Base_foo PROC
    ; 輸出 "Base::foo()"
    ; 省略具體實現(xiàn)
    ret
_Base_foo ENDP

; 基類的虛析構(gòu)函數(shù)
_Base_destructor PROC
    ; 輸出 "Base destructor"
    ; 省略具體實現(xiàn)
    ret
_Base_destructor ENDP

; 程序入口
_main PROC
    ; 創(chuàng)建派生類對象
    ; 省略具體實現(xiàn)

    ; 調(diào)用虛函數(shù)
    mov ecx, eax ; 將對象指針移到ecx寄存器
    mov eax, [eax] ; 獲取虛函數(shù)表指針
    call dword ptr [eax] ; 調(diào)用虛函數(shù)(偏移為0)

    ; 刪除對象
    mov eax, [ecx] ; 獲取虛函數(shù)表指針
    call dword ptr [eax + 4] ; 調(diào)用相應(yīng)的虛析構(gòu)函數(shù)(偏移為4)

    ; 退出程序
    ; 省略具體實現(xiàn)
_main ENDP

END

數(shù)據(jù)段定義了兩個虛函數(shù)表,一個是基類(_vftable_Base)的虛函數(shù)表,一個是派生類(_vftable_Derived)的虛函數(shù)表。虛函數(shù)表包含了類中虛函數(shù)的地址。C++程序編譯為MASM匯編代碼時,會顯式地將所有虛函數(shù)表的地址定義在數(shù)據(jù)段中。

; 數(shù)據(jù)段定義
.data
    ; 虛函數(shù)表
    _vftable_Base   dd OFFSET _Base_foo, OFFSET _Base_destructor
    _vftable_Derived dd OFFSET _Derived_foo, OFFSET _Derived_destructor

派生類的虛函數(shù)和虛析構(gòu)函數(shù):_Derived_foo 和 _Derived_destructor 分別是派生類的虛函數(shù)和虛析構(gòu)函數(shù)的實現(xiàn)。在虛析構(gòu)函數(shù)中,我們調(diào)用基類的析構(gòu)函數(shù),確保資源得到正確釋放。

; 派生類的虛函數(shù)
_Derived_foo PROC
    ; 輸出 "Derived::foo()"
    ; 省略具體實現(xiàn)
    ret
_Derived_foo ENDP

; 派生類的虛析構(gòu)函數(shù)
_Derived_destructor PROC
    ; 輸出 "Derived destructor"
    ; 省略具體實現(xiàn)

    ; 調(diào)用基類的析構(gòu)函數(shù)
    call _Base_destructor

    ret
_Derived_destructor ENDP

基類的虛函數(shù)和虛析構(gòu)函數(shù):_Base_foo 和 _Base_destructor 分別是基類的虛函數(shù)和虛析構(gòu)函數(shù)的實現(xiàn)。

; 基類的虛函數(shù)
_Base_foo PROC
    ; 輸出 "Base::foo()"
    ; 省略具體實現(xiàn)
    ret
_Base_foo ENDP

; 基類的虛析構(gòu)函數(shù)
_Base_destructor PROC
    ; 輸出 "Base destructor"
    ; 省略具體實現(xiàn)
    ret
_Base_destructor ENDP

_main 函數(shù)是程序的入口點。在這里,我們創(chuàng)建派生類對象、調(diào)用虛函數(shù)以及刪除對象。重點分析一下調(diào)用虛函數(shù)和刪除對象時調(diào)用虛析構(gòu)函數(shù)的部分。

; 程序入口
_main PROC
    ; 創(chuàng)建派生類對象
    ; 省略具體實現(xiàn)

    ; 調(diào)用虛函數(shù)
    mov ecx, eax ; 將對象指針移到ecx寄存器
    mov eax, [eax] ; 獲取虛函數(shù)表指針
    call dword ptr [eax] ; 調(diào)用虛函數(shù)(偏移為0)

    ; 刪除對象
    mov eax, [ecx] ; 獲取虛函數(shù)表指針
    call dword ptr [eax + 4] ; 調(diào)用相應(yīng)的虛析構(gòu)函數(shù)(偏移為4)

    ; 退出程序
    ; 省略具體實現(xiàn)
_main ENDP
  • 虛函數(shù)調(diào)用過程:
  1. 將對象指針移到ecx寄存器:mov ecx, eax
  2. 獲取虛函數(shù)表指針:mov eax, [eax]
  3. 調(diào)用虛函數(shù)(偏移為0):call dword ptr [eax]

mov ecx, eax 的意義是將對象指針(this指針)放入 ecx 寄存器中。在這個示例中,eax 寄存器包含了對象指針(即 this 指針)。將它移動到 ecx 寄存器是為了后面調(diào)用析構(gòu)函數(shù)時仍能使用該指針。在調(diào)用成員函數(shù)時,為了能夠在成員函數(shù)中獲取到this指針,一般都會進行這個過程,這對應(yīng)了本系列第一篇文章中說到:ecx寄存器存儲的很可能是this指針。

mov eax, [eax] 相當于一次指針解引用。在C++中,具有虛函數(shù)的類的對象內(nèi)存布局中,第一個成員通常是一個指向虛函數(shù)表的指針。因此,當使用 mov eax, [eax] 時,我們實際上是對對象指針進行了一次解引用,從而獲得了指向虛函數(shù)表的指針。
在匯編代碼中,call dword ptr [eax] 這條指令用于調(diào)用虛函數(shù)。在這里,dword ptr 是一個操作數(shù)類型修飾符,表示接下來要訪問的內(nèi)存數(shù)據(jù)是一個雙字(double word,32位)大小的數(shù)據(jù)。

call dword ptr [eax] 這條指令利用 eax 寄存器中的虛函數(shù)表指針調(diào)用虛函數(shù)。dword ptr 修飾符表示從內(nèi)存中讀取一個雙字大小的數(shù)據(jù)(這里是虛函數(shù)的地址),然后 call 指令根據(jù)這個地址跳轉(zhuǎn)到相應(yīng)的函數(shù)并執(zhí)行。這樣我們就實現(xiàn)了虛函數(shù)的調(diào)用。

  • 虛析構(gòu)函數(shù)調(diào)用過程:
  1. 獲取虛函數(shù)表指針:mov eax, [ecx]
  2. 調(diào)用相應(yīng)的虛析構(gòu)函數(shù)(偏移為4):call dword ptr [eax + 4]

在析構(gòu)函數(shù)調(diào)用過程中,同樣需要先獲取虛函數(shù)表指針。接著,根據(jù)虛函數(shù)表中的偏移量,調(diào)用相應(yīng)的虛析構(gòu)函數(shù)。在這個例子中,我們調(diào)用的是偏移為4的虛析構(gòu)函數(shù),即 Derived 類的析構(gòu)函數(shù)。然后在派生類的虛析構(gòu)函數(shù)內(nèi)部,調(diào)用基類的虛析構(gòu)函數(shù),完成整個析構(gòu)過程。

可以看到,虛函數(shù)調(diào)用和虛析構(gòu)函數(shù)調(diào)用都是通過虛函數(shù)表來實現(xiàn)的。虛函數(shù)表是一個存儲虛函數(shù)地址的數(shù)組,每個具有虛函數(shù)的類都有一個虛函數(shù)表。在運行時,對象的指針通過虛函數(shù)表來找到并調(diào)用對應(yīng)的虛函數(shù)。這使得程序在運行時能夠根據(jù)對象的實際類型調(diào)用正確的虛函數(shù),實現(xiàn)了多態(tài)性。

總結(jié)

以上就是對虛函數(shù)表與虛函數(shù)調(diào)用過程的匯編層面分析。雖然上述示例能夠闡述一些基本的原理和思想,但依舊建議讀者使用Visual Studio等工具,將C++程序編譯運行,并查看編譯出的MASM代碼,通過調(diào)試來理解虛函數(shù)的調(diào)用過程。為了減少非關(guān)鍵代碼的生成,并能夠通過斷點調(diào)試,建議切換為Release模式編譯,并將優(yōu)化選項設(shè)置為已禁用(/Od)。這樣編譯出來的MASM匯編程序,理論上是比較契合示例程序中的匯編代碼整體邏輯的,也方便打斷點調(diào)試。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容