一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

虛函數(shù)表與多態(tài)的認(rèn)知

 行者花雕 2020-06-07

虛函數(shù)表與多態(tài)

虛函數(shù)表與多態(tài),是C++開發(fā)人員終究要面對的問題。
雖然很久沒寫C++了,此處還是將其整理一下進(jìn)行記錄。
編譯器信息:

  • gcc: gcc (Debian 7.3.0-19) 7.3.0;
  • clang: 7.0.1-8 (tags/RELEASE_701/final).

1 類空間

class Empty {
public:
    Empty() = default;
    ~Empty() = default;
    void hello() { std::cout << "hello world" << std::endl; }
};
// sizeof(Empty) = 1

首先需要明確,空類(包含非虛函數(shù)),其大小為1。
為了能將class實(shí)例放到數(shù)組里,空類必須具有大小,否則數(shù)組sizeof將是災(zāi)難。
不過空類作為基類時(shí),為了對齊可能占用4各字節(jié)或以上,因此編譯器有空基類優(yōu)化。

空基類優(yōu)化:令非靜態(tài)數(shù)據(jù)成員、無虛函數(shù)的基類實(shí)際占用0字節(jié)。

現(xiàn)在,我們開始加入一個(gè)虛函數(shù),再次查看類大小。

class Empty {
public:
    Empty() = default;
    ~Empty() = default;
    void hello() { std::cout << "hello world" << std::endl; }
    virtual void virtual_test() {}
};
// sizeof(Empty) = 8

加入虛函數(shù)后,類大小從1字節(jié)增加至為8字節(jié)。
這是因?yàn)?,編譯器在類中隱式插入了虛函數(shù)表指針(void *vptr),指針大小為8字節(jié)。

關(guān)于編譯器在背后做的事情,建議看<<深度探索C++對象模型>>(雖然看了就忘,但是比沒看要好一些)。

2 虛函數(shù)表指針(vptr)與虛函數(shù)表(vtbl)

對于包含虛函數(shù)的類,編譯器會為類創(chuàng)建相應(yīng)的虛函數(shù)表(vtbl)。
虛函數(shù)表中,主要存放類所對應(yīng)的虛函數(shù)地址。
在編譯期間,編譯器會在構(gòu)造函數(shù)中,對vptr進(jìn)行賦值,數(shù)值為vtbl的地址。
偽代碼如下所示:

class Empty {
public:
    Empty() {
        vtpr = (void*)&Empty::vtbl;
    }
}

改進(jìn)一些,我們修改Empty類如下所示:

class Empty {
public:
    Empty() = default;
    virtual ~Empty() {}
    virtual void virtual_func1() {}
    virtual void virtual_func2() {}
public:
    int m1 = 0x01020304, m2 = 0x04030201;
};

int main() {
    Empty empty;
    std::cout << empty.m1 << std::endl;
    return 0;
}

主要改進(jìn)就是添加成員變量m1,m2,以及添加若干函數(shù)(包含虛函數(shù))。
使用gdb查看Empty實(shí)例的內(nèi)存布局,具體如下所示:

Empty實(shí)例內(nèi)存布局

由上圖可知,Empty實(shí)例的內(nèi)存布局為:

  1. vptr(紅線部分,指向Empty的虛表);
  2. m1,m2。

3 多態(tài)調(diào)用

C++的三大特性是封裝,繼承以及多態(tài),其中多態(tài)必須依靠虛函數(shù)實(shí)現(xiàn)。
通俗點(diǎn)說,如果通過調(diào)用虛函數(shù)表指針(vtpr)找到虛函數(shù)表(vtbl)的入口并執(zhí)行虛函數(shù),則程序使用到了多態(tài)。
舉個(gè)例子:

class Base {
public:
    virtual void virtual_func() {}
};

int main() {
    Base *a = new Base();
    a->virtual_func();  // 多態(tài)調(diào)用
    Base b;
    b.virtual_func();   // 非多態(tài)調(diào)用
    Base *c = &b;
    c->virtual_func();  // 多態(tài)調(diào)用
    return 0;
}

為了驗(yàn)證注釋中的觀點(diǎn),我們使用匯編代碼進(jìn)行佐證:

虛函數(shù)調(diào)用匯編代碼

上圖可以看出,三次調(diào)用virtual_func,匯編代碼存在較大不同。
原因是a,c實(shí)例調(diào)用virtual_func相對于b實(shí)例調(diào)用virtual_func,多了需要去虛表(vtbl)中查找virtual_func函數(shù)入口的過程。

