溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

C++虛函數(shù)在g++中的實(shí)現(xiàn)方法

發(fā)布時(shí)間:2020-10-23 15:19:21 來源:億速云 閱讀:147 作者:小新 欄目:編程語言

這篇文章主要介紹C++虛函數(shù)在g++中的實(shí)現(xiàn)方法,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!

探索C++虛函數(shù)在g++中的實(shí)現(xiàn)

在開始之前,原諒我先借用一張圖黑一下C++:

“無敵”的C++

如果你也在寫C++,請(qǐng)一定小心…至少,你要先有所了解: 當(dāng)你在寫虛函數(shù)的時(shí)候,g++在寫什么?

先寫個(gè)例子

為了探索C++虛函數(shù)的實(shí)現(xiàn),我們首先編寫幾個(gè)用來測(cè)試的類,代碼如下:

C++

#include <iostream>

using namespace std;

class Base1
{
public:
    virtual void f() {
        cout << "Base1::f()" << endl;
    }
};

class Base2
{
public:
    virtual void g() {
        cout << "Base2::g()" << endl;
    }
};

class Derived : public Base1, public Base2
{
public:
    virtual void f() {
        cout << "Derived::f()" << endl;
    }

    virtual void g() {
        cout << "Derived::g()" << endl;
    }

    virtual void h() {
        cout << "Derived::h()" << endl;
    }
};

int main(int argc, char *argv[])
{
    Derived ins;
    Base1 &b1 = ins;
    Base2 &b2 = ins;
    Derived &d = ins;

    b1.f();
    b2.g();
    d.f();
    d.g();
    d.h();
}

代碼采用了多繼承,是為了更多的分析出g++的實(shí)現(xiàn)本質(zhì),用UML簡(jiǎn)單的畫一下繼承關(guān)系:

C++虛函數(shù)在g++中的實(shí)現(xiàn)方法

示例代碼UML圖

代碼的輸出結(jié)果和預(yù)期的一致,C++實(shí)現(xiàn)了虛函數(shù)覆蓋功能,代碼輸出如下:

Derived::f()
Derived::g()
Derived::f()
Derived::g()
Derived::h()

開始分析!

我寫這篇文章的重點(diǎn)是嘗試解釋g++編譯在底層是如何實(shí)現(xiàn)虛函數(shù)覆蓋和動(dòng)態(tài)綁定的,因此我假定你已經(jīng)明白基本的虛函數(shù)概念以及虛函數(shù)表(vtbl)和虛函數(shù)表指針(vptr)的概念和在繼承實(shí)現(xiàn)中所承擔(dān)的作用,如果你還不清楚這些概念,建議你在繼續(xù)閱讀下面的分析前先補(bǔ)習(xí)一下相關(guān)知識(shí),陳皓的 《C++虛函數(shù)表解析》 系列是一個(gè)不錯(cuò)的選擇。

通過本文,我將嘗試解答下面這三個(gè)問題:

  1. g++如何實(shí)現(xiàn)虛函數(shù)的動(dòng)態(tài)綁定?

  2. vtbl在何時(shí)被創(chuàng)建?vptr又是在何時(shí)被初始化?

  3. 在Linux中運(yùn)行的C++程序虛擬存儲(chǔ)器中,vptr、vtbl存放在虛擬存儲(chǔ)的什么位置?

首先是第一個(gè)問題:

g++如何實(shí)現(xiàn)虛函數(shù)的動(dòng)態(tài)綁定?

這個(gè)問題乍看簡(jiǎn)單,大家都知道是通過vptr和vtbl實(shí)現(xiàn)的,那就讓我們刨根問底的看一看,g++是如何利用vptr和vtbl實(shí)現(xiàn)的。

第一步,使用 -fdump-class-hierarchy 參數(shù)導(dǎo)出g++生成的類內(nèi)存結(jié)構(gòu):

Vtable for Base1
Base1::_ZTV5Base1: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI5Base1)
8     Base1::f

Class Base1
   size=4 align=4
   base size=4 base align=4
Base1 (0xb6acb438) 0 nearly-empty
    vptr=((& Base1::_ZTV5Base1) + 8u)

