面試系列之C++的對象佈局【建議收藏】

咱們都知道C++多態是經過虛函數表來實現的,那具體是什麼樣的你們清楚嗎?開篇依舊提出來幾個問題:javascript

  • 普通類對象是什麼佈局?html

  • 帶虛函數的類對象是什麼佈局?java

  • 單繼承下不含有覆蓋函數的類對象是什麼佈局?python

  • 單繼承下含有覆蓋函數的類對象是什麼佈局?c++

  • 多繼承下不含有覆蓋函數的類對象是什麼佈局?git

  • 多繼承下含有覆蓋函數的類對象的是什麼佈局?github

  • 多繼承中不一樣的繼承順序產生的類對象佈局相同嗎?web

  • 虛繼承的類對象是什麼佈局?編程

  • 菱形繼承下類對象是什麼佈局?swift

  • 爲何要引入虛繼承?

  • 爲何虛函數表中有兩個析構函數?

  • 爲何構造函數不能是虛函數?

  • 爲何基類析構函數須要是虛函數?

要回答上述問題咱們首先須要瞭解什麼是多態。

什麼是多態?

多態能夠分爲編譯時多態和運行時多態。

  • 編譯時多態:基於模板和函數重載方式,在編譯時就已經肯定對象的行爲,也稱爲靜態綁定。

  • 運行時多態:面向對象的一大特點,經過繼承方式使得程序在運行時纔會肯定相應調用的方法,也稱爲動態綁定,它的實現主要是依賴於傳說中的虛函數表。

如何查看對象的佈局?

在gcc中可使用以下命令查看對象佈局:

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

在clang中可使用以下命令:

clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc// 查看對象佈局clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc// 查看虛函數表佈局

上面兩種方式其實足夠了,也可使用gdb來查看內存佈局,這裏能夠看文末相關參考資料。本文都是使用clang來查看的對象佈局。

接下來讓咱們一塊兒來探祕下各類繼承條件下類對象的佈局狀況吧~

普通類對象的佈局

下代碼:

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

畫出圖以下:

從結果中能夠看見,這個普通結構體Base的大小爲8字節,a佔4個字節,b佔4個字節。

帶虛函數的類對象佈局
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;}
// 這裏能夠查看對象的佈局和相應虛函數表的佈局clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.ccclang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc

對象佈局以下:

*** 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

這個含有虛函數的結構體大小爲16,在對象的頭部,前8個字節是虛函數表的指針,指向虛函數的相應函數指針地址,a佔4個字節,b佔4個字節,總大小爲16。

虛函數表佈局:

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

畫出對象佈局圖以下:

咱們來探祕下傳說中的虛函數表:

offset_to_top(0)表示當前這個虛函數表地址距離對象頂部地址的偏移量,由於對象的頭部就是虛函數表的指針,因此偏移量爲0。

RTTI指針指向存儲運行時類型信息(type_info)的地址,用於運行時類型識別,用於typeid和dynamic_cast。

RTTI下面就是虛函數表指針真正指向的地址啦,存儲了類裏面全部的虛函數,至於這裏爲何會有兩個析構函數,你們能夠先關注對象的佈局,最下面會介紹。

單繼承下不含有覆蓋函數的類對象的佈局

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;}

子類對象佈局:

*** 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

和上面相同,這個含有虛函數的結構體大小爲16,在對象的頭部,前8個字節是虛函數表的指針,指向虛函數的相應函數指針地址,a佔4個字節,b佔4個字節,總大小爲16。

子類虛函數表佈局:

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

畫圖以下:

這個和上面也是相同的,注意下虛函數表這裏的FuncB函數,仍是Base類中的FuncB,由於在子類中沒有重寫這個函數,那麼若是子類重寫這個函數後對象佈局是什麼樣的,請繼續往下看哈。

單繼承下含有覆蓋函數的類對象的佈局

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;}

子類對象佈局:

*** 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

依舊和上面相同,這個含有虛函數的結構體大小爲16,在對象的頭部,前8個字節是虛函數表的指針,指向虛函數的相應函數指針地址,a佔4個字節,b佔4個字節,總大小爲16。

子類虛函數表佈局:

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

注意這裏虛函數表中的FuncB函數已是Derive中的FuncB啦,由於在子類中重寫了父類的這個函數。

再注意這裏的RTTI中有了兩項,表示Base和Derive的虛表地址是相同的,Base類裏的虛函數和Derive類裏的虛函數都在這個鏈條下,這裏能夠繼續關注下面多繼承的狀況,看看有何不一樣。

多繼承下不含有覆蓋函數的類對象的佈局

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;}

類對象佈局:

*** 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,注意這裏有了兩個虛表指針,由於Derive是多繼承,通常狀況下繼承了幾個帶有虛函數的類,對象佈局中就有幾個虛表指針,而且子類也會繼承基類的數據,通常來講,不考慮內存對齊的話,子類(繼承父類)的大小=子類(不繼承父類)的大小+全部父類的大小

虛函數表佈局:

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

可畫出對象佈局圖以下:

offset_to_top(0)表示當前這個虛函數表(BaseA,Derive)地址距離對象頂部地址的偏移量,由於對象的頭部就是虛函數表的指針,因此偏移量爲0。

再注意這裏的RTTI中有了兩項,表示BaseA和Derive的虛表地址是相同的,BaseA類裏的虛函數和Derive類裏的虛函數都在這個鏈條下,截至到offset_to_top(-16)以前都是BaseA和Derive的虛函數表。

offset_to_top(-16)表示當前這個虛函數表(BaseB)地址距離對象頂部地址的偏移量,由於對象的頭部就是虛函數表的指針,因此偏移量爲-16,這裏用於this指針偏移,下一小節會介紹。

注意下後面的這個RTTI:只有一項,表示BaseB的虛函數表,後面也有兩個虛析構函數,爲何有四個Derive類的析構函數呢,又是怎麼調用呢,請繼續往下看~

多繼承下含有覆蓋函數的類對象的佈局

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;}

對象佈局:

*** 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,和上面同樣。

虛函數表佈局:

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)表示當前這個虛函數表(BaseA,Derive)地址距離對象頂部地址的偏移量,由於對象的頭部就是虛函數表的指針,因此偏移量爲0。

再注意這裏的RTTI中有了兩項,表示BaseA和Derive的虛表地址是相同的,BaseA類裏的虛函數和Derive類裏的虛函數都在這個鏈條下,截至到offset_to_top(-16)以前都是BaseA和Derive的虛函數表。

offset_to_top(-16)表示當前這個虛函數表(BaseB)地址距離對象頂部地址的偏移量,由於對象的頭部就是虛函數表的指針,因此偏移量爲-16。當基類BaseB的引用或指針base實際接受的是Derive類型的對象,執行base->FuncC()時候,因爲FuncC()已經被重寫,而此時的this指針指向的是BaseB類型的對象,須要對this指針進行調整,就是offset_to_top(-16),因此this指針向上調整了16字節,以後調用FuncC(),就調用到了被重寫後Derive虛函數表中的FuncC()函數。這些帶adjustment標記的函數都是須要進行指針調整的。至於上面所說的這裏虛函數是怎麼調用的,估計您也明白了吧~

多重繼承不一樣的繼承順序致使的類對象的佈局相同嗎?

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;}

對象佈局:

*** 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

這裏可見,對象佈局和上面的不相同啦,BaseB的虛函數表指針和數據在上面,BaseA的虛函數表指針和數據在下面,以A,B的順序繼承,對象的佈局就是A在上B在下,以B,A的順序繼承,對象的佈局就是B在上A在下。

虛函數表佈局:

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]

對象佈局圖以下:

虛函數表的佈局也有所不一樣,BaseB和Derive共用一個虛表地址,在整個虛表佈局的上方,而佈局的下半部分是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;}

對象佈局:

*** 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

虛繼承下,這裏的對象佈局和普通單繼承有所不一樣,普通單繼承下子類和基類共用一個虛表地址,而在虛繼承下,子類和虛基類分別有一個虛表地址的指針,兩個指針大小總和爲16,再加上a和b的大小8,爲24。

虛函數表:

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]

對象佈局圖以下:

vbase_offset(8)對象在對象佈局中與指向虛基類虛函數表的指針地址的偏移量

vcall_offset(-8)虛基類Base的引用或指針base實際接受的是Derive類型的對象,執行base->FuncB()時候,因爲FuncB()已經被重寫,而此時的this指針指向的是Base類型的對象,須要對this指針進行調整,就是vcall_offset(-8),因此this指針向上調整了8字節,以後調用FuncB(),就調用到了被重寫後的FuncB()函數。

虛繼承帶未覆蓋函數的對象佈局

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;}

對象佈局:

*** 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

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

虛函數表佈局:

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

對象佈局圖以下:

vbase_offset(8)對象在對象佈局中與指向虛基類虛函數表的指針地址的偏移量

vcall_offset(-8)虛基類Base的引用或指針base實際接受的是Derive類型的對象,執行base->FuncB()時候,因爲FuncB()已經被重寫,而此時的this指針指向的是Base類型的對象,須要對this指針進行調整,就是vcall_offset(-8),因此this指針向上調整了8字節,以後調用FuncB(),就調用到了被重寫後的FuncB()函數。

vcall_offset(0)當Base的引用或指針base實際接受的是Derive類型的對象,執行base->FuncC()時候,因爲FuncC()沒有被重寫,因此不須要對this指針進行調整,就是vcall_offset(0),以後調用FuncC()。

菱形繼承下類對象的佈局

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;}

類對象佈局:

*** 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,這裏不用作過多介紹啦,相信您已經知道了吧。

虛函數表:

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]

對象佈局圖以下:

vbase_offset (32)

vbase_offset (16)對象在對象佈局中與指向虛基類虛函數表的指針地址的偏移量

offset_to_top (0)

offset_to_top (-16)

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

vcall_offset(-32)虛基類Base的引用或指針base實際接受的是Derive類型的對象,執行base->FuncB()時候,因爲FuncB()已經被重寫,而此時的this指針指向的是Base類型的對象,須要對this指針進行調整,就是vcall_offset(-32),因此this指針向上調整了32字節,以後調用FuncB(),就調用到了被重寫後的FuncB()函數。

爲何要虛繼承?

如圖:

非虛繼承時,顯然D會繼承兩次A,內部就會存儲兩份A的數據浪費空間,並且還有二義性,D調用A的方法時,因爲有兩個A,究竟時調用哪一個A的方法呢,編譯器也不知道,就會報錯,因此有了虛繼承,解決了空間浪費以及二義性問題。在虛擬繼承下,只有一個共享的基類子對象被繼承,而不管該基類在派生層次中出現多少次。共享的基類子對象被稱爲虛基類。在虛繼承下,基類子對象的複製及由此而引發的二義性都被消除了。

爲何虛函數表中有兩個析構函數?

前面的代碼輸出中咱們能夠看到虛函數表中有兩個析構函數,一個標誌爲deleting,一個標誌爲complete,由於對象有兩種構造方式,棧構造和堆構造,因此對應的實現上,對象也有兩種析構方式,其中堆上對象的析構和棧上對象的析構不一樣之處在於,棧內存的析構不須要執行 delete 函數,會自動被回收。

爲何構造函數不能是虛函數?

構造函數就是爲了在編譯階段肯定對象的類型以及爲對象分配空間,若是類中有虛函數,那就會在構造函數中初始化虛函數表,虛函數的執行卻須要依賴虛函數表。若是構造函數是虛函數,那它就須要依賴虛函數表纔可執行,而只有在構造函數中才會初始化虛函數表,雞生蛋蛋生雞的問題,很矛盾,因此構造函數不能是虛函數。

爲何基類析構函數要是虛函數?

通常基類的析構函數都要設置成虛函數,由於若是不設置成虛函數,在析構的過程當中只會調用到基類的析構函數而不會調用到子類的析構函數,可能會產生內存泄漏。

小總結

offset_to_top對象在對象佈局中與對象頂部地址的偏移量。

RTTI指針指向存儲運行時類型信息(type_info)的地址,用於運行時類型識別,用於typeid和dynamic_cast。

vbase_offset對象在對象佈局中與指向虛基類虛函數表的指針地址的偏移量。

vcall_offset父類引用或指針指向子類對象,調用被子類重寫的方法時,用於對虛函數執行指針地址調整,方便成功調用被重寫的方法。

thunk: 表示上面虛函數表中帶有adjustment字段的函數調用須要先進行this指針調整,才能夠調用到被子類重寫的函數。

最後經過兩張圖總結一下對象在Linux中的佈局:

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

如圖:

a做爲對象指針存儲在棧中,指向在堆中的類A的實例內存,其中實例內存佈局中有虛函數表指針,指針指向的虛函數表存放在數據段中,虛函數表中的各個函數指針指向的函數在代碼段中。

虛表結構大致如上圖,正常的虛表結構中都含有後三項,當有虛繼承狀況下會有前兩個表項。

參考資料:

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新特性,全部知識點都在這了!

你的c++團隊還在禁用異常處理嗎?

JNI編程如何巧妙獲取JNIEnv

Linux 爲何要動態連接?與靜態連接的區別是什麼?

內存對齊之格式修訂版

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

gcc a.c 究竟經歷了什麼?

談談程序連接及分段那些事


「 在看的,麻煩點一下再走~ 」

本文分享自微信公衆號 - 程序喵大人(dmjjzl1)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索