4 內(nèi)存布局

下文將分別從單繼承,多繼承以及菱形繼承三點(diǎn)闡述虛表的內(nèi)存布局(使用g++導(dǎo)出內(nèi)存布局)。

4.1 單繼承

class A
{
    int ax;
    virtual void f0() {}
};
class B : public A
{
    int bx;
    virtual void f1() {}
};
class C : public B
{
    int cx;
    void f0() override {}
    virtual void f2() {}
};

內(nèi)存布局如下所示:

Vtable for A
A::vtable for A: 3 entries
0     (int (*)(...))0                   // 類型轉(zhuǎn)換偏移量
8     (int (*)(...))(& typeinfo for A)  // 運(yùn)行時(shí)類型信息(Run-Time Type Identification,RTTI)
16    (int (*)(...))A::f0               // 虛函數(shù)f0地址

Class A
   size=16 align=8
   base size=12 base align=8
A (0x0x7f753a178960) 0
    vptr=((& A::vtable for A) + 16)

Vtable for B
B::vtable for B: 4 entries
0     (int (*)(...))0                   // 類型轉(zhuǎn)換偏移量
8     (int (*)(...))(& typeinfo for B)  // 運(yùn)行時(shí)類型信息(Run-Time Type Identification,RTTI)
16    (int (*)(...))A::f0               // 虛函數(shù)f0地址(未override基類函數(shù),因此繼承自A)
24    (int (*)(...))B::f1               // 虛函數(shù)f1地址

Class B
   size=16 align=8
   base size=16 base align=8
B (0x0x7f753a00e1a0) 0
    vptr=((& B::vtable for B) + 16)
  A (0x0x7f753a178a20) 0
      primary-for B (0x0x7f753a00e1a0)

Vtable for C
C::vtable for C: 5 entries
0     (int (*)(...))0                   // 類型轉(zhuǎn)換偏移量
8     (int (*)(...))(& typeinfo for C)  // 運(yùn)行時(shí)類型信息(Run-Time Type Identification,RTTI)
16    (int (*)(...))C::f0               // 虛函數(shù)f0地址
24    (int (*)(...))B::f1               // 虛函數(shù)f1地址(未override基類函數(shù),因此繼承自B)
32    (int (*)(...))C::f2               // 虛函數(shù)f2地址

Class C
   size=24 align=8
   base size=20 base align=8
C (0x0x7f753a00e208) 0
    vptr=((& C::vtable for C) + 16)
  B (0x0x7f753a00e270) 0
      primary-for C (0x0x7f753a00e208)
    A (0x0x7f753a178ae0) 0
        primary-for B (0x0x7f753a00e270)

此處需要明確,Class A/B/C均有對應(yīng)的虛表。
虛表主要包含三類信息:

  • 類型轉(zhuǎn)換偏移量;
  • 運(yùn)行時(shí)類型信息(Run-Time Type Identification,RTTI);
  • 虛函數(shù)地址(可以包含多項(xiàng)),具體信息詳見注釋部分。

4.2 多繼承

class A {
    int ax;
    virtual void f0() {}
};
class B {
    int bx;
    virtual void f1() {}
};
class C : public A, public B {
    virtual void f0() override {}
    virtual void f1() override {}
};

得到類內(nèi)存布局如下所示:

// 因?yàn)轭怉與類B比較簡單,因此省略內(nèi)存布局(可參考單繼承內(nèi)存布局)

Vtable for C
C::vtable for C: 7 entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for C)
16    (int (*)(...))C::f0
24    (int (*)(...))C::f1
32    (int (*)(...))-16                             // 類型轉(zhuǎn)換偏移量
40    (int (*)(...))(& typeinfo for C)              // 運(yùn)行時(shí)類型信息(Run-Time Type Identification,RTTI)
48    (int (*)(...))C::non-virtual thunk to C::f1()

Class C
   size=32 align=8
   base size=28 base align=8
C (0x0x7f9ce2bde310) 0
    vptr=((& C::vtable for C) + 16)
  A (0x0x7f9ce2d37ae0) 0
      primary-for C (0x0x7f9ce2bde310)
  B (0x0x7f9ce2d37b40) 16
      vptr=((& C::vtable for C) + 48)

代碼中,類C繼承自類A以及類B,內(nèi)存布局發(fā)生了較大的變化(添加了末尾三行)。
g++的內(nèi)存布局比較晦澀,使用clang導(dǎo)出內(nèi)存布局(基本一致),會比較直觀:

*** Dumping AST Record Layout
         0 | struct C
         0 |   struct A (primary base)
         0 |     (A vtable pointer)
         8 |     int ax
        16 |   struct B (base)
        16 |     (B vtable pointer)
        24 |     int bx
           | [sizeof=32, dsize=28, align=8,
           |  nvsize=28, nvalign=8]

由clang的內(nèi)存布局可知,類C的實(shí)例中包含類A與類B的虛指針。
這是因?yàn)锳與B完全獨(dú)立,虛函數(shù)f0與f1之間沒有順序關(guān)系,相對于基類有著相同的起始位置偏移量。
因此在類C中,類A與類B的虛表信息必須保存在兩個(gè)不相交的區(qū)域中,使用兩個(gè)虛指針對其進(jìn)行索引。

                                                C Vtable (7 entities)
                                                +--------------------+
struct C                                        | offset_to_top (0)  |
object                                          +--------------------+
    0 - struct A (primary base)                 |     RTTI for C     |
    0 -   vptr_A -----------------------------> +--------------------+
    8 -   int ax                                |       C::f0()      |
   16 - struct B                                +--------------------+
   16 -   vptr_B ----------------------+        |       C::f1()      |
   24 -   int bx                       |        +--------------------+
   28 - int cx                         |        | offset_to_top (-16)|
sizeof(C): 32    align: 8              |        +--------------------+
                                       |        |     RTTI for C     |
                                       +------> +--------------------+
                                                |    Thunk C::f1()   |
                                                +--------------------+

上圖比較形象的描繪了虛指針,對應(yīng)虛表的內(nèi)容。
首先解釋offset_to_top: 基類轉(zhuǎn)換到派生類時(shí),this指針加上偏移量即可獲得實(shí)際類型的地址。
至于Thunk:

(1) 在B &b = c的場景中,引用的起始地址在C+16處,如果直接調(diào)用f1時(shí),會因?yàn)閠his指針多了16字節(jié)的偏移量導(dǎo)致錯(cuò)誤;
(2) Thunk提示this指針根據(jù)offset_to_top減去16字節(jié)偏移量,繼而調(diào)用f1函數(shù)。

Thunk解釋說明,當(dāng)基類引用持有派生類實(shí)例時(shí),調(diào)用相應(yīng)虛函數(shù),會利用到多態(tài)特性。

4.3 菱形繼承

class A {
public:
    virtual void foo() {}
    virtual void bar() {}
private:
    int ma;
};
class B : virtual public A {
public:
    virtual void foo() override {}
private:
    int mb;
};
class C : virtual public A {
public:
    virtual void bar() override {}
private:
    int mc;
};
class D : public B, public C {
public:
    virtual void foo() override {}
    virtual void bar() override {}
};

基類A中添加了成員變量ma,是因?yàn)轭怉中若不包含成員變量,派生類B/C/D會被優(yōu)化,較難理解。

首先查看類B的內(nèi)存布局:

*** Dumping AST Record Layout
         0 | class B
         0 |   (B vtable pointer)
         8 |   int mb
        16 |   class A (virtual base)
        16 |     (A vtable pointer)
        24 |     int ma
           | [sizeof=32, dsize=28, align=8,
           |  nvsize=12, nvalign=8]

需要注意,此時(shí)類B中包含兩個(gè)虛指針,且類A的虛指針起始位置為B+16。
查看類B的虛表結(jié)構(gòu),如下所示:

Vtable for 'B' (10 entries).
   0 | vbase_offset (16)
   1 | offset_to_top (0)
   2 | B RTTI
       -- (B, 0) vtable address --
   3 | void B::foo()
   4 | vcall_offset (0)
   5 | vcall_offset (-16)
   6 | offset_to_top (-16)
   7 | B RTTI
       -- (A, 16) vtable address --
   8 | void B::foo()
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
   9 | void A::bar()

此時(shí),虛表頭部增加了vbase_offset,這是因?yàn)樵诰幾g時(shí),無法確定基類A在類B內(nèi)存中的偏移量,因此需要在虛表中添加vbase_offset,標(biāo)記運(yùn)行時(shí)基類A在類B內(nèi)存中的位置。
此外,虛表中添加了兩項(xiàng)vcall_offset,這是應(yīng)對使用虛基類A的引用調(diào)用類B實(shí)例的虛函數(shù)時(shí),每一個(gè)虛函數(shù)相對于this指針的偏移量都可能不同,因此需要記錄在vcall_offset中。

  • vcall_offset (0): 對應(yīng)A::bar();
  • vcall_offset (-16): 對應(yīng)B::foo()。

