前言
??本文會展示內存對齊,及繼承、虛繼承等各個情況下內存的布局,并根據結果總結使用場景。
基本調試方法
??使用編譯器自帶的工具,在Visual Studio下,右鍵解決方案,在彈出的菜單下,點擊屬性:
/d1 reportAllClassLayout
,點擊確定。/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.h和immintrin.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層的雙虛繼承,這樣能省下兩個虛指針的空間;如果有后續的繼承,那么要根據需要(是否要消除歧義、繼承圖的樣子等),來選擇是否要虛繼承。