Vtable for Base2
Base2::_ZTV5Base2: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI5Base2)
8     Base2::g

Class Base2
   size=4 align=4
   base size=4 base align=4
Base2 (0xb6acb474) 0 nearly-empty
    vptr=((& Base2::_ZTV5Base2) + 8u)

Vtable for Derived
Derived::_ZTV7Derived: 8u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI7Derived)
8     Derived::f
12    Derived::g
16    Derived::h
20    (int (*)(...))-0x000000004
24    (int (*)(...))(& _ZTI7Derived)
28    Derived::_ZThn4_N7Derived1gEv

Class Derived
   size=8 align=4
   base size=8 base align=4
Derived (0xb6b12780) 0
    vptr=((& Derived::_ZTV7Derived) + 8u)
  Base1 (0xb6acb4b0) 0 nearly-empty
      primary-for Derived (0xb6b12780)
  Base2 (0xb6acb4ec) 4 nearly-empty
      vptr=((& Derived::_ZTV7Derived) + 28u)

如果看不明白這些亂七八糟的輸出,沒關(guān)系(當(dāng)然能看懂更好),把上面的輸出轉(zhuǎn)換成圖的形式就清楚了:

C++虛函數(shù)在g++中的實(shí)現(xiàn)方法

vptr和vtbl

其中有幾點(diǎn)尤其值得注意:

  1. 我用來測(cè)試的機(jī)器是32位機(jī),所有vptr占4個(gè)字節(jié),每個(gè)vtbl中的函數(shù)指針也是4個(gè)字節(jié)

  2. 每個(gè)類的主要(primal)vptr放在類內(nèi)存空間的起始位置(由于我沒有聲明任何成員變量,可能看不清楚)

  3. 在多繼承中,對(duì)應(yīng)各個(gè)基類的vptr按繼承順序依次放置在類內(nèi)存空間中,且子類與第一個(gè)基類共用同一個(gè)vptr

  4. 子類中聲明的虛函數(shù)除了覆蓋各個(gè)基類對(duì)應(yīng)函數(shù)的指針外,還額外添加一份到第一個(gè)基類的vptr中(體現(xiàn)了共用的意義)

有了內(nèi)存布局后,接下來觀察g++是如何在這樣的內(nèi)存布局上進(jìn)行動(dòng)態(tài)綁定的。

g++對(duì)每個(gè)類的指針或引用對(duì)象,如果是其類聲明中虛函數(shù),使用位于其內(nèi)存空間首地址上的vptr尋找找到vtbl進(jìn)而得到函數(shù)地址。如果是父類聲明而子類未覆蓋的虛函數(shù),使用對(duì)應(yīng)父類的vptr進(jìn)行尋址。

先來驗(yàn)證一下,使用 objdump -S 得到 b1.f() 的匯編指令:

Assembly (x86)

b1.f();
 8048734:       8b 44 24 24             mov    0x24(%esp),%eax    # 得到Base1對(duì)象的地址
 8048738:       8b 00                   mov    (%eax),%eax        # 對(duì)對(duì)象首地址上的vptr進(jìn)行解引用,得到vtbl地址
 804873a:       8b 10                   mov    (%eax),%edx        # 解引用vtbl上第一個(gè)虛函數(shù)的地址
 804873c:       8b 44 24 24             mov    0x24(%esp),%eax
 8048740:       89 04 24                mov    %eax,(%esp)
 8048743:       ff d2                   call   *%edx              # 調(diào)用函數(shù)

其過程和我們的分析完全一致,聰明的你可能發(fā)現(xiàn)了,b2怎么辦呢?Derived類的實(shí)例內(nèi)存首地址上的vptr并不是Base2類的?。〈鸢笇?shí)際上是因?yàn)間++在引用賦值語句 Base2 &b2 = ins 上動(dòng)了手腳:

Assembly (x86)