因此,當(dāng)A引用調(diào)用B實(shí)例的A::bar函數(shù)時(shí),因?yàn)閠his指針指向vptr_a,因此不需要進(jìn)行調(diào)整;調(diào)用B::foo()時(shí),因此foo函數(shù)被B重載,因此需要調(diào)整this指針指向vptr_b。

查看類D的內(nèi)存布局:

*** Dumping AST Record Layout
         0 | class D
         0 |   class B (primary base)
         0 |     (B vtable pointer)
         8 |     int mb
        16 |   class C (base)
        16 |     (C vtable pointer)
        24 |     int mc
        32 |   class A (virtual base)
        32 |     (A vtable pointer)
        40 |     int ma
           | [sizeof=48, dsize=44, align=8,
           |  nvsize=28, nvalign=8]

此時(shí),需要注意因?yàn)槭褂锰摾^承,所以類A在類D中只有一份,共擁有三個(gè)虛指針。
虛表內(nèi)容相對較為復(fù)雜,不過基本可以參照類B的虛表進(jìn)行解析,具體如下所示:

Vtable for 'D' (15 entries).
   0 | vbase_offset (32)
   1 | offset_to_top (0)
   2 | D RTTI
       -- (B, 0) vtable address --
       -- (D, 0) vtable address --
   3 | void D::foo()
   4 | void D::bar()
   5 | vbase_offset (16)
   6 | offset_to_top (-16)
   7 | D RTTI
       -- (C, 16) vtable address --
   8 | void D::bar()
       [this adjustment: -16 non-virtual]
   9 | vcall_offset (-32)
  10 | vcall_offset (-32)
  11 | offset_to_top (-32)
  12 | D RTTI
       -- (A, 32) vtable address --
  13 | void D::foo()
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  14 | void D::bar()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

5 擴(kuò)展

C++的虛表,以及運(yùn)行時(shí)的內(nèi)存模型是很復(fù)雜的問題,在編寫的過程中也是不斷的刷新自己的認(rèn)知。
下面提供一些方式,dump出內(nèi)存中對象的內(nèi)存模型,和類型的虛表結(jié)構(gòu)。

使用clang編譯器:clang++ -cc1 -emit-llvm -fdump-record-layouts -fdump-vtable-layouts main.cpp.

使用gcc編譯器:

g++ -fdump-class-hierarchy -c main.cpp
// g++ dump的內(nèi)容比較晦澀,因此需要使用c++ filt導(dǎo)出具有可讀性的文檔
cat [g++導(dǎo)出的文檔] | c++filt -n > [具有一定可讀性的輸出文檔]

本文內(nèi)存布局部分,參考于:https://zhuanlan.zhihu.com/p/41309205一文。

PS:
如果您覺得我的文章對您有幫助,請關(guān)注我的微信公眾號,謝謝!

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    白丝美女被插入视频在线观看| 可以在线看的欧美黄片| 爱在午夜降临前在线观看| 日韩中文字幕视频在线高清版| 国产又大又猛又粗又长又爽| 欧美日韩精品人妻二区三区| 国产精品亚洲精品亚洲| 视频一区二区黄色线观看| 日本精品啪啪一区二区三区 | 日韩精品一区二区三区av在线| 欧美欧美欧美欧美一区| 精品国产成人av一区二区三区| 伊人久久青草地综合婷婷| 亚洲欧美日韩国产综合在线| 欧美精品日韩精品一区| 国产欧美日韩综合精品二区| 久久国产人妻一区二区免费| 四季精品人妻av一区二区三区| 国产又长又粗又爽免费视频| 性感少妇无套内射在线视频| 国产日本欧美特黄在线观看| 欧美三级大黄片免费看| 在线观看日韩欧美综合黄片| 国产盗摄精品一区二区视频| 狠狠做五月深爱婷婷综合| 一区二区欧美另类稀缺| 国产主播精品福利午夜二区| 午夜精品在线观看视频午夜| 久久精品久久精品中文字幕| 久久99精品日韩人妻| 日本欧美一区二区三区高清| 亚洲国产91精品视频| 久久精品国产99精品亚洲| 国产对白老熟女正在播放| 91精品国产品国语在线不卡 | 夫妻性生活真人动作视频| 亚洲综合日韩精品欧美综合区| 国产高清一区二区不卡| 精品视频一区二区不卡| 国产又粗又猛又大爽又黄同志| 日韩一区二区三区免费av|