1. 程式人生 > >C++ 虛擬函式分析

C++ 虛擬函式分析

C++ 虛擬函式分析

虛擬函式呼叫屬於執行時多型,在類的繼承關係中,通過父類指標來呼叫不同子類物件的同名方法,而產生不同的效果。
C++ 中的多型是通過晚繫結(物件構造時)來實現的。

用法

在函式之前宣告關鍵字virtual表示這是一個虛擬函式,在函式後增加一個 = 0 表示這是一個純虛擬函式,純虛擬函式的類不能建立具體例項。

該示例作後文分析使用,一個包含純虛擬函式的父類,一個重寫了父類方法的子類,一個無繼承的類。

struct Base {
    Base() : val(7777) {}
    virtual int fuck(int a) = 0;
    int val;
};

struct Der : public Base {
    Der() = default;
    int fuck(int a) override { return val + 4396; }
};

struct A {
    A() = default;
    void funny(int a) {}
};

int main() {
    Der der;
    Base *pbase = &der;
    pbase->fuck(sizeof(Der)); // 呼叫 Der::fuck(int a);

    A a;
    a.funny(sizeof(A));  // A::funny(int a);

    return 3;
}

實現

原來就瞭解虛擬函式是通過虛表的偏移來獲取實際呼叫函式地址來實現的,但是在何時確定這個偏移和具體的偏移細節也沒有說明,今兒個來探探究竟。

拿上面的程式碼進行反彙編獲提取部分函式,main,Base::Base(), Base::fuck(), Der::Der(), Der::fuck, A::funny() 如下:

_ZN4BaseC2Ev:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)   // 還是 main 函式的棧幀 -32(%rpb) 的地址
    leaq    16+_ZTV4Base(%rip), %rdx  // 關鍵點來了,取虛表偏移 16 的地址也就是 __cxa_pure_virtual,這裡是沒有意義的
    movq    -8(%rbp), %rax
    movq    %rdx, (%rax)     // 將 __cxa_pure_virtual 的地址存放在 地址rax 的記憶體中(這個例子中也就是main 函式的棧幀 -32(%rpb) 的地方), 
    movq    -8(%rbp), %rax   // 然後往後偏移 8 個位元組,也就是跳過虛表指標,對成員變數 val 初始化。
    movl    $7777, 8(%rax)
    nop                      // 注:上面是用這個示例中實際的地址帶入的,實際上對於一個有的類的處理是一個通用邏輯的,建構函式傳入的第一個引數 rdi 是 this 指標,由於有虛表存在的影響,這裡會修改 this 指標所在地址的內容,也就是虛表的偏移地址(非起始地址)
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   _ZN4BaseC2Ev, .-_ZN4BaseC2Ev
    .weak   _ZN4BaseC1Ev
    .set    _ZN4BaseC1Ev,_ZN4BaseC2Ev
    .section    .text._ZN3Der4fuckEi,"axG",@progbits,_ZN3Der4fuckEi,comdat
    .align 2
    .weak   _ZN3Der4fuckEi
    .type   _ZN3Der4fuckEi, @function
_ZN3Der4fuckEi:
.LFB3:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movl    %esi, -12(%rbp)
    movq    -8(%rbp), %rax
    movl    8(%rax), %eax   // 成員變數 val,val 是從 rdi 中偏移 8 位元組取的值
    addl    $4396, %eax     // val + 4396
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE3:
    .size   _ZN3Der4fuckEi, .-_ZN3Der4fuckEi
    .section    .text._ZN1A5funnyEi,"axG",@progbits,_ZN1A5funnyEi,comdat
    .align 2
    .weak   _ZN1A5funnyEi
    .type   _ZN1A5funnyEi, @function
