當(dāng)前位置:首頁 > 公眾號(hào)精選 > 程序喵大人
[導(dǎo)讀]我們都知道C++多態(tài)是通過虛函數(shù)表來實(shí)現(xiàn)的,那具體是什么樣的大家清楚嗎?開篇依舊提出來幾個(gè)問題: 普通類對(duì)象是什么布局? 帶虛函數(shù)的類對(duì)象是什么布局? 單繼承下不含有覆蓋函數(shù)的類對(duì)象是什么布局? 單繼承下含有覆蓋函數(shù)的類對(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ì)象的布局情況吧~

普通類對(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

畫出圖如下:

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

帶虛函數(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.ccclang -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ì)象布局圖如下:

我們來探秘下傳說中的虛函數(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ì)介紹。

單繼承下不含有覆蓋函數(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()

畫圖如下:

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

單繼承下含有覆蓋函數(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()

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

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

多繼承下不含有覆蓋函數(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ì)象布局圖如下:

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ù)往下看~

多繼承下含有覆蓋函數(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]

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ì)您也明白了吧~

多重繼承不同的繼承順序?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ì)象布局圖如下:

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

虛繼承的布局

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ì)象布局圖如下:

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ù)。

虛繼承帶未覆蓋函數(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ì)象布局圖如下:

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()。

菱形繼承下類對(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ì)象布局圖如下:

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ù)。

為什么要虛繼承?

如圖:

非虛繼承時(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ì)象被繼承,而無論該基類在派生層次中出現(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的基類

如圖:

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

虛表結(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





c++11新特性,所有知識(shí)點(diǎn)都在這了!

你的c++團(tuán)隊(duì)還在禁用異常處理嗎?

JNI編程如何巧妙獲取JNIEnv

Linux 為什么要?jiǎng)討B(tài)鏈接?與靜態(tài)鏈接的區(qū)別是什么?

內(nèi)存對(duì)齊之格式修訂版

c++11新特性之智能指針

gcc a.c 究竟經(jīng)歷了什么?

談?wù)劤绦蜴溄蛹胺侄文切┦?/span>


「 在看的,麻煩點(diǎn)一下再走~ 」

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競爭優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡稱"軟通動(dòng)力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