面試系列之C++的對(duì)象布局【建議收藏】

我們都知道C++多態(tài)是通過虛函數(shù)表來實(shí)現(xiàn)的,那具體是什么樣的大家清楚嗎?開篇依舊提出來幾個(gè)問題:

  • 普通類對(duì)象是什么布局?
  • 帶虛函數(shù)的類對(duì)象是什么布局?
  • 單繼承下不含有覆蓋函數(shù)的類對(duì)象是什么布局?
  • 單繼承下含有覆蓋函數(shù)的類對(duì)象是什么布局?
  • 多繼承下不含有覆蓋函數(shù)的類對(duì)象是什么布局?
  • 多繼承下含有覆蓋函數(shù)的類對(duì)象的是什么布局?
  • 多繼承中不同的繼承順序產(chǎn)生的類對(duì)象布局相同嗎?
  • 虛繼承的類對(duì)象是什么布局?
  • 菱形繼承下類對(duì)象是什么布局?
  • 為什么要引入虛繼承?
  • 為什么虛函數(shù)表中有兩個(gè)析構(gòu)函數(shù)?
  • 為什么構(gòu)造函數(shù)不能是虛函數(shù)?
  • 為什么基類析構(gòu)函數(shù)需要是虛函數(shù)?

要回答上述問題我們首先需要了解什么是多態(tài)。

什么是多態(tài)

多態(tài)可以分為編譯時(shí)多態(tài)和運(yùn)行時(shí)多態(tài)。

  • 編譯時(shí)多態(tài):基于模板和函數(shù)重載方式,在編譯時(shí)就已經(jīng)確定對(duì)象的行為,也稱為靜態(tài)綁定。

  • 運(yùn)行時(shí)多態(tài):面向?qū)ο蟮囊淮筇厣ㄟ^繼承方式使得程序在運(yùn)行時(shí)才會(huì)確定相應(yīng)調(diào)用的方法,也稱為動(dòng)態(tài)綁定,它的實(shí)現(xiàn)主要是依賴于傳說中的虛函數(shù)表。

如何查看對(duì)象的布局

在gcc中可以使用如下命令查看對(duì)象布局:

g++ -fdump-class-hierarchy model.cc后查看生成的文件

在clang中可以使用如下命令:

clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc
// 查看對(duì)象布局
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc
// 查看虛函數(shù)表布局

上面兩種方式其實(shí)足夠了,也可以使用gdb來查看內(nèi)存布局,這里可以看文末相關(guān)參考資料。本文都是使用clang來查看的對(duì)象布局。

接下來讓我們一起來探秘下各種繼承條件下類對(duì)象的布局情況吧~

1. 普通類對(duì)象的布局

如下代碼:

struct Base {
    Base() = default;
    ~Base() = default;
    
    void Func() {}

    int a;
    int b;
};

int main() {
    Base a;
    return 0; 
}

// 使用clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc查看

輸出如下:

*** Dumping AST Record Layout
         0 | struct Base
         0 |   int a
         4 |   int b
           | [sizeof=8, dsize=8, align=4,
           |  nvsize=8, nvalign=4]

*** Dumping IRgen Record Layout

畫出圖如下:

1.png

從結(jié)果中可以看見,這個(gè)普通結(jié)構(gòu)體Base的大小為8字節(jié),a占4個(gè)字節(jié),b占4個(gè)字節(jié)。

2. 帶虛函數(shù)的類對(duì)象的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("FuncB\n");
    }

    int a;
    int b;
};

int main() {
    Base a;
    return 0; 
}

// 這里可以查看對(duì)象的布局和相應(yīng)虛函數(shù)表的布局
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc

對(duì)象布局如下:

*** Dumping AST Record Layout
         0 | struct Base
         0 |   (Base vtable pointer)
         8 |   int a
        12 |   int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

這個(gè)含有虛函數(shù)的結(jié)構(gòu)體大小為16,在對(duì)象的頭部,前8個(gè)字節(jié)是虛函數(shù)表的指針,指向虛函數(shù)的相應(yīng)函數(shù)指針地址,a占4個(gè)字節(jié),b占4個(gè)字節(jié),總大小為16。

虛函數(shù)表布局:

Vtable for 'Base' (5 entries).
   0 | offset_to_top (0)
   1 | Base RTTI
       -- (Base, 0) vtable address --
   2 | Base::~Base() [complete]
   3 | Base::~Base() [deleting]
   4 | void Base::FuncB()

