1. 程式人生 > >C#中StructLayout的特性

C#中StructLayout的特性

先讓我們看四個首要的根基概念:

  1.資料型別自身的對齊值:

  對於char型資料,其自身對齊值為1,對於short型為2,對於int,float,double型別,其自身對齊值為4,單位位元組。

  2.結構 體或者類的自身對齊值:其成員中自身對齊值最大的那個值。

  3.指定對齊值:#pragma pack (value)時的指定對齊值value。

  4.資料成員、結構 體和類的有效對齊值:自身對齊值和指定對齊值中小的那個值。

有 了這些值,我們就可以很方便的來討論具體資料結構的成員和其自身的對齊方式。有效對齊值N是最終用來決定資料存放地址方式的值,最重要。有效對齊N,就是 表示“對齊在N上”,也就是說該資料的"存放起始地址%N=0".而資料結構中的資料變數都是按定義的先後順序來排放的。第一個資料變數的起始地址就是數 據結構的起始地址。結構體的成員變數要對齊排放,結構體本身也要根據自身的有效對齊值圓整(就是結構體成員變數佔用總長度需要是對結構體有效對齊值的整數 倍,


對齊原因 
大部分的參考資料都是如是說的: 
1、平臺原因(移植原因):不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。 
2、效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。 
對齊規則 
每個特定平臺上的編譯器都有自己的預設“對齊係數”(也叫對齊模數)。程式設計師可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是你要指定的“對齊係數”。 
規則: 
1、資料成員對齊規則:結構(struct)(或聯合(union))的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員的對齊按照#pragma pack指定的數值和這個資料成員自身長度中,比較小的那個進行。 
2、結構(或聯合)的整體對齊規則:在資料成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最

大資料成員長度中,比較小的那個進行。 
3、結合1、2可推斷:當#pragma pack的n值等於或超過所有資料成員長度的時候,這個n值的大小將不產生任何效果。 
Win32平臺下的微軟C編譯器(cl.exefor 80×86)的對齊策略: 
1)結構體變數的首地址是其最長基本型別成員的整數倍; 
備註:編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本資料型別,然後尋找記憶體地址能是該基本資料型別的整倍的位置,作為結構體的首地址。將這個最寬的基本資料型別的大小作為上面介紹的對齊模數。 
2)結構體每個成員相對於結構體首地址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充位元組(internal adding); 
備註:為結構體的一個成員開闢空間之前,編譯器首先檢查預開闢空間的首地址相對於結構體首地址的偏移是否是本成員的整數倍,若是,則存放本成員,反之,則在本成員和上一個成員之間填充一定的位元組,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個位元組。 
3)結構體的總大小為結構體最寬基本型別成員大小的整數倍,如有需要,編譯器會在最末一個成員之後加上填充位元組(trailing padding)。 
備註: 
a、結構體總大小是包括填充位元組,最後一個成員滿足上面兩條以外,還必須滿足第三條,否則就必須在最後填充幾個位元組以達到本條要求。 
b、如果結構體記憶體在長度大於處理器位數的元素,那麼就以處理器的倍數為對齊單位;否則,如果結構體內的元素的長度都小於處理器的倍數的時候,便以結構體裡面最長的資料元素為對齊單位。 
4) 結構體內型別相同的連續元素將在連續的空間內,和陣列一樣。 
驗證試驗 
我們通過一系列例子的詳細說明來證明這個規則吧! 
我試驗用的編譯器包括GCC 3.4.2和VC6.0的C編譯器,平臺為Windows XP + Sp2。 
我們將用典型的struct對齊來說明。首先我們定義一個struct:

pragma pack(n) /* n = 1, 2, 4, 8, 16 */

struct test_t { 
int a; 
char b; 
short c; 
char d[6]; 
};

pragma pack(n)

首先我們首先確認在試驗平臺上的各個型別的size,經驗證兩個編譯器的輸出均為: 
sizeof(char) = 1 
sizeof(short) = 2 
sizeof(int) = 4 
我們的試驗過程如下:通過#pragma pack(n)改變“對齊係數”,然後察看sizeof(struct test_t)的值。 
1、1位元組對齊(#pragma pack(1)) 
輸出結果:sizeof(struct test_t) = 13[兩個編譯器輸出一致] 
分析過程: 
1) 成員資料對齊

pragma pack(1)

