預處理指令#pragma pack詳解

預處理指令#pragma pack詳解

#pragma pack的大致作用即爲改變編譯器的對齊方式,先從指令和定義上來分析其功能。
部分內容參考http://www.cnblogs.com/King-Gentleman/p/5297355.html 以及MSDN。

簡單理解#pragma

作爲較爲複雜的預處理指令之一,它的作用爲更改編譯器的編譯狀態以及爲特定的編譯器提供特定的編譯指示,這些指示是具體針對某一種(或某一些)編譯器的,其他編譯器可能不知道該指示的含義又或者對該指示有不同的理解,也即是說,#pragma的實現是與具體平臺相關的。可以簡單將其理解爲該預處理指令是開發者和編譯器交互的一個工具。

#pragma pack指令說明

由於內存的讀取時間遠遠小於CPU的存儲速度,這裏用設定數據結構的對齊係數,即犧牲空間來換取時間的思想來提高CPU的存儲效率。

這裏先說編譯器的對齊配置。以vc6爲例,vc6中的編譯選項有 /Zp[1|2|4|8|16] ,/Zp1表示以1字節邊界對齊,相應的,/Zpn表示以n字節邊界對齊。n字節邊界對齊的意思是說,一個成員的地址必須安排在成員的尺寸的整數倍地址上或者是n的整數倍地址上,取它們中的最小值。也就是:
min ( sizeof ( member ), n)
實際上,1字節邊界對齊也就表示了結構成員之間沒有空洞。
/Zpn選項是應用於整個工程的,影響所有的參與編譯的結構。
要使用這個選項,可以在vc6中打開工程屬性頁,c/c++頁,選擇Code Generation分類,在Struct member alignment可以選擇。
而如果要專門針對某些結構定義使用對齊選項,可以使用#pragma pack編譯指令。指令語法如下: #pragma pack( [ show ] | [ push | pop ] [, identifier ] , n )

指令用法說明:

  1. pack提供數據聲明級別的控制,對定義不起作用;
  2. 調用pack時不指定參數,n將被設成默認值;
  3. 一旦改變數據類型的alignment,直接效果就是佔用memory的減少,但是performance會下降。

語法具體分析:

  1. show:可選參數;顯示當前packing aligment的字節數,以warning message的形式被顯示;
  2. push:可選參數;將當前指定的packing alignment數值進行壓棧操作,這裏的棧是the internal compiler stack,同時設置當前的packing alignment爲n;如果n沒有指定,則將當前的packing alignment數值壓棧;
  3. pop:可選參數;從internal compiler stack中刪除最頂端的record;如果沒有指定n,則當前棧頂record即爲新的packing alignment數值;如果指定了n,則n將成爲新的packing aligment數值;如果指定了identifier,則internal compiler stack中的record都將被pop直到identifier被找到,然後pop出identitier,同時設置packing alignment數值爲當前棧頂的record;如果指定的identifier並不存在於internal compiler stack,則pop操作被忽略;
  4. identifier:可選參數;當同push一起使用時,賦予當前被壓入棧中的record一個名稱;當同pop一起使用時,從internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier沒有被找到,則忽略pop操作;
  5. n:可選參數;指定packing的數值,以字節爲單位;缺省數值是8,合法的數值分別是1、2、4、8、16。

先以#pragma pack(n)的使用舉例,該條指令功能與意義和/Zpn選項相同。#pragma pack(4)表示後續結構單元的對齊係數爲4(具體規則見下面說明),#pragma pack()表示更改當前對齊係數爲默認值(未修改時,工程對齊係數默認爲8)。

#pragma pack(4)
    struct test{
        char m1;
        short int m2;
        int m3;
    };
#pragma pack()

    test t;

    printf("%d\n", sizeof(t));

上述代碼段執行結果爲8,在初識時可能會誤作是12,爲8的原因在於“結構體中的數據成員,除了第一個是始終放在最開始的地方,其它數據成員的地址必須是它本身大小或對齊參數兩者中較小的一個的倍數。”

數據對齊規則

經過上面這個例子,我們再來看對齊的規則。

  1. 複雜類型中各個成員按照它們被聲明的順序在內存中順序存儲,第一個成員的地址和整個類型的地址相同;
  2. 每個成員分別對齊,即每個成員按自己的方式對齊,並最小化長度;規則就是每個成員按其類型的對齊參數(通常是這個類型的大小)和指定對齊參數中較小的一個對齊;
  3. 結構體、聯合體或者類的數據成員,第一個放在偏移爲0的地方;以後每個數據成員的對齊,按照#pragma pack指定的數值和這個數據成員自身長度兩個中比較小的那個進行;也就是說,當#pragma pack指定的值等於或者超過所有數據成員長度的時候,這個指定值的大小將不產生任何效果;
  4. 複雜類型(如結構體)整體的對齊是按照結構體中長度最大的數據成員和#pragma pack指定值之間較小的那個值進行;這樣當數據成員爲複雜類型(如結構體)時,可以最小化長度;
  5. 複雜類型(如結構體)整體長度的計算必須取所用過的所有對齊參數的整數倍,不夠補空字節;也就是取所用過的所有對齊參數中最大的那個值的整數倍,因爲對齊參數都是2的n次方;這樣在處理數組時可以保證每一項都邊界對齊。

