C++类对象在内存中的布局
目錄
一、前言
二、C++ 類對象的內(nèi)存布局
2.1 只有數(shù)據(jù)成員的對象
2.2 沒有虛函數(shù)的對象
2.3 擁有僅一個虛函數(shù)的類對象
2.4 擁有多個虛函數(shù)的類對象
三、繼承關(guān)系中的C++類對象內(nèi)存分布
3.1 存在繼承關(guān)系且本身不存在虛函數(shù)的派生類的內(nèi)存布局
?3.2??本身不存在虛函數(shù)(不嚴謹)但存在基類虛函數(shù)覆蓋的單繼承類的內(nèi)存布局
3.3 定義了基類沒有的虛函數(shù)的單繼承的類對象布局
3.4 多繼承且存在虛函數(shù)覆蓋同時又存在自身定義的虛函數(shù)的類對象布局
3.5?如果第1個直接基類沒有虛函數(shù)
3.6?兩個基類都沒有虛函數(shù)表
?3.7 如果有三個基類: 虛函數(shù)表分別是有, 沒有, 有!
四、C++中父子對象指針間的轉(zhuǎn)換與函數(shù)調(diào)用
一、前言
大家都應(yīng)該知道C++的精髓是虛函數(shù)吧? 虛函數(shù)帶來的好處就是:可以定義一個基類的指針,其指向一個繼承類,當(dāng)通過基類的指針去調(diào)用函數(shù)時,可以在運行時決定該調(diào)用基類的函數(shù)還是繼承類的函數(shù)。虛函數(shù)是實現(xiàn)多態(tài)(動態(tài)綁定)/接口函數(shù)的基礎(chǔ)。可以說: 沒有虛函數(shù), C++將變得一無是處!
二、C++ 類對象的內(nèi)存布局
要想知道C++對象的內(nèi)存布局, 可以有多種方式, 比如:
2.1 只有數(shù)據(jù)成員的對象
類實現(xiàn)如下:
class Base1 { public:int base1_1;int base1_2; };對象大小及偏移:
| sizeof(Base1) | 8 |
| offsetof(Base1, base1_1) | 0 |
| offsetof(Base1, base1_2) | 4 |
可知對象布局:
?可以看到, 成員變量是按照定義的順序來保存的, 最先聲明的在最上邊, 然后依次保存!
類對象的大小就是所有成員變量大小之和.
2.2 沒有虛函數(shù)的對象
類實現(xiàn)如下:
class Base1 { public:int base1_1;int base1_2;void foo(){} };結(jié)果如下:
| sizeof(Base1) | 8 |
| offsetof(Base1, base1_1) | 0 |
| offsetof(Base1, base1_2) | 4 |
和前面的結(jié)果是一樣。
因為如果一個函數(shù)不是虛函數(shù),那么他就不可能會發(fā)生動態(tài)綁定,也就不會對對象的布局造成任何影響。
當(dāng)調(diào)用一個非虛函數(shù)時,那么調(diào)用的一定就是當(dāng)前指針類型擁有的那個成員函數(shù)。這種調(diào)用機制在編譯時期就確定下來了。
2.3 擁有僅一個虛函數(shù)的類對象
類實現(xiàn)如下:
class Base1 { public:int base1_1;int base1_2;virtual void base1_fun1() {} };結(jié)果如下:
| sizeof(Base1) | 12 |
| offsetof(Base1, base1_1) | 4 |
| offsetof(Base1, base1_2) | 8 |
從圖可以看出多了4個字節(jié),且 base1_1 和 base1_2 的偏移都各自向后多了4個字節(jié)!
說明類對象的最前面被多加了4個字節(jié)的內(nèi)容。
定義
我們通過VS來瞧瞧類Base1的變量b1的內(nèi)存布局情況:
(由于我沒有寫構(gòu)造函數(shù), 所以變量的數(shù)據(jù)沒有根據(jù), 但虛函數(shù)是編譯器為我們構(gòu)造的, 數(shù)據(jù)正確!)
(Debug模式下, 未初始化的變量值為0xCCCCCCCC, 即:-858983460)
看到?jīng)]? base1_1前面多了一個變量?__vfptr(常說的虛函數(shù)表vtable指針), 其類型為void**, 這說明它是一個void*指針(注意:不是數(shù)組).
再看看[0]元素, 其類型為void*, 其值為?ConsoleApplication2.exe!Base1::base1_fun1(void), 這是什么意思呢? 如果對WinDbg比較熟悉, 那么應(yīng)該知道這是一種慣用表示手法, 她就是指?Base1::base1_fun1() 函數(shù)的地址。
可得, __vfptr的定義偽代碼大概如下:
void* __fun[1] = { &Base1::base1_fun1 }; const void** __vfptr = &__fun[0];值得注意的是:
上面只是一種偽代碼方式, 語法不一定能通過
該類的對象大小為12個字節(jié), 大小及偏移信息如下:
| sizeof(Base1) | 12 |
| offsetof(__vfptr) | 0 |
| offsetof(base1_1) | 4 |
| offsetof(base1_2) | 8 |
大家有沒有留意這個__vfptr? 為什么它被定義成一個指向指針數(shù)組的指針, 而不是直接定義成一個指針數(shù)組呢?
我為什么要提這樣一個問題? 因為如果僅是一個指針的情況, 您就無法輕易地修改那個數(shù)組里面的內(nèi)容, 因為她并不屬于類對象的一部分。
屬于類對象的, 僅是一個指向虛函數(shù)表的一個指針__vfptr而已, 下一節(jié)我們將繼續(xù)討論這個問題。
注意到__vfptr前面的const修飾。她修飾的是那個虛函數(shù)表, 而不是__vfptr。
現(xiàn)在的對象布局如下:
虛函數(shù)指針__vfptr位于所有的成員變量之前定義.
注意到: 我并未在此說明__vfptr的具體指向, 只是說明了現(xiàn)在類對象的布局情況.
接下來看一個稍微復(fù)雜一點的情況, 我將清楚地描述虛函數(shù)表的構(gòu)成。
2.4 擁有多個虛函數(shù)的類對象
和前面一個例子差不多, 只是再加了一個虛函數(shù). 定義如下:
class Base1 { public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {} };?大小以及偏移信息如下:
有情況!? 多了一個虛函數(shù), 類對象大小卻依然是12個字節(jié)!
再來看看VS形象的表現(xiàn):
?
呀, __vfptr所指向的函數(shù)指針數(shù)組中出現(xiàn)了第2個元素, 其值為Base1類的第2個虛函數(shù)base1_fun2()的函數(shù)地址.
現(xiàn)在, 虛函數(shù)指針以及虛函數(shù)表的偽定義大概如下:
void* __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 }; const void** __vfptr = &__fun[0];通過上面兩張圖表, 我們可以得到如下結(jié)論:
前面已經(jīng)提到過: __vfptr只是一個指針, 她指向一個數(shù)組, 并且: 這個數(shù)組沒有包含到類定義內(nèi)部(只有一個指向虛函數(shù)表的虛函數(shù)表指針), 那么她們之間是怎樣一個關(guān)系呢?
不妨, 我們再定義一個類的變量b2,即Base1 b1; 現(xiàn)在再來看看__vfptr的指向:
?
通過Watch 1窗口我們看到:
由此我們可以總結(jié)出:
同一個類的不同實例共用同一份虛函數(shù)表, 她們都通過一個所謂的虛函數(shù)表指針__vfptr(定義為void**類型)指向該虛函數(shù)表。
是時候該展示一下類對象的內(nèi)存布局情況了:
?
那么問題就來了! 這個虛函數(shù)表保存在哪里呢? 其實, 我們無需過分追究她位于哪里, 重點是:
三、繼承關(guān)系中的C++類對象內(nèi)存分布
3.1 存在繼承關(guān)系且本身不存在虛函數(shù)的派生類的內(nèi)存布局
前面研究了那么多啦, 終于該到研究繼承類了! 先研究單繼承!
依然, 簡單地定義一個繼承類, 如下:
class Base1 { public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {} };class Derive1 : ?public Base1 { public:?int derive1_1;?int derive1_2; }我們再來看看現(xiàn)在的內(nèi)存布局(定義為Derive1 d1):
?
沒錯! 基類在上邊, 繼承類的成員在下邊依次定義! 展開來看看:
?
經(jīng)展開后來看, 前面部分完全就是Base1的東西: 虛函數(shù)表指針+成員變量定義.
并且, Base1的虛函數(shù)表的[0][1]兩項還是其本身就擁有的函數(shù): base1_fun1() 和 base1_fun2().
現(xiàn)在類的布局情況應(yīng)該是下面這樣:
?
3.2??本身不存在虛函數(shù)(不嚴謹)但存在基類虛函數(shù)覆蓋的單繼承類的內(nèi)存布局
標(biāo)題`本身不存在虛函數(shù)`的說法有些不嚴謹, 我的意思是說: 除經(jīng)過繼承而得來的基類虛函數(shù)以外, 自身沒有再定義其它的虛函數(shù).
Ok, 既然存在基類虛函數(shù)覆蓋, 那么來看看接下來的代碼會產(chǎn)生何種影響:
class Base1 { public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {} };class Derive1 : ?public Base1 { public:?int derive1_1;?int derive1_2;?// 覆蓋基類函數(shù)?virtual void base1_fun1() {} }可以看到, Derive1類 重寫了Base1類的base1_fun1()函數(shù), 也就是常說的虛函數(shù)覆蓋. 現(xiàn)在是怎樣布局的呢?
?
特別注意我高亮的那一行: 原本是Base1::base1_fun1(), 但由于繼承類重寫了基類Base1的此方法, 所以現(xiàn)在變成了Derive1::base1_fun1()!
那么, 無論是通過Derive1的指針還是Base1的指針來調(diào)用此方法, 調(diào)用的都將是被繼承類重寫后的那個方法(函數(shù)), 多態(tài)發(fā)生鳥!!!(這里其實指的就是C++中的動態(tài)綁定,即基類指針或引用指向派生類對象時,如果調(diào)用了虛函數(shù),判斷虛函數(shù)的方式是先到派生類中找。C++primer第五版 536頁的樣子有講。)
那么新的布局圖:
?
?這里有個問題,派生類重定義了基類的虛函數(shù),那基類的虛函數(shù)還存在嗎?
其實是存在的,也就是上面分析的這塊內(nèi)存是只要定義一個對象,就會分配這塊內(nèi)存到他的堆或棧上。
3.3 定義了基類沒有的虛函數(shù)的單繼承的類對象布局
說明一下: 由于前面一種情況只會造成覆蓋基類虛函數(shù)表的指針, 所以接下來我不再同時討論虛函數(shù)覆蓋的情況.
繼續(xù)貼代碼:
class Base1 { public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {} };class Derive1 : ?public Base1 { public:?int derive1_1;?int derive1_2;?virtual void derive1_fun1() {} }和3.1節(jié)不同的是多了一個自身定義的虛函數(shù)。和3.2節(jié)不同的是沒有基類虛函數(shù)的覆蓋。
?
咦, 有沒有發(fā)現(xiàn)問題? 表面上看來幾乎和3.1節(jié)情況完全一樣? 為嘛呢?
現(xiàn)在繼承類明明定義了自身的虛函數(shù), 但不見了??
那么, 來看看類對象的大小, 以及成員偏移情況吧:
?
居然沒有變化!!! 前面12個字節(jié)是Base1的, 有沒有覺得很奇怪?
好吧, 既然表面上沒辦法了, 我們就只能從匯編入手了, 來看看調(diào)用derive1_fun1()時的代碼:
Derive1 d1; Derive1* pd1 = &d1; pd1->derive1_fun1();要注意: 我為什么使用指針的方式調(diào)用? 說明一下: 因為如果不使用指針調(diào)用, 虛函數(shù)調(diào)用是不會發(fā)生動態(tài)綁定的哦! 你若直接?d1.derive1_fun1();?, 是不可能會發(fā)生動態(tài)綁定的, 但如果使用指針:?pd1->derive1_fun1();?, 那么 pd1就無從知道她所指向的對象到底是Derive1 還是繼承于Derive1的對象, 雖然這里我們并沒有對象繼承于Derive1, 但是她不得不這樣做, 畢竟繼承類不管你如何繼承, 都不會影響到基類, 對吧?
; pd1->derive1_fun1(); 00825466 mov eax,dword ptr [pd1] 00825469 mov edx,dword ptr [eax] 0082546B mov esi,esp 0082546D mov ecx,dword ptr [pd1] 00825470 mov eax,dword ptr [edx+8] 00825473 call eax匯編代碼解釋:
第2行: 由于pd1是指向d1的指針, 所以執(zhí)行此句后 eax 就是d1的地址
第3行: 又因為Base1::__vfptr是Base1的第1個成員, 同時也是Derive1的第1個成員, 那么: &__vfptr == &d1, clear? 所以當(dāng)執(zhí)行完?mov edx, dword ptr[eax]?后, edx就得到了__vfptr的值, 也就是虛函數(shù)表的地址.
第5行: 由于是__thiscall調(diào)用, 所以把this保存到ecx中.
第6行: 一定要注意到那個?edx+8, 由于edx是虛函數(shù)表的地址, 那么?edx+8將是虛函數(shù)表的第3個元素, 也就是__vftable[2]!!!
第7行: 調(diào)用虛函數(shù).
結(jié)果:
最新的類對象布局表示:
?
3.4 多繼承且存在虛函數(shù)覆蓋同時又存在自身定義的虛函數(shù)的類對象布局
真快, 該看看多繼承了, 多繼承很常見, 特別是接口類中!
依然寫點小類玩玩:
class Base1 { public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {} };class Base2 { public:int base2_1;int base2_2;virtual void base2_fun1() {}virtual void base2_fun2() {} };// 多繼承 class Derive1 : ?public Base1, public Base2 { public:?int derive1_1;?int derive1_2;?// 基類虛函數(shù)覆蓋?virtual void base1_fun1() {}?virtual void base2_fun2() {}?// 自身定義的虛函數(shù)?virtual void derive1_fun1() {}?virtual void derive1_fun2() {} }代碼變得越來越長啦! 為了代碼結(jié)構(gòu)清晰, 我盡量簡化定義.
初步了解一下對象大小及偏移信息:
?
貌似, 若有所思? 不管, 來看看VS再想:
?
結(jié)論:
好吧, 繼承反匯編, 這次的調(diào)用代碼如下:
Derive1 d1; Derive1* pd1 = &d1; pd1->derive1_fun2();反匯編代碼如下:
; pd1->derive1_fun2(); 00995306 mov eax,dword ptr [pd1] 00995309 mov edx,dword ptr [eax] 0099530B mov esi,esp 0099530D mov ecx,dword ptr [pd1] 00995310 mov eax,dword ptr [edx+0Ch] 00995313 call eax解釋下, 其實差不多:
第2行: 取d1的地址
第3行: 取Base1::__vfptr的值!!
第6行: 0x0C, 也就是第4個元素(下標(biāo)為[3])
結(jié)論:
Derive1的虛函數(shù)表依然是保存到第1個擁有虛函數(shù)表的那個基類的后面的.
看看現(xiàn)在的類對象布局圖:
(注:圖中有點錯誤,右上角應(yīng)該是?void* __vftable[4],多謝 shadow3002 的提醒)
(注:圖中有點錯誤,Derive1是存在虛函數(shù)覆蓋的。源圖丟失,請讀者注意不要被誤導(dǎo)。多謝 Oyster 的提醒)
?
如果第1個基類沒有虛函數(shù)表呢? 進入3.5節(jié)!
3.5?如果第1個直接基類沒有虛函數(shù)
這次的代碼應(yīng)該比上一個要稍微簡單一些, 因為把第1個類的虛函數(shù)給去掉鳥!
class Base1 { public:int base1_1;int base1_2; };class Base2 { public:int base2_1;int base2_2;virtual void base2_fun1() {}virtual void base2_fun2() {} };// 多繼承 class Derive1 : ?public Base1, public Base2 { public:?int derive1_1;?int derive1_2;?// 自身定義的虛函數(shù)?virtual void derive1_fun1() {}?virtual void derive1_fun2() {} }來看看VS的布局:
?
這次相對前面一次的圖來說還要簡單啦! Base1已經(jīng)沒有虛函數(shù)表了! (真實情況并非完全這樣, 請繼續(xù)往下看!)
現(xiàn)在的大小及偏移情況: 注意:?sizeof(Base1) == 8;
?
重點是看虛函數(shù)的位置, 進入函數(shù)調(diào)用(和前一次是一樣的):
Derive1 d1; Derive1* pd1 = &d1; pd1->derive1_fun2();反匯編調(diào)用代碼:
; pd1->derive1_fun2(); 012E4BA6 mov eax,dword ptr [pd1] 012E4BA9 mov edx,dword ptr [eax] 012E4BAB mov esi,esp 012E4BAD mov ecx,dword ptr [pd1] 012E4BB0 mov eax,dword ptr [edx+0Ch] 012E4BB3 call eax這段匯編代碼和前面一個完全一樣!, 那么問題就來了! Base1 已經(jīng)沒有虛函數(shù)表了, 為什么還是把b1的第1個元素當(dāng)作__vfptr呢?
不難猜測: 當(dāng)前的布局已經(jīng)發(fā)生了變化,?有虛函數(shù)表的基類放在對象內(nèi)存前面!??, 不過事實是否屬實? 需要仔細斟酌.
我們可以通過對基類成員變量求偏移來觀察:
?
可以看到:
&d1==0x~d4 &d1.Base1::__vfptr==0x~d4 &d1.base2_1==0x~d8 &d1.base2_2==0x~dc &d1.base1_1==0x~e0 &d1.base1_2==0x~e4所以不難驗證:?我們前面的推斷是正確的,?誰有虛函數(shù)表, 誰就放在前面!
現(xiàn)在類的布局情況:
?
那么, 如果兩個基類都沒有虛函數(shù)表呢?
3.6?兩個基類都沒有虛函數(shù)表
代碼如下:
class Base1 { public:int base1_1;int base1_2; };class Base2 { public:int base2_1;int base2_2; };// 多繼承 class Derive1 : ?public Base1, public Base2 { public:?int derive1_1;?int derive1_2;?// 自身定義的虛函數(shù)?virtual void derive1_fun1() {}?virtual void derive1_fun2() {} }前面吃了個虧, 現(xiàn)在先來看看VS的基本布局:
?
可以看到, 現(xiàn)在__vfptr已經(jīng)獨立出來了, 不再屬于Base1和Base2!
看看求偏移情況:
?
Ok, 問題解決! 注意高亮的那兩行,?&d1==&d1.__vfptr, 說明虛函數(shù)始終在最前面!
不用再廢話, 相信大家對這種情況已經(jīng)有底了.
對象布局:
?
3.7 如果有三個基類: 虛函數(shù)表分別是有, 沒有, 有!
這種情況其實已經(jīng)無需再討論了, 作為一個完結(jié)篇....
上代碼:
class Base1 { public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {} };class Base2 { public:int base2_1;int base2_2; };class Base3 { public:int base3_1;int base3_2;virtual void base3_fun1() {}virtual void base3_fun2() {} };// 多繼承 class Derive1 : ?public Base1, public Base2, public Base3 { public:?int derive1_1;?int derive1_2;?// 自身定義的虛函數(shù)?virtual void derive1_fun1() {}?virtual void derive1_fun2() {} }以下是偏移圖:
?
以下是對象布局圖(多謝?@Oyster?的手繪):
?
只需知道:?誰有虛函數(shù)表, 誰就往前靠!
四、C++中父子對象指針間的轉(zhuǎn)換與函數(shù)調(diào)用
講了那么多布局方面的東東, 終于到了尾聲, 好累呀!!!
通過前面的講解內(nèi)容, 大家至少應(yīng)該明白了各類情況下類對象的內(nèi)存布局了. 如果還不會.....呃..... !@#$%^&*
進入正題~
由于繼承完全擁有父類的所有, 包括數(shù)據(jù)成員與虛函數(shù)表, 所以:把一個繼承類強制轉(zhuǎn)換為一個基類是完全可行的.
如果有一個Derive1的指針, 那么:
- 得到Base1的指針: Base1* pb1 = pd1;
- 得到Base2的指針: Base2* pb2 = pd1;
- 得到Base3的指針: Base3* pb3 = pd1;
非常值得注意的是:
這是在基類與繼承類之間的轉(zhuǎn)換, 這種轉(zhuǎn)換會自動計算偏移! 按照前面的布局方式!
也就是說: 在這里極有可能:?pb1 != pb2 != pb3 ~~, 不要以為她們都等于 pd1!
至于函數(shù)調(diào)用, 我想, 不用說大家應(yīng)該知道了:
?
總結(jié)
以上是生活随笔為你收集整理的C++类对象在内存中的布局的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux发行版_7款颜值当道的Linu
- 下一篇: c++ 暂停功能_2020.10.16撸