Derived ins;
 804870d:       8d 44 24 1c             lea    0x1c(%esp),%eax
 8048711:       89 04 24                mov    %eax,(%esp)
 8048714:       e8 c3 01 00 00          call   80488dc <_ZN7DerivedC1Ev>
    Base1 &b1 = ins;
 8048719:       8d 44 24 1c             lea    0x1c(%esp),%eax
 804871d:       89 44 24 24             mov    %eax,0x24(%esp)
    Base2 &b2 = ins;
 8048721:       8d 44 24 1c             lea    0x1c(%esp),%eax   # 獲得ins實(shí)例地址
 8048725:       83 c0 04                add    $0x4,%eax         # 添加一個(gè)指針的偏移量
 8048728:       89 44 24 28             mov    %eax,0x28(%esp)   # 初始化引用
    Derived &d = ins;
 804872c:       8d 44 24 1c             lea    0x1c(%esp),%eax
 8048730:       89 44 24 2c             mov    %eax,0x2c(%esp)

雖然是指向同一個(gè)實(shí)例的引用,根據(jù)引用類型的不同,g++編譯器會(huì)為不同的引用賦予不同的地址。例如b2就獲得一個(gè)指針的偏移量,因此才保證了vptr的正確性。

PS:我們順便也證明了C++中的引用的真實(shí)身份就是指針…

接下來進(jìn)入第二個(gè)問題:

vtbl在何時(shí)被創(chuàng)建?vptr又是在何時(shí)被初始化?

既然我們已經(jīng)知道了g++是如何通過vptr和vtbl來實(shí)現(xiàn)虛函數(shù)魔法的,那么vptr和vtbl又是在什么時(shí)候被創(chuàng)建的呢?

vptr是一個(gè)相對(duì)容易思考的問題,因?yàn)関ptr明確的屬于一個(gè)實(shí)例,所以vptr的賦值理應(yīng)放在類的構(gòu)造函數(shù)中。 g++為每個(gè)有虛函數(shù)的類在構(gòu)造函數(shù)末尾中隱式的添加了為vptr賦值的操作 。

同樣通過生成的匯編代碼驗(yàn)證:

Assembly (x86)