struct test_t { 
int a; /* int型,長度4 > 1 按1對齊;起始offset=0 0%1=0;存放位置區間[0,3] */ 
char b; /* char型,長度1 = 1 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */ 
short c; /* short型,長度2 > 1 按1對齊;起始offset=5 5%1=0;存放位置區間[5,6] */ 
char d[6]; /* char型,長度1 = 1 按1對齊;起始offset=7 7%1=0;存放位置區間[7,C] */ 
};/char d[6]要看成6個char型變數/

pragma pack()

成員總大小=13 
2) 整體對齊 
整體對齊係數 = min((max(int,short,char), 1) = 1 
整體大小(size)=()(整體對齊係數) 圓整 = 13 /13%1=0/ [注1] 
2、2位元組對齊(#pragma pack(2)) 
輸出結果:sizeof(struct test_t) = 14 [兩個編譯器輸出一致] 
分析過程: 
1) 成員資料對齊

pragma pack(2)

struct test_t { 
int a; /* int型,長度4 > 2 按2對齊;起始offset=0 0%2=0;存放位置區間[0,3] */ 
char b; /* char型,長度1 < 2 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */ 
short c; /* short型,長度2 = 2 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */ 
char d[6]; /* char型,長度1 < 2 按1對齊;起始offset=8 8%1=0;存放位置區間[8,D] */ 
};

pragma pack()

成員總大小=14 
2) 整體對齊 
整體對齊係數 = min((max(int,short,char), 2) = 2 
整體大小(size)=()(整體對齊係數) 圓整 = 14 /* 14%2=0 */ 
3、4位元組對齊(#pragma pack(4)) 
輸出結果:sizeof(struct test_t) = 16 [兩個編譯器輸出一致] 
分析過程: 
1) 成員資料對齊

pragma pack(4)

struct test_t { 
int a; /* int型,長度4 = 4 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */ 
char b; /* char型,長度1 < 4 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */ 
short c; /short型, 長度2 < 4 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] 
char d[6]; /* char型,長度1 < 4 按1對齊;起始offset=8 8%1=0;存放位置區間[8,D] */ 
};

pragma pack()

