struct實例字段的內存佈局(Layout)和大小(Size)
在C/C++中,struct類型中的成員的一旦聲明,則實例中成員在內存中的佈局(Layout)順序就定下來了,即與成員聲明的順序相同,並且在默認情況下總是按照結構中佔用空間最大的成員進行對齊(Align);當然我們也可以通過設置或編碼來設置內存對齊的方式.
然而在.net託管環境中,CLR提供了更自由的方式來控制struct中Layout:我們可以在定義struct時,在struct上運用StructLayoutAttribute特性來控制成員的內存佈局。默認情況下,struct實例中的字段在棧上的佈局(Layout)順序與聲明中的順序相同,即在struct上運用[StructLayoutAttribute(LayoutKind.Sequential)]特性,這樣做的原因是結構常用於和非託管代碼交互的情形。如果我們正在創建一個與非託管代碼沒有任何互操作的struct類型,我們很可能希望改變C#編譯器的這種默認規則,因此LayoutKind除了Sequential成員之外,還有兩個成員Auto和Explicit,給StructLayoutAttribute傳入LayoutKind.Auto可以讓CLR按照自己選擇的最優方式來排列實例中的字段;傳入LayoutKind.Explicit可以使字段按照我們的在字段上設定的FieldOffset來更靈活的設置字段排序方式,但這種方式也挺危險的,如果設置錯誤後果將會比較嚴重。下面就看幾個示例,算下四個struct各佔多少Byte?
1.[StructLayout(LayoutKind.Sequential)]
{
bool i; //1Byte
double c;//8byte
bool b; //1byte
}
sizeof(StructDeft)得到的結果是24byte!啊哈,本身只有10byte的數據卻佔有了24byte的內存,這是因爲默認(LayoutKind.Sequential)情況下,CLR對struct的Layout的處理方法與C/C++中默認的處理方式相同(8+8+8=24),即按照結構中佔用空間最大的成員進行對齊(Align)。10byte的數據卻佔有了24byte,嚴重地浪費了內存,所以如果我們正在創建一個與非託管代碼沒有任何互操作的struct類型,最好還是不要使用默認的StructLayoutAttribute(LayoutKind.Sequential)特性。
2.[StructLayout(LayoutKind.Explicit)]
struct BadStruct
{
[FieldOffset(0)]
public bool i; //1Byte
[FieldOffset(0)]
public double c;//8byte
[FieldOffset(0)]
public bool b; //1byte
}
sizeof(BadStruct)得到的結果是9byte,顯然得出的基數9顯示CLR並沒對結構體進行任何內存對齊(Align);本身要佔有10byte的數據卻只佔了9byte,顯然有些數據被丟失了,這也正是我給struct取BadStruct作爲名字的原因。如果在struct上運用了[StructLayout(LayoutKind.Explicit)],計算FieldOffset一定要小心,例如我們使用上面BadStruct來進行下面的測試:
e.c = 0;
e.i = true;
Console.WriteLine(e.c);
輸出的結果不再是0了,而是4.94065645841247E-324,這是因爲e.c和e.i共享同一個byte,執行“e.i = true;時”也改變了e.c,CPU在按照浮點數的格式解析e.c時就得到了這個結果.所以在運用LayoutKind.Explicit時千萬別吧FieldOffset算錯了:)
3.[StructLayout(LayoutKind.Auto)]
sizeof(StructAuto)得到的結果是12byte。下面來測試下這StructAuto的三個字段是如何擺放的:
{
StructAuto s = new StructAuto();
Console.WriteLine(string.Format("i:{0}", (int)&(s.i)));
Console.WriteLine(string.Format("c:{0}", (int)&(s.c)));
Console.WriteLine(string.Format("b:{0}", (int)&(s.b)));
}
// 測試結果:
i:1242180
c:1242172
b:1242181
即CLR會對結構體中的字段順序進行調整,將i調到c之後,使得StructAuto的實例s佔有儘可能少的內存,並進行4byte的內存對齊(Align),字段順序調整結果如下圖所示:
4.空struct實例的Size
無論運用上面LayoutKind的Explicit、Auto還是Sequential,得到的sizeof(EmptyStct)都是1byte。
結論:
默認(LayoutKind.Sequential)情況下,CLR對struct的Layout的處理方法與C/C++中默認的處理方式相同,即按照結構中佔用空間最大的成員進行對齊(Align);
使用LayoutKind.Explicit的情況下,CLR不對結構體進行任何內存對齊(Align),而且我們要小心就是FieldOffset;
使用LayoutKind.Auto的情況下,CLR會對結構體中的字段順序進行調整,使實例佔有儘可能少的內存,並進行4byte的內存對齊(Align)。