class Derived : public Base1, public Base2
{
 80488dc:       55                      push   %ebp
 80488dd:       89 e5                   mov    %esp,%ebp
 80488df:       83 ec 18                sub    $0x18,%esp
 80488e2:       8b 45 08                mov    0x8(%ebp),%eax
 80488e5:       89 04 24                mov    %eax,(%esp)
 80488e8:       e8 d3 ff ff ff          call   80488c0 <_ZN5Base1C1Ev>
 80488ed:       8b 45 08                mov    0x8(%ebp),%eax
 80488f0:       83 c0 04                add    $0x4,%eax
 80488f3:       89 04 24                mov    %eax,(%esp)
 80488f6:       e8 d3 ff ff ff          call   80488ce <_ZN5Base2C1Ev>
 80488fb:       8b 45 08                mov    0x8(%ebp),%eax
 80488fe:       c7 00 48 8a 04 08       movl   $0x8048a48,(%eax)
 8048904:       8b 45 08                mov    0x8(%ebp),%eax
 8048907:       c7 40 04 5c 8a 04 08    movl   $0x8048a5c,0x4(%eax)
 804890e:       c9                      leave
 804890f:       c3                      ret

可以看到在代碼中,Derived類的構(gòu)造函數(shù)為實(shí)例的兩個(gè)vptr賦初值,可是,這兩個(gè)初值居然是立即數(shù)!立即數(shù)!立即數(shù)! 這說明了vtbl的生成并不是運(yùn)行時(shí)的,而是在編譯期就已經(jīng)確定了存放在這兩個(gè)地址上的 !

這個(gè)地址不出意料的屬于.rodata(只讀數(shù)據(jù)段),使用 objdump -s -j .rodata 提取出對(duì)應(yīng)的內(nèi)存觀察:

80489e0 03000000 01000200 00000000 42617365  ............Base
 80489f0 313a3a66 28290042 61736532 3a3a6728  1::f().Base2::g(
 8048a00 29004465 72697665 643a3a66 28290044  ).Derived::f().D
 8048a10 65726976 65643a3a 67282900 44657269  erived::g().Deri
 8048a20 7665643a 3a682829 00000000 00000000  ved::h()........
 8048a30 00000000 00000000 00000000 00000000  ................
 8048a40 00000000 a08a0408 34880408 68880408  ........4...h...
 8048a50 94880408 fcffffff a08a0408 60880408  ............`...
 8048a60 00000000 c88a0408 08880408 00000000  ................
 8048a70 00000000 d88a0408 dc870408 37446572  ............7Der
 8048a80 69766564 00000000 00000000 00000000  ived............
 8048a90 00000000 00000000 00000000 00000000  ................
 8048aa0 889f0408 7c8a0408 00000000 02000000  ....|...........
 8048ab0 d88a0408 02000000 c88a0408 02040000  ................
 8048ac0 35426173 65320000 a89e0408 c08a0408  5Base2..........
 8048ad0 35426173 65310000 a89e0408 d08a0408  5Base1..........

由于程序運(yùn)行的機(jī)器是小端機(jī),經(jīng)過簡(jiǎn)單的轉(zhuǎn)換就可以得到第一個(gè)vptr所指向的內(nèi)存中的第一條數(shù)據(jù)為0x80488834,如果把這個(gè)數(shù)據(jù)解釋為函數(shù)地址到匯編文件中查找,會(huì)得到:

Assembly (x86)

08048834 <_ZN7Derived1fEv>:
};

class Derived : public Base1, public Base2
{
public:
    virtual void f() {
 8048834:       55                      push   %ebp
 8048835:       89 e5                   mov    %esp,%ebp
 8048837:       83 ec 18                sub    $0x18,%esp

Bingo! g++在編譯期就為每個(gè)類確定了vtbl的內(nèi)容,并且在構(gòu)造函數(shù)中添加相應(yīng)代碼使vptr能夠指向已經(jīng)填好的vtbl的地址 。

這也同時(shí)為我們解答了第三個(gè)問題:

在Linux中運(yùn)行的C++程序虛擬存儲(chǔ)器中,vptr、vtbl存放在虛擬存儲(chǔ)的什么位置?

直接看圖:

C++虛函數(shù)在g++中的實(shí)現(xiàn)方法

虛函數(shù)在虛擬存儲(chǔ)器中的位置

圖中灰色部分應(yīng)該是你已經(jīng)熟悉的,彩色部分內(nèi)容和相關(guān)聯(lián)的箭頭描述了虛函數(shù)調(diào)用的過程(圖中展示的是通過new在堆區(qū)創(chuàng)建實(shí)例的情況,與示例代碼有所區(qū)別,小失誤,不要在意): 當(dāng)調(diào)用虛函數(shù)時(shí),首先通過位于棧區(qū)的實(shí)例的指針找到位于堆區(qū)中的實(shí)例地址,然后通過實(shí)例內(nèi)存開頭處的vptr找到位于.rodata段的vtbl,再根據(jù)偏移量找到想要調(diào)用的函數(shù)地址,最后跳轉(zhuǎn)到代碼段中的函數(shù)地址執(zhí)行目標(biāo)函數(shù) 。

總結(jié)

研究這些問題的起因是因?yàn)楣敬a出現(xiàn)了非常奇葩的行為,經(jīng)過追查定位到虛函數(shù)表出了問題,因此才有機(jī)會(huì)腳踏實(shí)地的對(duì)虛函數(shù)實(shí)現(xiàn)進(jìn)行一番探索。

也許你會(huì)想,即使我不明白這些底層原理,也一樣可以正常的使用虛函數(shù),也一樣可以寫出很好的面相對(duì)象的代碼?。?/p>

這一點(diǎn)兒也沒有錯(cuò),但是,C++作為全宇宙最復(fù)雜的程序設(shè)計(jì)語言,它提供的功能異常強(qiáng)大,無異于武俠小說中鋒利無比的屠龍寶刀。但武功不好的菜鳥如果胡亂舞弄寶刀,卻很容易反被其所傷。只有了解了C++底層的原理和機(jī)制,才能讓我們把C++這把屠龍寶刀使用的更加得心應(yīng)手,變化出更加華麗的招式,成為真正的武林高手。

以上是C++虛函數(shù)在g++中的實(shí)現(xiàn)方法的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對(duì)大家有幫助,更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI