C++內存對齊及內存布局

前言

??本文會展示內存對齊,及繼承、虛繼承等各個情況下內存的布局,并根據結果總結使用場景。

基本調試方法

??使用編譯器自帶的工具,在Visual Studio下,右鍵解決方案,在彈出的菜單下,點擊屬性:

??在屬性頁中,依次找到 配置屬性 》C/C++ 》命令行,在【其他選項】中輸入/d1 reportAllClassLayout,點擊確定。
??隨后重新編譯(重新生成解決方案),就可以在輸出欄看到內存的布局(輸出不少,可以點擊輸出欄后,用CTRL+F查找):
??當然,查看單個類也是可以的,只需要輸入:/d1 reportSingleClassLayout○○○,用類名直接代替最后的○○○即可。

一、內存對齊

1.單個變量

??C++普通類占用內存的只有成員變量,普通成員函數不占用類的內存空間:

class TEST{
    int a;
};
//class TEST    size(4):
//  +---
// 0    | a
//  +---

2.多個變量

class TEST{
    double d;
    int a;
};
//class TEST    size(16):
//  +---
// 0    | d
// 8    | a
//      | <alignment member> (size=4)
//  +---

??C++默認對齊大小為類內最大的基礎類型大小,因此int變量后多出4個字節的對齊空間。

3.多變量實驗

class TEST{
    char c;
    int a;
    double d;
};
//class TEST    size(16):
//  +---
// 0    | c
//      | <alignment member> (size=3)
// 4    | a
// 8    | d
//  +---
class TEST{
    int a;
    char c;
    double d;
};
//class TEST    size(16):
//  +---
// 0    | a
// 4    | c
//      | <alignment member> (size=3)
// 8    | d
//  +---
class TEST{
    double d;
    int a;
    char c;
};
//class TEST    size(16):
//  +---
// 0    | d
// 8    | a
//12    | c
//      | <alignment member> (size=3)
//  +---

??這三個都沒有變化,但假如:

class TEST{
    char c;
    double d;
    int a;
};
//class TEST    size(24):
//  +---
// 0    | c
//      | <alignment member> (size=7)
// 8    | d
//16    | a
//      | <alignment member> (size=4)
//  +---

4.內存對齊規則

??發生了變化,多了8個字節,由此得到規律:
??想像一個表格,列數為擁有最大基礎類型長度,例如上面的double,長度為8字節,則每行8列:

對齊\字節 1 2 3 4 5 6 7 8
0
8
16

??將變量從上到下填充,塞入當前變量時,如果能在當前行塞下,就塞入,塞不下,就另起一行再塞入:

對齊\字節 1 2 3 4 5 6 7 8
0 char
8 double_1 double_2 double_3 double_4 double_5 double_6 double_7 double_8
16 int_1 int_2 int_3 int_4

??同時,每個變量只會放在整[自己大小的]的字節處,double只會從整8字節開始,int只會從整4字節開始,short只會從整2字節開始,char就隨意放置,上面多變量實驗第一例的內存布局就如下圖所示:

對齊\字節 1 2 3 4 5 6 7 8
0 char int_1 int_2 int_3 int_4
8 double_1 double_2 double_3 double_4 double_5 double_6 double_7 double_8

數組成員

??數組成員相當于在當前位置直接定義當前數組長度個變量,對齊長度不會變成數組占用的字節數。

非基礎類型成員

struct Box {
    double a;
    char b;
};
class TEST{
    Box b;
    char c;
};
//class Box size(16):
//  +---
// 0    | a
// 8    | b
//      | <alignment member> (size=7)
//  +---
//
//class TEST    size(40):
//  +---
// 0    | Box b
//16    | c
//      | <alignment member> (size=7)
//  +---

??相當于直接把成員對象空間堆在里面,同時持有類的對齊也會受到成員對象的影響,上例TEST的對齊大小受Box的影響,變成了8

5.更大的對齊

??看起來內存對齊的最大對齊就是8,畢竟我們所熟知的類型中,只有double才會占用8字節,非基礎類型成員的大小也不會直接作用于內存對齊。
??不過更大的基礎類型也是存在的,就比如在SIMD類型中_m128和_m256分別會占用16字節和32字節,并且它們的空間占用會導致內存對齊數的增大。如果你的類中只有_m256和char的變量各一個,用上面的方法,會看到char后面會跟著31個內存對齊空間。
??這兩個類型分別在nmmintrin.himmintrin.h中,相關操作可以查閱SIMD的資料。

6.結論

??不同的變量排列方式會改變對象的大小,建議變量從大到小或從小到大排列,這樣空間會達到最優。
??學會內存的對齊有什么意義呢?從上例來看,好像只能優化對象的布局,使其占用空間更小。在當今的計算機,我們普遍不關注幾個字節的空間占用,一個用途,就是如果數據使用結構化存儲,當數據量十分龐大,成千上萬,乃至千萬、上億時,省下的空間能肉眼可見;不過這種存儲技術通常都有相關優化,一般也不是我們所關心的。
??不過我在編寫代碼時,遇到了一定要內存對齊的情況,是編寫SIMD(單指令流多數據流)時遇到的,詳情可見SIMD類型堆上分配方法探究

7.其他有關內存布局的C++特性與函數

①_declspec關鍵字
??__declspec用于指定所給定類型的實例的與Microsoft相關的存儲方式,通常用法是__declspec(表達式),當表達式為align(n)時(n為2的整次冪),可設置對象地址對齊:

_declspec(align(16)) class TEST {
    char c;
};
//class TEST    size(16):
//  +---
// 0    | c
//  +---

??可以看到對象最小大小為16,不過最終大小可以不為16的倍數,對齊地址和對齊內存塊還是有區別的,這種方式能對齊靜態存儲,但動態分配的內存不能保證對齊地址。
②alignas、alignof關鍵字
??C++11標準的關鍵字。
??alignof可在運行時得到類型的對齊值:

_declspec(align(4)) class TEST {
    double a;
};
int main() {
    std::cout << alignof(TEST);//output: 8
_declspec(align(16)) class TEST {
    double a;
};//output: 16

??alignas可在定義變量時,改變當前變量的對齊值:

_declspec(align(16)) class TEST {
    double a;
    double b;
    alignas(32) char c;
};
int main() {
    std::cout << alignof(TEST);//output: 32
    system("pause");
}
//class TEST    size(64):
//  +---
// 0    | a
// 8    | b
//      | <alignment member> (size=16)
//32    | c
//      | <alignment member> (size=31)
//  +---

③_mm_malloc, _mm_free函數
??SIMD庫帶的函數,用于分配地址對齊的動態內存,常常和placement new配合使用,使用事例參照上面那個遇到的SIMD坑。

二、繼承、帶有虛函數、虛繼承的內存布局

1.單個類

??帶有一個或多個虛函數,將得到一個虛表指針vfptr:

class TEST{
    int a;
    virtual bool func() {}
};
//class TEST    size(8):
//  +---
// 0    | {vfptr}
// 4    | a
//  +---

??虛表指針vfptr會指向存有虛函數的虛表,虛表本身大小我們不必考慮。
??vfptr會放到成員變量前,單單看這個例子,vfptr的大小是4,不過如果增加一個更大的變量:

class TEST{
    double d;
    int a;
    virtual bool func() {}
};
//class TEST    size(24):
//  +---
// 0    | {vfptr}
// 8    | d
//16    | a
//      | <alignment member> (size=4)
//  +---

??在x64平臺上vfptr大小變成了8,在x86平臺上,大小還是4,但內存會對齊四個,最終占用大小還是8;如果是SIMD類型,vfptr的大小甚至可能是16、32。

2.繼承

??此時出現繼承,有四種情況,有無虛函數、有無虛繼承的兩兩組合:

  • 無虛繼承,無虛函數
class TEST2 :  public TEST {
    int a2;
};
//class TEST2   size(32):
//  +---
// 0    | +--- (base class TEST)
// 0    | | {vfptr}
// 8    | | d
//16    | | a
//  | | <alignment member> (size=4)
//  | +---
//24    | a2
//      | <alignment member> (size=4)
//  +---

??直接把父類的內存放到自己的最前面,同時內存布局繼承父類的(父類有double,內存對齊為8,子類也對齊為8),和上面的對象成員方式基本一致。

  • 無虛繼承,有虛函數
class TEST2 :  public TEST {
    int a2;
    virtual void func2() {}
};

??生成結果和無虛繼承,無虛函數等同,沒有變化(沒出現虛表指針vfptr),推測為:和父類共用虛表指針。

  • 虛繼承,無虛函數
class TEST2 :  virtual public TEST {
    int a2;
};
//class TEST2   size(32):
//  +---
// 0    | {vbptr}
// 4    | a2
//  +---
//  +--- (virtual base TEST)
// 8    | {vfptr}
//16    | d
//24    | a
//      | <alignment member> (size=4)
//  +---

??在自身成員變量前,增加指向父類的虛指針(vbptr),這個虛指針的大小和對齊不與父類相同,但如果自身用更大的基礎變量,同樣會導致vbptr向更大變量對齊:

class TEST2 :  virtual public TEST {
    int a2;
    double d2;
};
//class TEST2   size(48):
//  +---
// 0    | {vbptr}
// 8    | a2
//      | <alignment member> (size=4)
//16    | d2
//  +---
//  +--- (virtual base TEST)
//24    | {vfptr}
//32    | d
//40    | a
//      | <alignment member> (size=4)
//  +---

??圖中可見,TEST2的vbptr的大小變成了8。

  • 虛繼承,有虛函數
class TEST2 :  virtual public TEST {
    int a2;
    double d2;
    virtual void func2() {}
};
//class TEST2   size(56):
//  +---
// 0    | {vfptr}
// 8    | {vbptr}
//16    | a2
//      | <alignment member> (size=4)
//24    | d2
//  +---
//  +--- (virtual base TEST)
//32    | {vfptr}
//40    | d
//48    | a
//      | <alignment member> (size=4)
//  +---

??在父類虛指針vbptr前,再次出現了自己指向虛表的虛指針vfptr,其大小變化方式和vbptr相同,在這個有double類型的TEST2中,大小都變成了8。

3.總結

三、鉆石繼承結構的內存布局

1.一把梭式的直接繼承

class TEST{
    int a;
    virtual bool func() {}
};

class TEST2 :  public TEST {
    int a2;
    virtual void func2() {}
};

class TEST3 : public TEST {
    int a3;
    virtual void func3() {}
};

class TEST4 : public TEST2, public TEST3 {
    int a4;
    virtual void func4() {}
};
//class TEST    size(8):
//  +---
// 0    | {vfptr}
// 4    | a
//  +---
//class TEST2   size(12):
//  +---
// 0    | +--- (base class TEST)
// 0    | | {vfptr}
// 4    | | a
//  | +---
// 8    | a2
//  +---
//TEST3與TEST2一致
//.....
//class TEST4   size(28):
//  +---
// 0    | +--- (base class TEST2)
// 0    | | +--- (base class TEST)
// 0    | | | {vfptr}
// 4    | | | a
//  | | +---
// 8    | | a2
//  | +---
//12    | +--- (base class TEST3)
//12    | | +--- (base class TEST)
//12    | | | {vfptr}
//16    | | | a
//  | | +---
//20    | | a3
//  | +---
//24    | a4
//  +---

??和前面說的一樣,都是直接堆放,可以發現,TEST2和TEST3的空間中,各有一個TEST。

2.各層虛繼承

??我們將上圖從上到下分為1,2,3三層。

①2對1層單個虛繼承實驗
class TEST2 : virtual public TEST {
    int a2;
    virtual void func2() {}
};
//其他不變
//class TEST2   size(20):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a2
//  +---
//  +--- (virtual base TEST)
//12    | {vfptr}
//16    | a
//  +---
//class TEST4   size(36):
//  +---
// 0    | +--- (base class TEST2)
// 0    | | {vfptr}
// 4    | | {vbptr}
// 8    | | a2
//  | +---
//12    | +--- (base class TEST3)
//12    | | +--- (base class TEST)
//12    | | | {vfptr}
//16    | | | a
//  | | +---
//20    | | a3
//  | +---
//24    | a4
//  +---
//  +--- (virtual base TEST)
//28    | {vfptr}
//32    | a
//  +---

??TEST2變成了前面虛繼承,有虛函數的情況,而TEST4因為未采取虛繼承,依舊是直接把TEST2直接放在前面

②2對1層雙虛繼承實驗
//對TEST3做與TEST2相同的變化,既變為虛繼承
//class TEST4   size(36):
//  +---
// 0    | +--- (base class TEST2)
// 0    | | {vfptr}
// 4    | | {vbptr}
// 8    | | a2
//  | +---
//12    | +--- (base class TEST3)
//12    | | {vfptr}
//16    | | {vbptr}
//20    | | a3
//  | +---
//24    | a4
//  +---
//  +--- (virtual base TEST)
//28    | {vfptr}
//32    | a
//  +---

??TEST4的粗暴堆放沒有變化,但TEST只有一個了!

③2對1層全虛繼承,3對2層單虛繼承
class TEST4 : virtual public TEST2, public TEST3 {
    int a4;
    virtual void func4() {}
};
//class TEST4   size(36):
//  +---
// 0    | +--- (base class TEST3)
// 0    | | {vfptr}
// 4    | | {vbptr}
// 8    | | a3
//  | +---
//12    | a4
//  +---
//  +--- (virtual base TEST)
//16    | {vfptr}
//20    | a
//  +---
//  +--- (virtual base TEST2)
//24    | {vfptr}
//28    | {vbptr}
//32    | a2
//  +---

??TEST4只堆放TEST3,TEST2不再被堆放,但TEST4的父節點指針vbptr和虛表指針vfptr都未出現。

④全虛繼承
//class TEST4   size(44):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a4
//  +---
//  +--- (virtual base TEST)
//12    | {vfptr}
//16    | a
//  +---
//  +--- (virtual base TEST2)
//20    | {vfptr}
//24    | {vbptr}
//28    | a2
//  +---
//  +--- (virtual base TEST3)
//32    | {vfptr}
//36    | {vbptr}
//40    | a3
//  +---

??TEST4的vfptr和vbptr同時出現了!

⑤3對2層全虛繼承,2對1層非全虛繼承
//class TEST4   size(44):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a4
//  +---
//  +--- (virtual base TEST2)
//12    | +--- (base class TEST)
//12    | | {vfptr}
//16    | | a
//  | +---
//20    | a2
//  +---
//  +--- (virtual base TEST)
//24    | {vfptr}
//28    | a
//  +---
//  +--- (virtual base TEST3)
//32    | {vfptr}
//36    | {vbptr}
//40    | a3
//  +---

??TEST2再次堆放TEST空間,并且TEST3也指向一個TEST空間,可以看出2對1層未全虛繼承,就無法消除存在多個TEST的歧義。

⑥更多繼承

??如果有后續的繼承結構,例如下面:
class TEST4 : virtual public TEST2, public TEST3 {
    int a4;
    virtual void func4() {}
};

class TEST5 : public TEST3 {
    int a5;
    virtual void func5() {}
};

class TEST6 : virtual public TEST4, virtual public TEST5 {
    int a6;
    virtual void func6() {}
};
//class TEST6   size(64):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a6
//  +---
//  +--- (virtual base TEST)
//12    | {vfptr}
//16    | a
//  +---
//  +--- (virtual base TEST2)
//20    | {vfptr}
//24    | {vbptr}
//28    | a2
//  +---
//  +--- (virtual base TEST4)
//32    | +--- (base class TEST3)
//32    | | {vfptr}
//36    | | {vbptr}
//40    | | a3
//  | +---
//44    | a4
//  +---
//  +--- (virtual base TEST5)
//48    | +--- (base class TEST3)
//48    | | {vfptr}
//52    | | {vbptr}
//56    | | a3
//  | +---
//60    | a5
//  +---

??如果TEST4和TEST5未保證對TEST3的虛繼承,TEST6就會存在兩個TEST3,不過TEST依舊只存在一個。
??不過這種情況下,TEST4對TEST2是否為虛繼承就無關緊要了

3.結論

??2對1層的雙虛繼承,就足夠保證消除二義性,如果保證TEST4這一層未來不會繼續被繼承,可以不用保證3對2層的雙虛繼承,這樣能省下兩個虛指針的空間;如果有后續的繼承,那么要根據需要(是否要消除歧義、繼承圖的樣子等),來選擇是否要虛繼承。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容