面試系列之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ì)象的布局情況吧~
如下代碼:
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é)。
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ì)象布局圖如下:
我們來探秘下傳說中的虛函數(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]
adjustment: -16 non-virtual]
8 | Derive::~Derive() [deleting]
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]
adjustment: -16 non-virtual]
9 | Derive::~Derive() [deleting]
adjustment: -16 non-virtual]
10 | void Derive::FuncC()
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]
adjustment: -16 non-virtual]
9 | Derive::~Derive() [deleting]
adjustment: -16 non-virtual]
10 | void Derive::FuncB()
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]
adjustment: 0 non-virtual, -24 vcall offset offset]
11 | Derive::~Derive() [deleting]
adjustment: 0 non-virtual, -24 vcall offset offset]
12 | void Derive::FuncB()
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]
adjustment: 0 non-virtual, -24 vcall offset offset]
12 | Derive::~Derive() [deleting]
adjustment: 0 non-virtual, -24 vcall offset offset]
13 | void Derive::FuncB()
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]
adjustment: -16 non-virtual]
11 | Derive::~Derive() [deleting]
adjustment: -16 non-virtual]
12 | void Derive::FuncB()
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]
adjustment: 0 non-virtual, -24 vcall offset offset]
18 | Derive::~Derive() [deleting]
adjustment: 0 non-virtual, -24 vcall offset offset]
19 | void Derive::FuncB()
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)都在這了!
Linux 為什么要?jiǎng)討B(tài)鏈接?與靜態(tài)鏈接的區(qū)別是什么?
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!