成員總大小=14 
2) 整體對齊 
整體對齊係數 = min((max(int,short,char), 4) = 4 
整體大小(size)=()(整體對齊係數) 圓整 = 16 /16%4=0
4、8位元組對齊(#pragma pack(8)) 
輸出結果:sizeof(struct test_t) = 16 [兩個編譯器輸出一致] 
分析過程: 
1) 成員資料對齊

pragma pack(8)

struct test_t { 
int a; /* int型,長度4 < 8 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */ 
char b; /* char型,長度1 < 8 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */ 
short c; /* short型,長度2 < 8 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */ 
char d[6]; /* char型,長度1 < 8 按1對齊;起始offset=8 8%1=0;存放位置區間[8,D] */ 
};

pragma pack()

成員總大小=14 
2) 整體對齊 
整體對齊係數 = min((max(int,short,char), 8) = 4 
整體大小(size)=()(整體對齊係數) 圓整 = 16 /16%4=0
5、16位元組對齊(#pragma pack(16)) 
輸出結果:sizeof(struct test_t) = 16 [兩個編譯器輸出一致] 
分析過程: 
1) 成員資料對齊

pragma pack(16)

struct test_t { 
int a; /* int型,長度4 < 16 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */ 
char b; /* char型,長度1 < 16 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */ 
short c; /* short型,長度2 < 16 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */ 
char d[6]; /* char型,長度1 < 16 按1對齊;起始offset=8 8%1=0;存放位置區間[8,D] */ 
};

pragma pack()

成員總大小=14 
2) 整體對齊 
整體對齊係數 = min((max(int,short,char), 16) = 4 
整體大小(size)=()(整體對齊係數) 圓整 = 16 /16%4=0
基本結論 
8位元組和16位元組對齊試驗證明了“規則”的第3點:“當#pragma pack的n值等於或超過所有資料成員長度的時候,這個n值的大小將不產生任何效果”。另外記憶體對齊是個很複雜的東西,讀者不妨把上述結構體中加個double型成員進去練習一下,上面所說的在有些時候也可能不正確。呵呵^_^ 
[注1] 
什麼是“圓整”? 
舉例說明:如上面的8位元組對齊中的“整體對齊”,整體大小=9 按 4 圓整 = 12 
圓整的過程:從9開始每次加一,看是否能被4整除,這裡9,10,11均不能被4整除,到12時可以,則圓整結束。 
上面文字表述太不直觀了,鄙人給段程式碼直觀的體現出來,程式碼如下:

pragma pack(4) /* n = 1, 2, 4, 8, 16 */

  struct test_t{ 
  int a; 
  char b; 
  short c; 
  char d[6]; 
  }ttt; 
  void print_hex_data(char *info, char *data, int len) 
  { 
  int i; 
  dbg_printf(“%s:\n\r”, info); 
  for(i = 0; i < len; i++){ 
  dbg_printf(“%02x “, (unsigned char)data[i]); 
  if (0 == ((i+1) % 32)) 
  dbg_printf(“\n”); 
  } 
  dbg_printf(“\n\r”); 
  } 
  int main() 
  { 
  ttt.a = 0x1a2a3a4a; 
  ttt.b = 0x1b; 
  ttt.c = 0x1c2c; 
  char *s = “123456”; 
  memcpy(ttt.d, s, 6); 
  print_hex_data(“struct_data”, (char *)&ttt, sizeof(struct test_t)); 
  return 0; 
  }

pragma pack(1)的結果:

4a 3a 2a 1a 1b 2c 1c 31 32 33 34 35 36

pragma pack(2)的結果:

4a 3a 2a 1a 1b 00 2c 1c 31 32 33 34 35 36

pragma pack(4)的結果:

4a 3a 2a 1a 1b 00 2c 1c 31 32 33 34 35 36 00 00

pragma pack(8)的結果:

4a 3a 2a 1a 1b 00 2c 1c 31 32 33 34 35 36 00 00

pragma pack(16)的結果:

4a 3a 2a 1a 1b 00 2c 1c 31 32 33 34 35 36 00 00

StructLayout特性     
     公共語言執行庫利用StructLayoutAttribute控制類或結構的資料欄位在託管記憶體中的物理佈局,即類或結構需要按某種方式排列。如果要將類傳遞給需要指定佈局的非託管程式碼,則顯式控制類佈局是重要的。它的建構函式中用 LayoutKind值初始化 StructLayoutAttribute 類的新例項。 LayoutKind.Sequential 用於強制將成員按其出現的順序進行順序佈局。
 
  StructLayout特性允許我們控制Structure語句塊的元素在記憶體中的排列方式,以及當這些元素被傳遞給外部DLL時,執行庫排列這些元素的方式。Visual   Basic結構的成員在記憶體中的順序是按照它們出現在原始碼中的順序排列的,儘管編譯器可以自由的插入填充位元組來安排這些成員,以便使得16位數值用子邊界對齊,32位數值用雙字邊界對齊。    
    
  使用這種排列(未壓縮佈局)提供的效能最佳。     
        
  在Visual   Basic   6的使用者自定義結構是未壓縮的,而且我們不可以改變這一預設設定。在VB.NET中可以改變這種設定,並且可以通過System.Runtime.InteropServices.StructLayout   特性精確的控制每一個結構成員的位置。
System.Runtime.InteropServices.StructLayout   允許的值有StructLayout.Auto   StructLayout.Sequential   StructLayout.Explicit.     
1.Sequential,順序佈局,比如
struct S1
{
  int a;
  int b;
}
那麼預設情況下在記憶體裡是先排a,再排b
也就是如果能取到a的地址,和b的地址,則相差一個int型別的長度,4位元組
[StructLayout(LayoutKind.Sequential)] 
struct S1
{
  int a;
  int b;
}
這樣和上一個是一樣的.因為預設的記憶體排列就是Sequential,也就是按成員的先後順序排列.
2.Explicit,精確佈局
需要用FieldOffset()設定每個成員的位置
這樣就可以實現類似c的公用體的功能
[StructLayout(LayoutKind.Explicit)] 
struct S1
{
  [FieldOffset(0)]
  int a;
  [FieldOffset(0)]
  int b;
}
這樣a和b在記憶體中地址相同 
    
  StructLayout特性支援三種附加欄位:CharSet、Pack、Size。     
·   CharSet定義在結構中的字串成員在結構被傳給DLL時的排列方式。可以是Unicode、Ansi或Auto。     
  預設為Auto,在WIN   NT/2000/XP中表示字串按照Unicode字串進行排列,在WIN   95/98/Me中則表示按照ANSI字串進行排列。     
·   Pack定義了結構的封裝大小。可以是1、2、4、8、16、32、64、128或特殊值0。特殊值0表示當前操作平臺預設的壓縮大小。     
 

  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct LIST_OPEN
    {
        public int dwServerId;
        public int dwListId;
        public System.UInt16 wRecordSize;
        public System.UInt16 wDummy;
        public int dwFileSize;
        public int dwTotalRecs;
        public NS_PREFETCHLIST sPrefetch;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 24)]
        public string szSrcMach;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 24)]
        public string szSrcComp;
    }