_ZN1A5funnyEi:
.LFB4:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movl    %esi, -12(%rbp)
    nop
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE4:
    .size   _ZN1A5funnyEi, .-_ZN1A5funnyEi
    .section    .text._ZN3DerC2Ev,"axG",@progbits,_ZN3DerC5Ev,comdat
    .align 2
    .weak   _ZN3DerC2Ev
    .type   _ZN3DerC2Ev, @function
_ZN3DerC2Ev:
.LFB7:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movq    %rdi, -8(%rbp)   // rdi 是取的 main 棧幀 -32(%rbp) 的地址
    movq    -8(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN4BaseC2Ev     // Base 的建構函式,並且又把傳進來的引數作為實參傳進去了,這裡跟蹤進去
    leaq    16+_ZTV3Der(%rip), %rdx  // 取虛表偏移16位元組 _ZN3Der4fuckEi 的地址 
    movq    -8(%rbp), %rax
    movq    %rdx, (%rax)     // rax 在之前的 Base建構函式中是被修改了的,這裡將繼續修改內容,前一次的修改失效。
    nop
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE7:
    .size   _ZN3DerC2Ev, .-_ZN3DerC2Ev
    .weak   _ZN3DerC1Ev
    .set    _ZN3DerC1Ev,_ZN3DerC2Ev
    .text
    .globl  main
    .type   main, @function
main:
.LFB5:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $48, %rsp
    leaq    -32(%rbp), %rax // 取 -32(%rbp) 的地址,對應 Base *pbase;
    movq    %rax, %rdi
    call    _ZN3DerC1Ev     // 呼叫了建構函式,並且以-32(%rbp) 的地址作為引數,這裡跟蹤進去
    leaq    -32(%rbp), %rax // -32(%rbp) 被修改,該記憶體中的內容為 Der 虛表的偏移地址 
    movq    %rax, -8(%rbp)
    movq    -8(%rbp), %rax
    movq    (%rax), %rax    // rax = M[rax],取出虛表偏移中的地址
    movq    (%rax), %rdx    // rdx = M[rax] , 取出虛表偏移的內容(也就是函式地址),算上上面這是做了兩次解引用
    movq    -8(%rbp), %rax
    movl    $16, %esi       // sizeof(Der) = 16, 包含一個虛表指標和 int val;
    movq    %rax, %rdi      // 虛表偏移中的地址
    call    *%rdx           // 呼叫函式
    leaq    -33(%rbp), %rax
    movl    $1, %esi
    movq    %rax, %rdi
    call    _ZN1A5funnyEi   // 普通成員函式,實現簡單
    movl    $3, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE5:
    .size   main, .-main
    .weak   _ZTV3Der
    .section    .data.rel.ro.local._ZTV3Der,"awG",@progbits,_ZTV3Der,comdat
    .align 8
    .type   _ZTV3Der, @object
    .size   _ZTV3Der, 24
_ZTV3Der:
    .quad   0
    .quad   _ZTI3Der
    .quad   _ZN3Der4fuckEi  // Der::fuck(int a);
    .weak   _ZTV4Base
    .section    .data.rel.ro._ZTV4Base,"awG",@progbits,_ZTV4Base,comdat
    .align 8
    .type   _ZTV4Base, @object
    .size   _ZTV4Base, 24
_ZTV4Base:
    .quad   0
    .quad   _ZTI4Base
    .quad   __cxa_pure_virtual  // 純虛擬函式,無對應符號表
    .weak   _ZTI3Der
    .section    .data.rel.ro._ZTI3Der,"awG",@progbits,_ZTI3Der,comdat
    .align 8
    .type   _ZTI3Der, @object
    .size   _ZTI3Der, 24

現在是一個純虛擬函式,類中也沒有虛解構函式,通過反彙編來看一些這個實現。

_ZTV3Der_ZTV4Base 是兩個虛表,大小為 24, 8 位元組對齊,分別對應 Der 子類和 Base 父類。虛表中偏移 16 位元組(偏移大小可能和實現相關)為虛擬函式地址,每次建構函式的被呼叫的時候,會將該偏移地址儲存到父類指標所在記憶體中,所以在上程式碼中看到,在 Base 和 Der 類的構函式中都出現了設定偏移地址的操作,但是子類建構函式會覆蓋父類的修改。這樣一來,實際的函式執行地址依賴建構函式,子類物件被構造就呼叫子類的方法,父類構造就呼叫父類的方法(非純虛擬函式),實現了執行時多型。

增加一個虛擬函式後, 後面的虛擬函式地址就新增到虛表之中,如下

virtual void Base::shit() {}
void Der::shit() override {}

_ZTV3Der:
    .quad   0
    .quad   _ZTI3Der
    .quad   _ZN3Der4fuckEi
    .quad   _ZN3Der4shitEv
    .weak   _ZTV4Base
    .section    .data.rel.ro._ZTV4Base,"awG",@progbits,_ZTV4Base,comdat
    .align 8
    .type   _ZTV4Base, @object
    .size   _ZTV4Base, 32
_ZTV4Base:
    .quad   0
    .quad   _ZTI4Base
    .quad   __cxa_pure_virtual
    .quad   _ZN4Base4shitEv
    .weak   _ZTI3Der
    .section    .data.rel.ro._ZTI3Der,"awG",@progbits,_ZTI3Der,comdat
    .align 8
    .type   _ZTI3Der, @object
    .size   _ZTI3Der, 24

再呼叫另外一個虛擬函式就簡單很多了,直接地址進行偏移(這裡shit在fuck之後,所以+8)

    movq    -8(%rbp), %rax
    movq    (%rax), %rax
    addq    $8, %rax
    movq    (%rax), %rdx
    movq    -8(%rbp), %rax
    movq    %rax, %rdi
    call    *%rdx

簡單畫了一下虛擬函式執行的記憶體結構圖

相關推薦

C++ 虛擬函式分析

C++ 虛擬函式分析 虛擬函式呼叫屬於執行時多型,在類的繼承關係中,通過父類指標來呼叫不同子類物件的同名方法,而產生不同的效果。 C++ 中的多型是通過晚繫結(物件構造時)來實現的。 用法 在函式之前宣告關鍵字virtual表示這是一個虛擬函式,在函式後增加一個 = 0 表示這是一個純虛擬函式,純虛擬函式的類

C++物件模型中的虛擬函式分析

對於虛擬函式,知道它的含義,也能夠描述出來。參照百度百科,也就是“它提供了‘動態繫結’機制”。 可總是感覺有些迷糊,於是敲了一段程式碼出來試驗,一探究竟(程式設計環境是VC6.0)。對比程式碼和結果,一切都不言自明。 現在把程式碼和結果貼上來,作為儲存記錄,同時也歡迎大家提出意見,以臻完善。

C++虛擬函式在g++中的實現分析

本文是我在追查一個詭異core問題的過程中收穫的一點心得,把公司專案相關的背景和特定條件去掉後,僅取其中通用的C++虛擬函式實現部分知識記錄於此。 在開始之前,原諒我先借用一張圖黑一下C++: “無敵”的C++ 如果你也在寫C++,請一定小心…至少,你要先有所瞭解: 當你在寫虛擬函式的時候,g

C++虛擬函式表例項分析

我們先來看看程式碼: #include <iostream> using namespace std; class Base {public:    virtual void f() {cout<<"base::f"<<endl;}  &

C++虛擬函式表以及記憶體對齊文章

C++虛擬函式表以及記憶體對齊文章 C++ 物件的記憶體佈局(上) https://blog.csdn.net/haoel/article/details/3081328 C++ 物件的記憶體佈局(下) https://blog.csdn.net/haoel/article/deta

c# 虛擬函式Virtual與重寫override

C#程式碼   using System; namespace Smz.Test { class A { public virtua

【轉】C++虛擬函式

引言 C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算法。比如:模板技術,RTTI技術,虛擬函

C++虛擬函式表在虛繼承和繼承中的差別

下面的程式碼在gcc和VC中的結果 #include <cstdio> class A { public: virtual void funcaa() { printf("class A %s\n",__func__); } }; class AA:virtual pu

C++虛擬函式本質論

C++虛擬函式本質論 13.2.1 多型性原理 1.多型性概念 對多型性最簡單的理解就是一種事物有多種形態。在面向物件設計中,多型性指的是向不同的物件傳送同一訊息時,會產生不同的動作(或行為、功能)。所謂的“向不同的物件傳送同一訊息”,其實就是指呼叫不同物件的某個函式;而“產生不同的動作

C++ 虛擬函式的內部實現

單繼承的情況下 若類有虛擬函式,則在建構函式的時候編譯器會自動為類的例項(物件)在其記憶體的首部(0地址偏移處)增添一個虛擬函式表指標vfptr,指向該類的虛擬函式表。虛擬函式表中會存放該類所有的虛擬函式地址,普通函式則不會被放入其中。如果是子類重寫了父類的虛擬函式,那麼在建立虛擬函

c++虛擬函式(override)和過載函式(overload)的比較

1. 過載函式要求函式有相同的函式名稱,並有不同的引數序列;而虛擬函式則要求完全相同; 2. 過載函式可以是成員函式或友元函式,而虛擬函式只能是成員函式; 3. 過載函式的呼叫是以所傳遞引數的差別作為呼叫不同函式的依據,虛擬函式是根據物件動態型別的不同去呼叫不同

C++ 虛擬函式的兩個例子

1. 第一個例子是朋友告訴我Qt中的某個實現 1 #include <iostream> 2 3 // Qt中的某個實現 4 class A{ 5 public: 6 A() = default; 7 virtual ~A() = default; 8 9 virtua

深入淺出理解c++虛擬函式

    深入淺出理解c++虛擬函式   記得幾個月前看過C++虛擬函式的問題,當時其實就看懂了,最近筆試中遇到了虛擬函式竟然不太確定,所以還是理解的不深刻,所以想通過這篇文章來鞏固下。   裝逼一刻: 最近,本人思想發生了巨

理解C++虛擬函式

1、簡單介紹   C++虛擬函式是定義在基類中的函式,子類可以選擇重寫。在類中宣告(沒有寫函式體的為宣告)虛擬函式的格式如下: virtual void display(); 2、虛擬函式的作用   在使用指向子類物件的基類指標,並呼叫子

C++ 虛擬函式的預設引數問題

前些日子,有個同學問我一個關於虛擬函式的預設引數問題。他是從某個論壇上看到的,但是自己沒想通,便來找我。現在分享一下這個問題。先看一小段程式碼: #include <iostream> using namespace std; class A

C++虛擬函式表(含測試程式碼)

自己搞不懂C++虛擬函式之間的呼叫關係,特地花費一個下午加一個晚上查資料學習,現在把學到的發上來,供大家學習批評; 在此之前感謝這些大佬的部落格等,為我解惑甚多: 1、虛表與虛表指標 C++中的虛擬函式的實現一般是通過虛擬函式表(V-Table)來實

C++ 虛擬函式表解析

1 前言 C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現

C++虛擬函式

2018年11月12日 15:56:57 ChessC 閱讀數:7 個人分類: 一些總結

C++虛擬函式(2)

class x { char *p; public: x(int sz) {p=new char[sz];} virtual ~x(){cout << "~x()\n"; delete []p;} }; class y : public x {

C++ 虛擬函式表指標以及虛擬函式指標的確定

【摘要】 很多教材上都有介紹到虛指標、虛擬函式與虛擬函式表,有的說類物件共享一個虛擬函式表,有的說,一個類物件擁有一個虛擬函式表;還有的說,無論使用者聲明瞭多少個類物件,但是,這個VTABLE虛擬函式表只有一個;也有的在說,每個具有虛擬函式的類的物件裡面都有一