日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C++类对象在内存中的布局

發(fā)布時間:2025/3/15 c/c++ 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++类对象在内存中的布局 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

一、前言

二、C++ 類對象的內存布局

2.1 只有數據成員的對象

2.2 沒有虛函數的對象

2.3 擁有僅一個虛函數的類對象

2.4 擁有多個虛函數的類對象

三、繼承關系中的C++類對象內存分布

3.1 存在繼承關系且本身不存在虛函數的派生類的內存布局

?3.2??本身不存在虛函數(不嚴謹)但存在基類虛函數覆蓋的單繼承類的內存布局

3.3 定義了基類沒有的虛函數的單繼承的類對象布局

3.4 多繼承且存在虛函數覆蓋同時又存在自身定義的虛函數的類對象布局

3.5?如果第1個直接基類沒有虛函數

3.6?兩個基類都沒有虛函數表

?3.7 如果有三個基類: 虛函數表分別是有, 沒有, 有!

四、C++中父子對象指針間的轉換與函數調用

一、前言

大家都應該知道C++的精髓是虛函數吧? 虛函數帶來的好處就是:可以定義一個基類的指針,其指向一個繼承類,當通過基類的指針去調用函數時,可以在運行時決定該調用基類的函數還是繼承類的函數。虛函數是實現多態(tài)(動態(tài)綁定)/接口函數的基礎。可以說: 沒有虛函數, C++將變得一無是處!

二、C++ 類對象的內存布局