另外,在相同的對齊方式下,結構體內部數據定義的順序不同,結構體整體佔據內存空間也不同。舉例如下:

struct A {
int a;            // a的自身對齊值爲4,偏移地址爲0x00~0x03,a的起始地址0x00滿足0x00%4=0;
char b;           // b的自身對齊值爲1,由於緊跟a之後的地址,即0x04滿足0x04%1=0,所以b存放在0x04地址空間
short c;          // c的自身對齊值爲2,由於緊跟b之後的地址0x05%2不是0,而0x06%2=0,因此c的存放起始地址爲0x06,存放在0x06~0x07空間。
                  // 在b和c之間的0x05地址 則補空字節。
};

結構體A中包含了4字節長度的int一個,1字節長度的char一個和2字節長度的short型數據一個。所以A用到的空間應該是7字節。但是因爲編譯器要對數據成員在空間上進行對齊。而由於結構體自身對齊值取數據成員中自身對齊值的最大值,即4,並且0x00~0x07的8字節空間滿足8%4=0,所以sizeof(strcut A)值爲8。
現在把該結構體調整成員變量的順序。

struct B {
char b;           // b的自身對齊值爲1,其起始地址爲0x00,由於滿足0x00%1=0,所以b存放在0x00地址空間
int a;            // a的自身對齊值爲4,由於緊跟b之後的地址0x01%4不是0,而0x04%4=0,因此c的存放起始地址爲0x04,存放在0x04~0x07空間。
                  //  在b和a之間的0x01~0x03地址則補3個空字節。
short c;          // c的自身對齊值爲2,由於緊跟a之後的地址0x08%2=0,因此c的存放起始地址爲0x08,存放在0x08~0x09空間。
};

這時候同樣是總共7個字節的變量,但是由於結構體自身對齊值取數據成員中自身對齊值的最大值,即4,並且0x00~0x09的10字節空間不滿足10%4=0,而12%4=0,所以sizeof(struct B)的值卻是12,即在緊跟c之後的0x0A~0x0B地址還需補兩個空字節,使得整個結構體佔用的字節空間爲12個字節。

現在我們使用預處理指令來指定結構體的對齊係數:

#pragma pack (2) /*指定按2字節對齊,等價於#pragma pack(push,2)*/
struct C {
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊,等價於#pragma pack(pop)*/

由上述規則4可知,結構體的對齊是按照結構體中長度最大的數據成員和#pragma pack指定值之間較小的那個值進行的,所以這裏該值爲2,sizeof(struct C)值是8。

這些例子引出了幾個具體的概念如下:

  1. 數據類型自身的對齊值:就是上面交代的基本數據類型的自身對齊值。
  2. 指定對齊值:#pragma pack (value)時的指定對齊值value。
  3. 結構體或者類的自身對齊值:其數據成員中自身對齊值最大的那個值。
  4. 數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中小的那個值。

其他參數使用舉例

#pragma pack(push, 1)  // 將對齊係數1壓入internal compiler stack,同時設置當前的packing alignment爲1
#pragma pack(push, 4)  // 將4也壓入棧,同時設置當前的packing alignment爲4
#pragma pack(show)    // 顯示當前packing alignment的字節數
#pragma pack(pop)     // 彈出internal compiler stack頂端的一條記錄,這裏彈出了4
#pragma pack(show)   // 此時頂端的packing alignment值爲1,
    struct test{
        char m1;
        short m2; 
        int m3;
    };
#pragma pack()  // 更改爲缺省值8 
#pragma pack(show)

在VS2013下的編譯結果如下圖所示。
編譯結果

另外值得一提的是,在ARM平臺的編譯器中,沒有提供如“#pragma pack”這樣帶參數的對齊指令,只有一個關鍵字 __packed。
__packed 限定符將所有有效類型的對齊邊界設置爲 1,如果一個結構沒有這個限定符,默認向表數能力最強的那個數據類型對齊。

typedef __packed struct 
{
    double dValue1;
    char   u8Value2;
    int    u32Value3;
} ASampleStructor;

上例中,size值爲13,說明1字節對齊後,該結構總長爲13字節。去掉__packed對齊後,爲16字節。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章