#pragma pack(n)------內存對齊問題

在C語言中,結構是一種複合數據類型,其構成元素既可以是基本數據類型(如int、long、float等)的變量,也可以是一些複合數據類型(如數組、結構、聯合等)的數據單元。在結構中,編譯器爲結構的每個成員按其自然對界(alignment)條件分配空間。各個成員按照它們被聲明的順序在內存中順序存儲,第一個成員的地址和整個結構的地址相同

 

爲了能使CPU對變量進行高效快速的訪問,變量的起始地址應該具有某些特性,即所謂的“對齊”。例如對於4字節的int類型變量,其起始地址應位於4字節邊界上,即起始地址能夠被4整除。變量的對齊規則如下(32位系統):


  Type                                                Alignment

 

  char                                        在字節邊界上對齊
  short (16-bit)                          雙字節邊界上對齊     2
  int and long (32-bit)              4字節邊界上對齊
  float                                        4字節邊界上對齊
  double                                    8字節邊界上對齊
      
 structures
      單獨考慮結構體的個成員,它們在不同的字節邊界上對齊。  其中最大的字節邊界數就是該結構的字節邊界數(LINUX下面不是這樣的,下面我會解釋)

      如果結構體中有結構體成員,那麼這是一個遞歸的過程。
      設編譯器設定的最大對齊字節邊界數爲n,對於結構體中的某一成員item,它相對於結構首地址的實際字節對齊數
      目X應該滿足以下規則:
      X = min(n, sizeof(item))   // 也就是說雖然可以手動規定程序的字節對齊大小,但是還是和程序默認的取最小值

 