此例中用到MashalAs特性,它用於描述欄位、方法或引數的封送處理格式。用它作為引數字首並指定目標需要的資料型別。
例如,以下程式碼將兩個引數作為資料型別長指標封送給 Windows API 函式的字串 (LPStr): 
[MarshalAs(UnmanagedType.LPStr)] 
String existingfile; 
[MarshalAs(UnmanagedType.LPStr)] 
String newfile; 
注意結構作為引數時候,一般前面要加上ref修飾符,否則會出現錯誤:物件的引用沒有指定物件的例項。
[ DllImport( "kernel32", EntryPoint="GetVersionEx" )] 
public static extern bool GetVersionEx2( ref OSVersionInfo2 osvi );

1. 使用場景

公共語言執行時控制資料欄位的類或結構在託管記憶體中的物理佈局。但是,如果想要將型別傳遞到非託管程式碼,需要使用 StructLayout 屬性。

2. 記憶體分配問題。

如果不顯示的設定記憶體對齊方式(通過StructLayout.Pack屬性決定), C#預設是以4個位元組(byte)為單位,會出現“多分配”記憶體的情況。 例如:

Class Example
{
   public byte b1;
   public char c2;
   public int i3;
}

預設情況下(StructLayout.Pack = 4),Framework編譯器會為example物件分配8個位元組(欄位c2後面會補齊2個byte )。每個成員的索引和大小結果為:

     Size: 8

     b1 Offset: 0, lenght =1, 

     c2 Offset: 1, length = 1,

     i3 offset: 4, length = 4

C++ 編譯器的分配方式則為:

  Size: 6

     b1 Offset: 0, lenght =1, 

     c2 Offset: 1, length = 1,

     i3 offset: 2, length = 4

由於記憶體分配的大小不一致,導致在傳遞物件marshal的時候回出現問題!!

3. 解決方案。

3.1 通過設定StructLayout.Pack的值來達到記憶體大小分配一致。

例如在上面的例子中,設定StructLayout.Pack =2 或者 StructLayout.Pack =1. 但是這種方法可能會因為硬體約束導致效能或者其他問題。

3.2 通過預留欄位來“補齊”記憶體分配。

這種做法在實際專案中使用較多,既保證了長度一致,也為以後擴充套件提供了一種容錯的可能。 如果採取這種方式,重新定義如下:

Class Example
{
   public byte b1;
   public char c2;
   public int i3;
   
   [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
   public byte[] reserved;
}

至此,C#和C++分配的記憶體大小同為8,問題解決  :)  

C#-StructLayoutAttribute(結構體佈局)

[摘要:struct真例欄位的記憶體結構(Layout)戰鉅細(Size) 正在C/C&#43;&#43;中,struct範例中的成員的一旦宣告,則真例中成員正在記憶體中的結構(Layout)挨次便定下去了,即取成員宣告的挨次相] 

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成員之外,還有兩個成員AutoExplicit,給StructLayoutAttribute傳入LayoutKind.Auto可以讓CLR按照自己選擇的最優方式來排列例項中的欄位;傳入LayoutKind.Explicit可以使欄位按照我們的在欄位上設定的FieldOffset來更靈活的設定欄位排序方式,但這種方式也挺危險的,如果設定錯誤後果將會比較嚴重。下面就看幾個示例,算下四個struct各佔多少Byte?

1.[StructLayout(LayoutKind.Sequential)]

struct StructDeft //C#編譯器會自動在上面運用[StructLayout(LayoutKind.Sequential)]{
    
bool i;  //1Bytedouble c;//8bytebool 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)]

[StructLayout(LayoutKind.Explicit)]
struct BadStruct
{
    [FieldOffset(
0)]
    
publicbool i;  //1Byte    [FieldOffset(0)]
    
publicdouble c;//8byte    [FieldOffset(0)]
    
publicbool b;  //1byte}

        sizeof(BadStruct)得到的結果是9byte,顯然得出的基數9顯示CLR並沒對結構體進行任何記憶體對齊(Align);本身要佔有10byte的資料卻只佔了9byte,顯然有些資料被丟失了,這也正是我給struct取BadStruct作為名字的原因。如果在struct上運用了[StructLayout(LayoutKind.Explicit)],計算FieldOffset一定要小心,例如我們使用上面BadStruct來進行下面的測試

StructExpt e =new StructExpt();
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的三個欄位是如何擺放的:

unsafe{
      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

struct EmptyStruct{}

    無論運用上面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)。