畫出對(duì)象布局圖如下:

2.png

我們來探秘下傳說中的虛函數(shù)表:

offset_to_top(0):表示當(dāng)前這個(gè)虛函數(shù)表地址距離對(duì)象頂部地址的偏移量,因?yàn)閷?duì)象的頭部就是虛函數(shù)表的指針,所以偏移量為0。

RTTI指針:指向存儲(chǔ)運(yùn)行時(shí)類型信息(type_info)的地址,用于運(yùn)行時(shí)類型識(shí)別,用于typeid和dynamic_cast。

RTTI下面就是虛函數(shù)表指針真正指向的地址啦,存儲(chǔ)了類里面所有的虛函數(shù),至于這里為什么會(huì)有兩個(gè)析構(gòu)函數(shù),大家可以先關(guān)注對(duì)象的布局,最下面會(huì)介紹。

3. 單繼承下不含有覆蓋函數(shù)的類對(duì)象的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("Base FuncB\n");
    }

    int a;
    int b;
};

struct Derive : public Base{
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

子類對(duì)象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct Base (primary base)
         0 |     (Base vtable pointer)
         8 |     int a
        12 |     int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

和上面相同,這個(gè)含有虛函數(shù)的結(jié)構(gòu)體大小為16,在對(duì)象的頭部,前8個(gè)字節(jié)是虛函數(shù)表的指針,指向虛函數(shù)的相應(yīng)函數(shù)指針地址,a占4個(gè)字節(jié),b占4個(gè)字節(jié),總大小為16。

子類虛函數(shù)表布局:

Vtable for 'Derive' (5 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (Base, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Base::FuncB()

畫圖如下:

3.png

這個(gè)和上面也是相同的,注意下虛函數(shù)表這里的FuncB函數(shù),還是Base類中的FuncB,因?yàn)樵谧宇愔袥]有重寫這個(gè)函數(shù),那么如果子類重寫這個(gè)函數(shù)后對(duì)象布局是什么樣的,請(qǐng)繼續(xù)往下看哈。

4. 單繼承下含有覆蓋函數(shù)的類對(duì)象的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("Base FuncB\n");
    }

    int a;
    int b;
};