例如,對於結構體
      struct {
      char a;
      long b;
      } T;


      當位於32位系統,n=8時:
      a的偏移爲0,
      b的偏移爲4,中間填充了3個字節, b的X爲4;


      當位於32位系統,n=2時:
      a的偏移爲0,
      b的偏移爲2,中間填充了1個字節,b的X爲2;


      結構體的sizeof:
      設結構體的最後一個成員爲LastItem,其相對於結構體首地址的偏移爲offset(LastItem),其大小爲
      sizeof(LastItem),結構體的字節對齊數爲N,則:結構體的sizeof 爲: 若offset(LastItem)+
      sizeof(LastItem)能夠被N整除,那麼就是offset(LastItem)+
      sizeof(LastItem),否則,在後面填充,直到能夠被N整除。


      另外:
      1) 對於空結構體,sizeof == 1;因爲必須保證結構體的每一個實例在內存中都有獨一無二的地址.
      2)結構體的靜態成員不對結構體的大小產生影響,因爲靜態變量的存儲位置與結構體的實例地址無關。例如:
      struct {static int I;} T; struct {char a; static int I;} T1;
      sizeof(T) == 1; sizeof(T1) == 1;
      下面是CSDN上提出的問題(原文<>:http://community.csdn.net/Expert/TopicView3.asp?id=3804035)
      ---------------------------------------

 

 

#pragma pack(8)

      struct s1{
      short a;
      long b;
      };

      struct s2{
      char c;
      s1 d;
      long long e;
      };

      #pragma pack()

      問
      1.sizeof(s2) = ?
      2.s2的c後面空了幾個字節接着是d?
      ---------------------------------------


      sizeof(S2)結果爲24    //  linux下是20,這個就是和一般編程工具的區別,在linux下,它的對齊規則是上一個如果小於相鄰的下一個,就按下一個的大小對齊,而不是按所有大小的裏面最大的!!!
      成員對齊有一個重要的條件,即每個成員分別對齊.即每個成員按自己的方式對齊.
      也就是說上面雖然指定了按8字節對齊,但並不是所有的成員都是以8字節對齊.其對齊的規則是,每個成員按其類型的對齊參數(通常是這個類型的大小)和指定對齊參數(這裏是8字節)中較小的一個對齊.並且結構的長度必須爲所用過的所有對齊參數的整數倍,不夠就補空字節.
      S1中,成員a是1字節默認按1字節對齊,指定對齊參數爲8,這兩個值中取1,a按1字節對齊;成員b是4個字節,默認是按4字節對齊,這時就按4字節對齊,所以sizeof(S1)應該爲8;
      S2 中,c和S1中的a一樣,按1字節對齊,而d
      是個結構,它是8個字節,它按什麼對齊呢?對於結構來說,它的默認對齊方式就是它的所有成員使用的對齊參數中最大的一個,S1的就是4.所以,成員d就是按4字節對齊.成員e是8個字節,它是默認按8字節對齊,和指定的一樣,所以它對到8字節的邊界上,這時,已經使用了12個字節了,所以又添加了4個字節的空,從第16個字節開始放置成員e.這時,長度爲24,已經可以被8(成員e按8字節對齊)整除.這樣,一共使用了24個字節.
      a b
      S1的內存佈局:11**,1111,
      c S1.a S1.b d
      S2的內存佈局:1***,11**,1111,****11111111

      這裏有三點很重要:
      1.每個成員分別按自己的方式對齊,並能最小化長度
      2.複雜類型(如結構)的默認對齊方式是它最長的成員的對齊方式,這樣在成員是複雜類型時,可以最小化長度
      3.對齊後的長度必須是成員中最大的對齊參數的整數倍,這樣在處理數組時可以保證每一項都邊界對齊

  

  

對於數組,比如:
      char a[3];這種,它的對齊方式和分別寫3個char是一樣的.也就是說它還是按1個字節對齊.
      如果寫: typedef char Array3[3];
      Array3這種類型的對齊方式還是按1個字節對齊,而不是按它的長度.
      不論類型是什麼,對齊的邊界一定是1,2,4,8,16,32,64....中的一個.

 

 

 

 一下是更多關於對齊的內容:

 如下一段代碼:
      #pragma pack(4)
      class TestB
      {
      public:
      int aa;
      char a;
      short b;
      char c;
      };
      int nSize = sizeof(TestB);
      這裏nSize結果爲12,在預料之中。
        現在去掉第一個成員變量爲如下代碼:
      #pragma pack(4)
      class TestC
      {
      public:
      char a;
      short b;
      char c;
      };
      int nSize = sizeof(TestC);
      按照正常的填充方式nSize的結果應該是8,爲什麼結果顯示nSize爲6呢?
      事實上,很多人對#pragma pack的理解是錯誤的。
      #pragma pack規定的對齊長度,實際使用的規則是:
      結構,聯合,或者類的數據成員,第一個放在偏移爲0的地方,以後每個數據成員的對齊,按照#pragma
      pack指定的數值和這個數據成員自身長度中,比較小的那個進行。
      也就是說,當#pragma pack的值等於或超過所有數據成員長度的時候,這個值的大小將不產生任何效果。
      而結構整體的對齊,則按照結構體中最大的數據成員和 #pragma pack指定值 之間,較小的那個進行。

      具體解釋


      #pragma pack(4)   //四個就換行
      class TestB
      {
      public:
      int aa; //第一個成員,放在[0,3]偏移的位置,
      char a; //第二個成員,自身長爲1,#pragma pack(4),取小值,也就是1,所以這個成員按一字節對齊,放在偏移[4]的位置。
      short b; //第三個成員,自身長2,#pragma pack(4),取2,按2字節對齊,所以放在偏移[6,7]的位置。
      char c; //第四個,自身長爲1,放在[8]的位置。
      };
      這個類實際佔據的內存空間是9字節
      類之間的對齊,是按照類內部最大的成員的長度,和#pragma pack規定的值之中較小的一個對齊的。
      所以這個例子中,類之間對齊的長度是min(sizeof(int),4),也就是4。
      9按照4字節圓整的結果是12,所以sizeof(TestB)是12。 4+

 

      如果
      #pragma pack(2)
      class TestB
      {
      public:
      int aa; //第一個成員,放在[0,3]偏移的位置,
      char a; //第二個成員,自身長爲1,#pragma pack(4),取小值,也就是1,所以這個成員按一字節對齊,放在偏移[4]的位置。
      short b; //第三個成員,自身長2,#pragma pack(4),取2,按2字節對齊,所以放在偏移[6,7]的位置。
      char c; //第四個,自身長爲1,放在[8]的位置。
      };
      //可以看出,上面的位置完全沒有變化,只是類之間改爲按2字節對齊,9按2圓整的結果是10。
      //所以 sizeof(TestB)是10。   4+2+2

 


      最後看原貼:
      現在去掉第一個成員變量爲如下代碼:
      #pragma pack(4)
      class TestC
      {
      public:
      char a;//第一個成員,放在[0]偏移的位置,
      short b;//第二個成員,自身長2,#pragma pack(4),取2,按2字節對齊,所以放在偏移[2,3]的位置。
      char c;//第三個,自身長爲1,放在[4]的位置。
      };
      //整個類的大小是5字節,按照min(sizeof(short),4)字節對齊,也就是2字節對齊,結果是6
      //所以sizeof(TestC)是6。

      感謝 Michael 提出疑問,在此補充:
      #pragma pack


      當數據定義中出現__declspec( align()
      )時,指定類型的對齊長度還要用自身長度和這裏指定的數值比較,然後取其中較大的。最終類/結構的對齊長度也需要和這個數值比較,然後取其中較大的。

      可以這樣理解, __declspec( align() ) 和 #pragma
      pack是一對兄弟,前者規定了對齊的最小值,後者規定了對齊的最大值,兩者同時出現時,前者擁有更高的優先級。
      __declspec ( align()
      )的一個特點是,它僅僅規定了數據對齊的位置,而沒有規定數據實際佔用的內存長度,當指定的數據被放置在確定的位置之後,其後的數據填充仍然是按照#pragma
      pack規定的方式填充的,這時候類/結構的實際大小和內存格局的規則是這樣的:
      在__declspec( align () )之前,數據按照#pragma pack規定的方式填充,如前所述。當遇到__declspec(
      align() )的時候,首先尋找距離當前偏移向後最近的對齊點(滿足對齊長度爲 max(數據自身長度,指定值)
      ),然後把被指定的數據類型從這個點開始填充,其後的數據類型從它的後面開始,仍然按照#pragma pack填充,直到遇到下一個__declspec(
      align() )。
      當所有數據填充完畢,把結構的整體對齊數值和__declspec( align() )規定的值做比較,取其中較大的作爲整個結構的對齊長度。
      特別的,當__declspec( align() )指定的數值比對應類型長度小的時候,這個指定不起作用。

 

 

       首先請大家先看下面代碼:
          typedef struct
          {
            UINT32  NumElements;
            union
            {
               UINT32  ObjectHandle;
             }Entry;
           }STR_ARRAY, *PSTR_ARRAY;
          還有這兩句#pragma pack(push, 1)
          #pragma pack(pop)
          #pragma  pack( [ n ] )
          該指令指定結構和聯合成員的緊湊對齊。而一個完整的轉換單元的結構和聯合的緊湊對齊由/ Z p 選項設置。緊湊對齊用p a c e
      編譯指示在數據說明層設置。該編譯指示在其出現後的第一個結構或聯合說明處生效。該編譯指示對定義無效。當你使用#pragma  pack ( n )
      時, 這裏n 爲1 、2 、4 、8 或1 6 。第一個結構成員之後的每個結構成員都被存儲在更小的成員類型或n
      字節界限內。如果你使用無參量的#pragma  pack , 結構成員被緊湊爲以/ Z p 指定的值。該缺省/ Z p 緊湊值爲/ Z p 8 。
          編譯器也支持以下增強型語法:
          #pragma  pack( [ [ { p u s h | p o p } , ] [ 標識符, ] ] [ n] )若不同的組件使用p
      a c k 編譯指示指定不同的緊湊對齊, 這個語法允許你把程序組件組合爲一個單獨的轉換單元。帶p u s h 參量的p a c k
      編譯指示的每次出現將當前的緊湊對齊存儲到一個內部編譯器堆棧中。編譯指示的參量表從左到右讀取。如果你使用p u s h , 則當前緊湊值被存儲起來;
      如果你給出一個n 的值, 該值將成爲新的緊湊值。若你指定一個
      標識符, 即你選定一個名稱, 則該標識符將和這個新的的緊湊值聯繫起來。帶一個p o p 參量的p a c k
      編譯指示的每次出現都會檢索內部編譯器堆棧頂的值,並且使該值爲新的緊湊對齊值。如果你使用p o p
      參量且內部編譯器堆棧是空的,則緊湊值爲命令行給定的值, 並且將產生一個警告信息。若你使用p o p 且指定一
      個n 的值, 該值將成爲新的緊湊值。若你使用p o p 且指定一個標識符, 所有存儲在堆棧中的值將從棧中刪除, 直到找到一個匹配的標識符,
      這個與標識符相關的緊湊值也從棧中移出, 並且這個僅在標識符入棧之前存在的緊湊值成爲新的緊湊值。如果未找到匹配的標識符, 將使用命令行設置的緊湊值,
      並且將產生一個一級警告。缺省緊湊對齊爲8 。p a c k 編譯指示的新的增強功能讓你編寫頭文件, 確保在遇到該頭文件的前後的緊湊值是一樣的。
          什麼是內存對齊
          考慮下面的結構:
               struct foo
               {
                 char c1;
                 short s;
                 char c2;
                 int i;
                };
         
          假設這個結構的成員在內存中是緊湊排列的,假設c1的地址是0,那麼s的地址就應該是1,c2的地址就是3,i的地址就是4。也就是
          c1 00000000, s 00000001, c2 00000003, i 00000004。
          可是,我們在Visual c/c++ 6中寫一個簡單的程序:
               struct foo a;
          printf("c1 %p, s %p, c2 %p, i %p/n",
              (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
          運行,輸出:
               c1 00000000, s 00000002, c2 00000004, i 00000008。
          爲什麼會這樣?這就是內存對齊而導致的問題。
      爲什麼會有內存對齊
          以下內容節選自《Intel Architecture 32 Manual》。
         
字,雙字,和四字在自然邊界上不需要在內存中對齊。(對字,雙字,和四字來說,自然邊界分別是偶數地址,可以被4整除的地址,和可以被8整除的地址。)
         
      無論如何,爲了提高程序的性能,數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;然而,對齊的內存訪問僅需要一次訪問。
         
      一個字或雙字操作數跨越了4字節邊界,或者一個四字操作數跨越了8字節邊界,被認爲是未對齊的,從而需要兩次總線週期來訪問內存。一個字起始地址是奇數但卻沒有跨越字邊界被認爲是對齊的,能夠在一個總線週期中被訪問。
         
      某些操作雙四字的指令需要內存操作數在自然邊界上對齊。如果操作數沒有對齊,這些指令將會產生一個通用保護異常(#GP)。雙四字的自然邊界是能夠被16
      整除的地址。其他的操作雙四字的指令允許未對齊的訪問(不會產生通用保護異常),然而,需要額外的內存總線週期來訪問內存中未對齊的數據。
      編譯器對內存對齊的處理
          缺省情況下,c/c++編譯器默認將結構、棧中的成員數據進行內存對齊。因此,上面的程序輸出就變成了:
      c1 00000000, s 00000002, c2 00000004, i 00000008。
      編譯器將未對齊的成員向後移,將每一個都成員對齊到自然邊界上,從而也導致了整個結構的尺寸變大。儘管會犧牲一點空間(成員之間有空洞),但提高了性能。
      也正是這個原因,我們不可以斷言sizeof(foo) == 8。在這個例子中,sizeof(foo) == 12。
      如何避免內存對齊的影響
          那麼,能不能既達到提高性能的目的,又能節約一點空間呢?有一點小技巧可以使用。比如我們可以將上面的結構改成:
          struct bar
          {
              char c1;
              char c2;
              short s;
              int i;
          };
          這樣一來,每個成員都對齊在其自然邊界上,從而避免了編譯器自動對齊。在這個例子中,sizeof(bar) == 8。
         
      這個技巧有一個重要的作用,尤其是這個結構作爲API的一部分提供給第三方開發使用的時候。第三方開發者可能將編譯器的默認對齊選項改變,從而造成這個結構在你的發行的DLL中使用某種對齊方式,而在第三方開發者哪裏卻使用另外一種對齊方式。這將會導致重大問題。
          比如,foo結構,我們的DLL使用默認對齊選項,對齊爲
      c1 00000000, s 00000002, c2 00000004, i 00000008,同時sizeof(foo) == 12。
      而第三方將對齊選項關閉,導致
          c1 00000000, s 00000001, c2 00000003, i 00000004,同時sizeof(foo) == 8。
      如何使用c/c++中的對齊選項
          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  )
          意義和/Zpn選項相同。比如:
          #pragma pack(1)
          struct foo_pack
          {
              char c1;
              short s;
              char c2;
              int i;
          };
          #pragma pack()
          棧內存對齊
          我們可以觀察到,在vc6中棧的對齊方式不受結構成員對齊選項的影響。(本來就是兩碼事)。它總是保持對齊,而且對齊在4字節邊界上。
          驗證代碼
          #include <stdio.h>
          struct foo
          {
              char c1;
              short s;
              char c2;
              int i;
          };
          struct bar
          {
              char c1;
              char c2;
              short s;
              int i;
          };
          #pragma pack(1)
          struct foo_pack
          {
              char c1;
              short s;
              char c2;
              int i;
          };
          #pragma pack()

          int main(int argc, char* argv[])
          {
              char c1;
              short s;
              char c2;
              int i;
          struct foo a;
          struct bar b;
          struct foo_pack p;
          printf("stack c1 %p, s %p, c2 %p, i %p/n",
              (unsigned int)(void*)&c1 - (unsigned int)(void*)&i,
              (unsigned int)(void*)&s - (unsigned int)(void*)&i,
              (unsigned int)(void*)&c2 - (unsigned int)(void*)&i,
              (unsigned int)(void*)&i - (unsigned int)(void*)&i);
          printf("struct foo c1 %p, s %p, c2 %p, i %p/n",
              (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
          printf("struct bar c1 %p, c2 %p, s %p, i %p/n",
              (unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,
              (unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,
              (unsigned int)(void*)&b.s - (unsigned int)(void*)&b,
              (unsigned int)(void*)&b.i - (unsigned int)(void*)&b);
          printf("struct foo_pack c1 %p, s %p, c2 %p, i %p/n",
              (unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,
              (unsigned int)(void*)&p.s - (unsigned int)(void*)&p,
              (unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,
              (unsigned int)(void*)&p.i - (unsigned int)(void*)&p);
          printf("sizeof foo is %d/n", sizeof(foo));
          printf("sizeof bar is %d/n", sizeof(bar));
          printf("sizeof foo_pack is %d/n", sizeof(foo_pack));
         
          return 0;

          } 

 

 

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