要想知道C++對象的內存布局, 可以有多種方式, 比如:

  • 輸出成員變量的偏移, 通過offsetof宏來得到
  • 通過調試器查看, 比如常用的VS
  • 2.1 只有數據成員的對象

    類實現如下:

    class Base1 { public:int base1_1;int base1_2; };

    對象大小及偏移:

    sizeof(Base1)8
    offsetof(Base1, base1_1)0
    offsetof(Base1, base1_2)4

    可知對象布局:

    ?可以看到, 成員變量是按照定義的順序來保存的, 最先聲明的在最上邊, 然后依次保存!
    類對象的大小就是所有成員變量大小之和.

    2.2 沒有虛函數的對象

    類實現如下:

    class Base1 { public:int base1_1;int base1_2;void foo(){} };

    結果如下:

    sizeof(Base1)8
    offsetof(Base1, base1_1)0
    offsetof(Base1, base1_2)4

    和前面的結果是一樣。
    因為如果一個函數不是虛函數,那么他就不可能會發(fā)生動態(tài)綁定,也就不會對對象的布局造成任何影響。
    當調用一個非虛函數時,那么調用的一定就是當前指針類型擁有的那個成員函數。這種調用機制在編譯時期就確定下來了。

    2.3 擁有僅一個虛函數的類對象

    類實現如下:

    class Base1 { public:int base1_1;int base1_2;virtual void base1_fun1() {} };

    結果如下:

    sizeof(Base1)12
    offsetof(Base1, base1_1)4
    offsetof(Base1, base1_2)8

    從圖可以看出多了4個字節(jié),且 base1_1 和 base1_2 的偏移都各自向后多了4個字節(jié)!
    說明類對象的最前面被多加了4個字節(jié)的內容。
    定義

    Base1 b1;

    我們通過VS來瞧瞧類Base1的變量b1的內存布局情況:
    (由于我沒有寫構造函數, 所以變量的數據沒有根據, 但虛函數是編譯器為我們構造的, 數據正確!)
    (Debug模式下, 未初始化的變量值為0xCCCCCCCC, 即:-858983460)

    看到沒? base1_1前面多了一個變量?__vfptr(常說的虛函數表vtable指針), 其類型為void**, 這說明它是一個void*指針(注意:不是數組).

    再看看[0]元素, 其類型為void*, 其值為?ConsoleApplication2.exe!Base1::base1_fun1(void), 這是什么意思呢? 如果對WinDbg比較熟悉, 那么應該知道這是一種慣用表示手法, 她就是指?Base1::base1_fun1() 函數的地址。

    可得, __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? 為什么它被定義成一個指向指針數組的指針, 而不是直接定義成一個指針數組呢?

    我為什么要提這樣一個問題? 因為如果僅是一個指針的情況, 您就無法輕易地修改那個數組里面的內容, 因為她并不屬于類對象的一部分。
    屬于類對象的, 僅是一個指向虛函數表的一個指針__vfptr而已, 下一節(jié)我們將繼續(xù)討論這個問題。

    注意到__vfptr前面的const修飾。她修飾的是那個虛函數表, 而不是__vfptr。

    現在的對象布局如下:

    虛函數指針__vfptr位于所有的成員變量之前定義.

    注意到: 我并未在此說明__vfptr的具體指向, 只是說明了現在類對象的布局情況.
    接下來看一個稍微復雜一點的情況, 我將清楚地描述虛函數表的構成。

    2.4 擁有多個虛函數的類對象

    和前面一個例子差不多, 只是再加了一個虛函數. 定義如下:

    class Base1 { public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {} };

    ?大小以及偏移信息如下:

    有情況!? 多了一個虛函數, 類對象大小卻依然是12個字節(jié)!

    再來看看VS形象的表現:

    ?

    呀, __vfptr所指向的函數指針數組中出現了第2個元素, 其值為Base1類的第2個虛函數base1_fun2()的函數地址.

    現在, 虛函數指針以及虛函數表的偽定義大概如下:

    void* __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 }; const void** __vfptr = &__fun[0];

    通過上面兩張圖表, 我們可以得到如下結論:

  • 更加肯定前面我們所描述的: __vfptr只是一個指針, 她指向一個函數指針數組(即: 虛函數表)
  • 增加一個虛函數, 只是簡單地向該類對應的虛函數表中增加一項而已, 并不會影響到類對象的大小以及布局情況
  • 前面已經提到過: __vfptr只是一個指針, 她指向一個數組, 并且: 這個數組沒有包含到類定義內部(只有一個指向虛函數表的虛函數表指針), 那么她們之間是怎樣一個關系呢?
    不妨, 我們再定義一個類的變量b2,即Base1 b1; 現在再來看看__vfptr的指向:

    ?

    通過Watch 1窗口我們看到:

  • b1和b2是類的兩個變量, 理所當然, 她們的地址是不同的(見 &b1 和 &b2)
  • 雖然b1和b2是類的兩個變量, 但是: 她們的__vfptr的指向卻是同一個虛函數表
  • 由此我們可以總結出:

    同一個類的不同實例共用同一份虛函數表, 她們都通過一個所謂的虛函數表指針__vfptr(定義為void**類型)指向該虛函數表。

    是時候該展示一下類對象的內存布局情況了:

    ?

    那么問題就來了! 這個虛函數表保存在哪里呢? 其實, 我們無需過分追究她位于哪里, 重點是:

  • 她是編譯器在編譯時期為我們創(chuàng)建好的, 只存在一份
  • 定義類對象時, 編譯器自動將類對象的__vfptr指向這個虛函數表
  • 三、繼承關系中的C++類對象內存分布

    3.1 存在繼承關系且本身不存在虛函數的派生類的內存布局

    前面研究了那么多啦, 終于該到研究繼承類了! 先研究單繼承!

    依然, 簡單地定義一個繼承類, 如下:

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

    我們再來看看現在的內存布局(定義為Derive1 d1):

    ?

    沒錯! 基類在上邊, 繼承類的成員在下邊依次定義! 展開來看看:

    ?

    經展開后來看, 前面部分完全就是Base1的東西: 虛函數表指針+成員變量定義.
    并且, Base1的虛函數表的[0][1]兩項還是其本身就擁有的函數: base1_fun1() 和 base1_fun2().

    現在類的布局情況應該是下面這樣:

    ?

    3.2??本身不存在虛函數(不嚴謹)但存在基類虛函數覆蓋的單繼承類的內存布局

    標題`本身不存在虛函數`的說法有些不嚴謹, 我的意思是說: 除經過繼承而得來的基類虛函數以外, 自身沒有再定義其它的虛函數.

    Ok, 既然存在基類虛函數覆蓋, 那么來看看接下來的代碼會產生何種影響:

    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 base1_fun1() {} }

    可以看到, Derive1類 重寫了Base1類的base1_fun1()函數, 也就是常說的虛函數覆蓋. 現在是怎樣布局的呢?

    ?

    特別注意我高亮的那一行: 原本是Base1::base1_fun1(), 但由于繼承類重寫了基類Base1的此方法, 所以現在變成了Derive1::base1_fun1()!

    那么, 無論是通過Derive1的指針還是Base1的指針來調用此方法, 調用的都將是被繼承類重寫后的那個方法(函數), 多態(tài)發(fā)生鳥!!!(這里其實指的就是C++中的動態(tài)綁定,即基類指針或引用指向派生類對象時,如果調用了虛函數,判斷虛函數的方式是先到派生類中找。C++primer第五版 536頁的樣子有講。)

    那么新的布局圖:

    ?

    ?這里有個問題,派生類重定義了基類的虛函數,那基類的虛函數還存在嗎?

    其實是存在的,也就是上面分析的這塊內存是只要定義一個對象,就會分配這塊內存到他的堆或棧上。

    3.3 定義了基類沒有的虛函數的單繼承的類對象布局

    說明一下: 由于前面一種情況只會造成覆蓋基類虛函數表的指針, 所以接下來我不再同時討論虛函數覆蓋的情況.

    繼續(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é)不同的是多了一個自身定義的虛函數。和3.2節(jié)不同的是沒有基類虛函數的覆蓋。

    ?

    咦, 有沒有發(fā)現問題? 表面上看來幾乎和3.1節(jié)情況完全一樣? 為嘛呢?
    現在繼承類明明定義了自身的虛函數, 但不見了??
    那么, 來看看類對象的大小, 以及成員偏移情況吧:

    ?

    居然沒有變化!!! 前面12個字節(jié)是Base1的, 有沒有覺得很奇怪?

    好吧, 既然表面上沒辦法了, 我們就只能從匯編入手了, 來看看調用derive1_fun1()時的代碼:

    Derive1 d1; Derive1* pd1 = &d1; pd1->derive1_fun1();

    要注意: 我為什么使用指針的方式調用? 說明一下: 因為如果不使用指針調用, 虛函數調用是不會發(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? 所以當執(zhí)行完?mov edx, dword ptr[eax]?后, edx就得到了__vfptr的值, 也就是虛函數表的地址.
    第5行: 由于是__thiscall調用, 所以把this保存到ecx中.
    第6行: 一定要注意到那個?edx+8, 由于edx是虛函數表的地址, 那么?edx+8將是虛函數表的第3個元素, 也就是__vftable[2]!!!
    第7行: 調用虛函數.

    結果:

  • 現在我們應該知道內幕了!?繼承類Derive1的虛函數表被加在基類的后面! 事實的確就是這樣!
  • 由于Base1只知道自己的兩個虛函數索引[0][1], 所以就算在后面加上了[2], Base1根本不知情, 不會對她造成任何影響.
  • 如果基類沒有虛函數呢? 這個問題我們留到3.5節(jié)再來討論!
  • 最新的類對象布局表示:

    ?

    3.4 多繼承且存在虛函數覆蓋同時又存在自身定義的虛函數的類對象布局

    真快, 該看看多繼承了, 多繼承很常見, 特別是接口類中!

    依然寫點小類玩玩:

    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;?// 基類虛函數覆蓋?virtual void base1_fun1() {}?virtual void base2_fun2() {}?// 自身定義的虛函數?virtual void derive1_fun1() {}?virtual void derive1_fun2() {} }

    代碼變得越來越長啦! 為了代碼結構清晰, 我盡量簡化定義.

    初步了解一下對象大小及偏移信息:

    ?

    貌似, 若有所思? 不管, 來看看VS再想:

    ?

    結論:

  • 按照基類的聲明順序, 基類的成員依次分布在繼承中.
  • 注意被我高亮的那兩行, 已經發(fā)生了虛函數覆蓋!
  • 我們自己定義的虛函數呢? 怎么還是看不見?!
  • 好吧, 繼承反匯編, 這次的調用代碼如下:

    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個元素(下標為[3])

    結論:

    Derive1的虛函數表依然是保存到第1個擁有虛函數表的那個基類的后面的.

    看看現在的類對象布局圖:

    (注:圖中有點錯誤,右上角應該是?void* __vftable[4],多謝 shadow3002 的提醒)

    (注:圖中有點錯誤,Derive1是存在虛函數覆蓋的。源圖丟失,請讀者注意不要被誤導。多謝 Oyster 的提醒)

    ?

    如果第1個基類沒有虛函數表呢? 進入3.5節(jié)!

    3.5?如果第1個直接基類沒有虛函數

    這次的代碼應該比上一個要稍微簡單一些, 因為把第1個類的虛函數給去掉鳥!

    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;?// 自身定義的虛函數?virtual void derive1_fun1() {}?virtual void derive1_fun2() {} }

    來看看VS的布局:

    ?

    這次相對前面一次的圖來說還要簡單啦! Base1已經沒有虛函數表了! (真實情況并非完全這樣, 請繼續(xù)往下看!)

    現在的大小及偏移情況: 注意:?sizeof(Base1) == 8;

    ?

    重點是看虛函數的位置, 進入函數調用(和前一次是一樣的):

    Derive1 d1; Derive1* pd1 = &d1; pd1->derive1_fun2();

    反匯編調用代碼:

    ; 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 已經沒有虛函數表了, 為什么還是把b1的第1個元素當作__vfptr呢?
    不難猜測: 當前的布局已經發(fā)生了變化,?有虛函數表的基類放在對象內存前面!??, 不過事實是否屬實? 需要仔細斟酌.

    我們可以通過對基類成員變量求偏移來觀察:

    ?

    可以看到:

    &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

    所以不難驗證:?我們前面的推斷是正確的,?誰有虛函數表, 誰就放在前面!

    現在類的布局情況:

    ?

    那么, 如果兩個基類都沒有虛函數表呢?

    3.6?兩個基類都沒有虛函數表

    代碼如下:

    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;?// 自身定義的虛函數?virtual void derive1_fun1() {}?virtual void derive1_fun2() {} }

    前面吃了個虧, 現在先來看看VS的基本布局:

    ?

    可以看到, 現在__vfptr已經獨立出來了, 不再屬于Base1和Base2!

    看看求偏移情況:

    ?

    Ok, 問題解決! 注意高亮的那兩行,?&d1==&d1.__vfptr, 說明虛函數始終在最前面!

    不用再廢話, 相信大家對這種情況已經有底了.

    對象布局:

    ?

    3.7 如果有三個基類: 虛函數表分別是有, 沒有, 有!

    這種情況其實已經無需再討論了, 作為一個完結篇....

    上代碼:

    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;?// 自身定義的虛函數?virtual void derive1_fun1() {}?virtual void derive1_fun2() {} }

    以下是偏移圖:

    ?

    以下是對象布局圖(多謝?@Oyster?的手繪):

    ?

    只需知道:?誰有虛函數表, 誰就往前靠!

    四、C++中父子對象指針間的轉換與函數調用

    講了那么多布局方面的東東, 終于到了尾聲, 好累呀!!!

    通過前面的講解內容, 大家至少應該明白了各類情況下類對象的內存布局了. 如果還不會.....呃..... !@#$%^&*

    進入正題~

    由于繼承完全擁有父類的所有, 包括數據成員與虛函數表, 所以:把一個繼承類強制轉換為一個基類是完全可行的.

    如果有一個Derive1的指針, 那么:

    • 得到Base1的指針: Base1* pb1 = pd1;
    • 得到Base2的指針: Base2* pb2 = pd1;
    • 得到Base3的指針: Base3* pb3 = pd1;

    非常值得注意的是:

    這是在基類與繼承類之間的轉換, 這種轉換會自動計算偏移! 按照前面的布局方式!
    也就是說: 在這里極有可能:?pb1 != pb2 != pb3 ~~, 不要以為她們都等于 pd1!

    至于函數調用, 我想, 不用說大家應該知道了:

  • 如果不是虛函數, 直接調用指針對應的基本類的那個函數
  • 如果是虛函數, 則查找虛函數表, 并進行后續(xù)的調用. 虛函數表在定義一個時, 編譯器就為我們創(chuàng)建好了的. 所有的, 同一個類, 共用同一份虛函數表.(共享表,但是不是在同一塊內存上)
  • ?

    總結

    以上是生活随笔為你收集整理的C++类对象在内存中的布局的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。