struct Derive : public Base{
    void FuncB() override {
        printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

子類對(duì)象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct Base (primary base)
         0 |     (Base vtable pointer)
         8 |     int a
        12 |     int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

依舊和上面相同,這個(gè)含有虛函數(shù)的結(jié)構(gòu)體大小為16,在對(duì)象的頭部,前8個(gè)字節(jié)是虛函數(shù)表的指針,指向虛函數(shù)的相應(yīng)函數(shù)指針地址,a占4個(gè)字節(jié),b占4個(gè)字節(jié),總大小為16。

子類虛函數(shù)表布局:

Vtable for 'Derive' (5 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (Base, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncB()
4.png

注意這里虛函數(shù)表中的FuncB函數(shù)已經(jīng)是Derive中的FuncB啦,因?yàn)樵谧宇愔兄貙懥烁割惖倪@個(gè)函數(shù)。

再注意這里的RTTI中有了兩項(xiàng),表示Base和Derive的虛表地址是相同的,Base類里的虛函數(shù)和Derive類里的虛函數(shù)都在這個(gè)鏈條下,這里可以繼續(xù)關(guān)注下面多繼承的情況,看看有何不同。

5. 多繼承下不含有覆蓋函數(shù)的類對(duì)象的布局

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseA, public BaseB{
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

類對(duì)象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseA (primary base)
         0 |     (BaseA vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseB (base)
        16 |     (BaseB vtable pointer)
        24 |     int a
       28 |     int b
          | [sizeof=32, dsize=32, align=8,
          |  nvsize=32, nvalign=8]

Derive大小為32,注意這里有了兩個(gè)虛表指針,因?yàn)镈erive是多繼承,一般情況下繼承了幾個(gè)帶有虛函數(shù)的類,對(duì)象布局中就有幾個(gè)虛表指針,并且子類也會(huì)繼承基類的數(shù)據(jù),一般來說,不考慮內(nèi)存對(duì)齊的話,子類(繼承父類)的大小=子類(不繼承父類)的大小+所有父類的大小。

虛函數(shù)表布局:

Vtable for 'Derive' (10 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseA, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void BaseA::FuncB()
   5 | offset_to_top (-16)
   6 | Derive RTTI
       -- (BaseB, 16) vtable address --
   7 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   8 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
   9 | void BaseB::FuncC()

可畫出對(duì)象布局圖如下:

5.png

offset_to_top(0):表示當(dāng)前這個(gè)虛函數(shù)表(BaseA,Derive)地址距離對(duì)象頂部地址的偏移量,因?yàn)閷?duì)象的頭部就是虛函數(shù)表的指針,所以偏移量為0。

再注意這里的RTTI中有了兩項(xiàng),表示BaseA和Derive的虛表地址是相同的,BaseA類里的虛函數(shù)和Derive類里的虛函數(shù)都在這個(gè)鏈條下,截至到offset_to_top(-16)之前都是BaseA和Derive的虛函數(shù)表。

offset_to_top(-16):表示當(dāng)前這個(gè)虛函數(shù)表(BaseB)地址距離對(duì)象頂部地址的偏移量,因?yàn)閷?duì)象的頭部就是虛函數(shù)表的指針,所以偏移量為-16,這里用于this指針偏移,下一小節(jié)會(huì)介紹。

注意下后面的這個(gè)RTTI:只有一項(xiàng),表示BaseB的虛函數(shù)表,后面也有兩個(gè)虛析構(gòu)函數(shù),為什么有四個(gè)Derive類的析構(gòu)函數(shù)呢,又是怎么調(diào)用呢,請(qǐng)繼續(xù)往下看~

6. 多繼承下含有覆蓋函數(shù)的類對(duì)象的布局

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseA, public BaseB{
    void FuncB() override {
        printf("Derive FuncB \n");
    }

    void FuncC() override {
        printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

對(duì)象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseA (primary base)
         0 |     (BaseA vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseB (base)
        16 |     (BaseB vtable pointer)
        24 |     int a
        28 |     int b
           | [sizeof=32, dsize=32, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

類大小仍然是32,和上面一樣。

虛函數(shù)表布局:

Vtable for 'Derive' (11 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseA, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncB()
   5 | void Derive::FuncC()
   6 | offset_to_top (-16)
   7 | Derive RTTI
       -- (BaseB, 16) vtable address --
   8 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   9 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  10 | void Derive::FuncC()
       [this adjustment: -16 non-virtual]
6.png

offset_to_top(0):表示當(dāng)前這個(gè)虛函數(shù)表(BaseA,Derive)地址距離對(duì)象頂部地址的偏移量,因?yàn)閷?duì)象的頭部就是虛函數(shù)表的指針,所以偏移量為0。

再注意這里的RTTI中有了兩項(xiàng),表示BaseA和Derive的虛表地址是相同的,BaseA類里的虛函數(shù)和Derive類里的虛函數(shù)都在這個(gè)鏈條下,截至到offset_to_top(-16)之前都是BaseA和Derive的虛函數(shù)表。

offset_to_top(-16):表示當(dāng)前這個(gè)虛函數(shù)表(BaseB)地址距離對(duì)象頂部地址的偏移量,因?yàn)閷?duì)象的頭部就是虛函數(shù)表的指針,所以偏移量為-16。當(dāng)基類BaseB的引用或指針base實(shí)際接受的是Derive類型的對(duì)象,執(zhí)行base->FuncC()時(shí)候,由于FuncC()已經(jīng)被重寫,而此時(shí)的this指針指向的是BaseB類型的對(duì)象,需要對(duì)this指針進(jìn)行調(diào)整,就是offset_to_top(-16),所以this指針向上調(diào)整了16字節(jié),之后調(diào)用FuncC(),就調(diào)用到了被重寫后Derive虛函數(shù)表中的FuncC()函數(shù)。這些帶adjustment標(biāo)記的函數(shù)都是需要進(jìn)行指針調(diào)整的。至于上面所說的這里虛函數(shù)是怎么調(diào)用的,估計(jì)您也明白了吧~

7. 多重繼承不同的繼承順序?qū)е碌念悓?duì)象的布局相同嗎?

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseB, public BaseA{
    void FuncB() override {
        printf("Derive FuncB \n");
    }

    void FuncC() override {
        printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

對(duì)象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseB (primary base)
         0 |     (BaseB vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseA (base)
        16 |     (BaseA vtable pointer)
        24 |     int a
        28 |     int b
           | [sizeof=32, dsize=32, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

這里可見,對(duì)象布局和上面的不相同啦,BaseB的虛函數(shù)表指針和數(shù)據(jù)在上面,BaseA的虛函數(shù)表指針和數(shù)據(jù)在下面,以A,B的順序繼承,對(duì)象的布局就是A在上B在下,以B,A的順序繼承,對(duì)象的布局就是B在上A在下。

虛函數(shù)表布局:

Vtable for 'Derive' (11 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseB, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncC()
   5 | void Derive::FuncB()
   6 | offset_to_top (-16)
   7 | Derive RTTI
       -- (BaseA, 16) vtable address --
   8 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   9 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  10 | void Derive::FuncB()
       [this adjustment: -16 non-virtual]

對(duì)象布局圖如下:

7.png

虛函數(shù)表的布局也有所不同,BaseB和Derive共用一個(gè)虛表地址,在整個(gè)虛表布局的上方,而布局的下半部分是BaseA的虛表,可見繼承順序不同,子類的虛表布局也有所不同。

8. 虛繼承的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct Derive : virtual public Base{
    void FuncB() override {
        printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

對(duì)象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   (Derive vtable pointer)
         8 |   struct Base (virtual base)
         8 |     (Base vtable pointer)
        16 |     int a
        20 |     int b
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout

虛繼承下,這里的對(duì)象布局和普通單繼承有所不同,普通單繼承下子類和基類共用一個(gè)虛表地址,而在虛繼承下,子類和虛基類分別有一個(gè)虛表地址的指針,兩個(gè)指針大小總和為16,再加上a和b的大小8,為24。

虛函數(shù)表:

Vtable for 'Derive' (13 entries).
   0 | vbase_offset (8)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (Derive, 0) vtable address --
   3 | void Derive::FuncB()
   4 | Derive::~Derive() [complete]
   5 | Derive::~Derive() [deleting]
   6 | vcall_offset (-8)
   7 | vcall_offset (-8)
   8 | offset_to_top (-8)
   9 | Derive RTTI
       -- (Base, 8) vtable address --
  10 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  11 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  12 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

對(duì)象布局圖如下:

8.png

vbase_offset(8):對(duì)象在對(duì)象布局中與指向虛基類虛函數(shù)表的指針地址的偏移量

vcall_offset(-8):當(dāng)虛基類Base的引用或指針base實(shí)際接受的是Derive類型的對(duì)象,執(zhí)行base->FuncB()時(shí)候,由于FuncB()已經(jīng)被重寫,而此時(shí)的this指針指向的是Base類型的對(duì)象,需要對(duì)this指針進(jìn)行調(diào)整,就是vcall_offset(-8),所以this指針向上調(diào)整了8字節(jié),之后調(diào)用FuncB(),就調(diào)用到了被重寫后的FuncB()函數(shù)。

9. 虛繼承帶未覆蓋函數(shù)的對(duì)象布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("Base FuncB\n");
    }

    virtual void FuncC() {
        printf("Base FuncC\n");
    }

    int a;
    int b;
};

struct Derive : virtual public Base{
    void FuncB() override {
        printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

對(duì)象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   (Derive vtable pointer)
         8 |   struct Base (virtual base)
         8 |     (Base vtable pointer)
        16 |     int a
        20 |     int b
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout

和上面虛繼承情況下相同,普通單繼承下子類和基類共用一個(gè)虛表地址,而在虛繼承下,子類和虛基類分別有一個(gè)虛表地址的指針,兩個(gè)指針大小總和為16,再加上a和b的大小8,為24。

虛函數(shù)表布局:

Vtable for 'Derive' (15 entries).
   0 | vbase_offset (8)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (Derive, 0) vtable address --
   3 | void Derive::FuncB()
   4 | Derive::~Derive() [complete]
   5 | Derive::~Derive() [deleting]
   6 | vcall_offset (0)
   7 | vcall_offset (-8)
   8 | vcall_offset (-8)
   9 | offset_to_top (-8)
  10 | Derive RTTI
       -- (Base, 8) vtable address --
  11 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  12 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  13 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]
  14 | void Base::FuncC()

對(duì)象布局圖如下:

9.png

vbase_offset(8):對(duì)象在對(duì)象布局中與指向虛基類虛函數(shù)表的指針地址的偏移量

vcall_offset(-8):當(dāng)虛基類Base的引用或指針base實(shí)際接受的是Derive類型的對(duì)象,執(zhí)行base->FuncB()時(shí)候,由于FuncB()已經(jīng)被重寫,而此時(shí)的this指針指向的是Base類型的對(duì)象,需要對(duì)this指針進(jìn)行調(diào)整,就是vcall_offset(-8),所以this指針向上調(diào)整了8字節(jié),之后調(diào)用FuncB(),就調(diào)用到了被重寫后的FuncB()函數(shù)。

vcall_offset(0):當(dāng)Base的引用或指針base實(shí)際接受的是Derive類型的對(duì)象,執(zhí)行base->FuncC()時(shí)候,由于FuncC()沒有被重寫,所以不需要對(duì)this指針進(jìn)行調(diào)整,就是vcall_offset(0),之后調(diào)用FuncC()。

10. 菱形繼承下類對(duì)象的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseA : virtual public Base {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB : virtual public Base {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseB, public BaseA{
    void FuncB() override {
        printf("Derive FuncB \n");
    }

    void FuncC() override {
        printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

類對(duì)象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseB (primary base)
         0 |     (BaseB vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseA (base)
        16 |     (BaseA vtable pointer)
        24 |     int a
        28 |     int b
        32 |   struct Base (virtual base)
        32 |     (Base vtable pointer)
        40 |     int a
        44 |     int b
           | [sizeof=48, dsize=48, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

大小為48,這里不用做過多介紹啦,相信您已經(jīng)知道了吧。

虛函數(shù)表:

Vtable for 'Derive' (20 entries).
   0 | vbase_offset (32)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (BaseB, 0) vtable address --
       -- (Derive, 0) vtable address --
   3 | Derive::~Derive() [complete]
   4 | Derive::~Derive() [deleting]
   5 | void Derive::FuncC()
   6 | void Derive::FuncB()
   7 | vbase_offset (16)
   8 | offset_to_top (-16)
   9 | Derive RTTI
       -- (BaseA, 16) vtable address --
  10 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
  11 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  12 | void Derive::FuncB()
       [this adjustment: -16 non-virtual]
  13 | vcall_offset (-32)
  14 | vcall_offset (-32)
  15 | offset_to_top (-32)
  16 | Derive RTTI
       -- (Base, 32) vtable address --
  17 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  18 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  19 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

對(duì)象布局圖如下:

10.png

vbase_offset (32)

vbase_offset (16):對(duì)象在對(duì)象布局中與指向虛基類虛函數(shù)表的指針地址的偏移量

offset_to_top (0)

offset_to_top (-16)

offset_to_top (-32):指向虛函數(shù)表的地址與對(duì)象頂部地址的偏移量。

vcall_offset(-32):當(dāng)虛基類Base的引用或指針base實(shí)際接受的是Derive類型的對(duì)象,執(zhí)行base->FuncB()時(shí)候,由于FuncB()已經(jīng)被重寫,而此時(shí)的this指針指向的是Base類型的對(duì)象,需要對(duì)this指針進(jìn)行調(diào)整,就是vcall_offset(-32),所以this指針向上調(diào)整了32字節(jié),之后調(diào)用FuncB(),就調(diào)用到了被重寫后的FuncB()函數(shù)。

為什么要虛繼承

如圖:

11.png

非虛繼承時(shí),顯然D會(huì)繼承兩次A,內(nèi)部就會(huì)存儲(chǔ)兩份A的數(shù)據(jù)浪費(fèi)空間,而且還有二義性,D調(diào)用A的方法時(shí),由于有兩個(gè)A,究竟時(shí)調(diào)用哪個(gè)A的方法呢,編譯器也不知道,就會(huì)報(bào)錯(cuò),所以有了虛繼承,解決了空間浪費(fèi)以及二義性問題。在虛擬繼承下,只有一個(gè)共享的基類子對(duì)象被繼承,而無(wú)論該基類在派生層次中出現(xiàn)多少次。共享的基類子對(duì)象被稱為虛基類。在虛繼承下,基類子對(duì)象的復(fù)制及由此而引起的二義性都被消除了。

為什么虛函數(shù)表中有兩個(gè)析構(gòu)函數(shù)

前面的代碼輸出中我們可以看到虛函數(shù)表中有兩個(gè)析構(gòu)函數(shù),一個(gè)標(biāo)志為deleting,一個(gè)標(biāo)志為complete,因?yàn)閷?duì)象有兩種構(gòu)造方式,棧構(gòu)造和堆構(gòu)造,所以對(duì)應(yīng)的實(shí)現(xiàn)上,對(duì)象也有兩種析構(gòu)方式,其中堆上對(duì)象的析構(gòu)和棧上對(duì)象的析構(gòu)不同之處在于,棧內(nèi)存的析構(gòu)不需要執(zhí)行 delete 函數(shù),會(huì)自動(dòng)被回收。

為什么構(gòu)造函數(shù)不能是虛函數(shù)。

構(gòu)造函數(shù)就是為了在編譯階段確定對(duì)象的類型以及為對(duì)象分配空間,如果類中有虛函數(shù),那就會(huì)在構(gòu)造函數(shù)中初始化虛函數(shù)表,虛函數(shù)的執(zhí)行卻需要依賴虛函數(shù)表。如果構(gòu)造函數(shù)是虛函數(shù),那它就需要依賴虛函數(shù)表才可執(zhí)行,而只有在構(gòu)造函數(shù)中才會(huì)初始化虛函數(shù)表,雞生蛋蛋生雞的問題,很矛盾,所以構(gòu)造函數(shù)不能是虛函數(shù)。

為什么基類析構(gòu)函數(shù)要是虛函數(shù)。

一般基類的析構(gòu)函數(shù)都要設(shè)置成虛函數(shù),因?yàn)槿绻辉O(shè)置成虛函數(shù),在析構(gòu)的過程中只會(huì)調(diào)用到基類的析構(gòu)函數(shù)而不會(huì)調(diào)用到子類的析構(gòu)函數(shù),可能會(huì)產(chǎn)生內(nèi)存泄漏。

小總結(jié)

offset_to_top:對(duì)象在對(duì)象布局中與對(duì)象頂部地址的偏移量。

RTTI指針:指向存儲(chǔ)運(yùn)行時(shí)類型信息(type_info)的地址,用于運(yùn)行時(shí)類型識(shí)別,用于typeid和dynamic_cast。

vbase_offset:對(duì)象在對(duì)象布局中與指向虛基類虛函數(shù)表的指針地址的偏移量。

vcall_offset:父類引用或指針指向子類對(duì)象,調(diào)用被子類重寫的方法時(shí),用于對(duì)虛函數(shù)執(zhí)行指針地址調(diào)整,方便成功調(diào)用被重寫的方法。

thunk: 表示上面虛函數(shù)表中帶有adjustment字段的函數(shù)調(diào)用需要先進(jìn)行this指針調(diào)整,才可以調(diào)用到被子類重寫的函數(shù)。

最后通過兩張圖總結(jié)一下對(duì)象在Linux中的布局:

A *a = new Derive(); // A為Derive的基類

如圖:

12.png

a作為對(duì)象指針存儲(chǔ)在棧中,指向在堆中的類A的實(shí)例內(nèi)存,其中實(shí)例內(nèi)存布局中有虛函數(shù)表指針,指針指向的虛函數(shù)表存放在數(shù)據(jù)段中,虛函數(shù)表中的各個(gè)函數(shù)指針指向的函數(shù)在代碼段中。

13.png

虛表結(jié)構(gòu)大體如上圖,正常的虛表結(jié)構(gòu)中都含有后三項(xiàng),當(dāng)有虛繼承情況下會(huì)有前兩個(gè)表項(xiàng)。

參考資料:

https://www.cnblogs.com/qg-whz/p/4909359.html

https://blog.csdn.net/fuzhongmin05/article/details/59112081

https://zhuanlan.zhihu.com/p/67177829

https://mp.weixin.qq.com/s/sqpwQpPYBFkPWCmccruvNw

https://jacktang816.github.io/post/virtualfunction/

https://blog.mengy.org/cpp-virtual-table-2/

https://blog.mengy.org/cpp-virtual-table-1/

https://blog.mengy.org/extend-gdb-with-python/

https://www.zhihu.com/question/389546003/answer/1194780618

https://www.zhihu.com/question/29251261/answer/1297439131

https://zhuanlan.zhihu.com/p/41309205

https://wizardforcel.gitbooks.io/100-gdb-tips/examine-memory.html

https://www.cnblogs.com/xhb19960928/p/11720314.html

https://www.lagou.com/lgeduarticle/113008.html

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

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