結構體的大小

運算符sizeof可以計算出給定類型的大小,對於32位系統來說,
sizeof(char) = 1; sizeof(int) = 4。
基本數據類型的大小很好計算,我們來看一下如何計算構造數據類型的大小。
    C語言中的構造數據類型有三種:數組、結構體和共用體。
數組是相同類型的元素的集合,只要會計算單個元素的大小,整個數組所佔空間等於基礎元素大小乘上元素的個數。
結構體中的成員可以是不同的數據類型,成員按照定義時的順序依次存儲在連續的內存空間。和數組不一樣的是,結構體的大小不是所有成員大小簡單的相加,需要考慮到系統在存儲結構體變量時的地址對齊問題。看下面這樣的一個結構體:
      struct stu1
       {
       int    i;
       char c;
       int j;
       };
先介紹一個相關的概念——偏移量。偏移量指的是結構體變量中成員的地址和結構體變量地址的差。結構體大小等於最後一個成員的偏移量加上最後一個成員的大小。顯然,結構體變量中第一個成員的地址就是結構體變量的首地址。因此,第一個成員i的偏移量爲0。第二個成員c的偏移量是第一個成員的偏移量加上第一個成員的大小(0+4),其值爲4;第三個成員j的偏移量是第二個成員的偏移量加上第二個成員的大小(4+1),其值爲5。
實際上,由於存儲變量時地址對齊的要求,編譯器在編譯程序時會遵循兩條原則:
一、結構體變量中成員的偏移量必須是成員大小的整數倍(0被認爲是任何數的整數倍)
二、結構體大小必須是所有成員大小的整數倍。
對照第一條,上面的例子中前兩個成員的偏移量都滿足要求,但第三個成員的偏移量爲5,並不是自身(int)大小的整數倍。編譯器在處理時會在第二個成員後面補上3個空字節,使得第三個成員的偏移量變成8。
對照第二條,結構體大小等於最後一個成員的偏移量加上其大小,上面的例子中計算出來的大小爲12,滿足要求。
再看一個滿足第一條,不滿足第二條的情況
struct stu2
       {
       int   k;
       short t;
};
成員k的偏移量爲0;成員t的偏移量爲4,都不需要調整。但計算出來的大小爲6,顯然不是成員k大小的整數倍。因此,編譯器會在成員t後面補上2個字節,使得結構體的大小變成8從而滿足第二個要求。
由此可見,大家在定義結構體類型時需要考慮到字節對齊的情況,不同的順序會影響到結構體的大小。對比下面兩種定義順序
struct stu3                              struct stu4
{                                      {
 char c1;                               char c1;
 int i;                                  char c2;
 char c2;                               int   i;
}                                      }
雖然結構體stu3和stu4中成員都一樣,但sizeof(struct stu3)的值爲12而sizeof(struct stu4)的值爲8。
如果結構體中的成員又是另外一種結構體類型時應該怎麼計算呢?只需把其展開即可。但有一點需要注意,展開後的結構體的第一個成員的偏移量應當是被展開的結構體中最大的成員的整數倍。看下面的例子,
struct temp
       {
       short i;    +1
       struct
       {
          char c;  +5
          int j;   +6
       } ss;       +10+2
          int k;   +4
};             =16
結構體temp的成員ss.c的偏移量應該是4,而不是2。整個結構體大小應該是16。
 
所以,結構體的成員排序應該是從大到小
 
 
 
 
 
現在回到我們關心的struct上來。ANSI C規定一種結構類型的大小是它所有字段的大小以及字段之間或字段尾部的填充區大小之和。嗯?填充區?對,這就是爲了使結構體字段滿足內存對齊要求而額外分配給結構體的空間。那麼結構體本身有什麼對齊要求嗎?有的,ANSI C標準規定結構體類型的對齊要求不能比它所有字段中要求最嚴格的那個寬鬆,可以更嚴格(但此非強制要求,VC7.1就僅僅是讓它們一樣嚴格)。我們來看一個例子(以下所有試驗的環境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1,內存對齊編譯選項是"默認",即不指定/Zp與/pack選項):

  typedef struct ms1
  {
     char a;
     int b;
  } MS1;

    假設MS1按如下方式內存佈局(本文所有示意圖中的內存地址從左至右遞增):
       _____________________________
       |       |                   |
       |   a   |        b          |
       |       |                   |
       +---------------------------+
 Bytes:    1             4

    因爲MS1中有最強對齊要求的是b字段(int),所以根據編譯器的對齊規則以及ANSI C標準,MS1對象的首地址一定是4(int類型的對齊模數)的倍數。那麼上述內存佈局中的b字段能滿足int類型的對齊要求嗎?嗯,當然不能。如果你是編譯器,你會如何巧妙安排來滿足CPU的癖好呢?呵呵,經過1毫秒的艱苦思考,你一定得出瞭如下的方案:

       _______________________________________
       |       |///////////|                 |
       |   a   |//padding//|       b         |
       |       |///////////|                 |
       +-------------------------------------+
 Bytes:    1         3             4

   這個方案在a與b之間多分配了3個填充(padding)字節,這樣當整個struct對象首地址滿足4字節的對齊要求時,b字段也一定能滿足int型的4字節對齊規定。那麼sizeof(MS1)顯然就應該是8,而b字段相對於結構體首地址的偏移就是4。非常好理解,對嗎?現在我們把MS1中的字段交換一下順序:

  typedef struct ms2
  {
     int a;
     char b;
  } MS2;

    或許你認爲MS2比MS1的情況要簡單,它的佈局應該就是

       _______________________
       |             |       |
       |     a       |   b   |
       |             |       |
       +---------------------+
 Bytes:      4           1

    因爲MS2對象同樣要滿足4字節對齊規定,而此時a的地址與結構體的首地址相等,所以它一定也是4字節對齊。嗯,分析得有道理,可是卻不全面。讓我們來考慮一下定義一個MS2類型的數組會出現什麼問題。C標準保證,任何類型(包括自定義結構類型)的數組所佔空間的大小一定等於一個單獨的該類型數據的大小乘以數組元素的個數。換句話說,數組各元素之間不會有空隙。按照上面的方案,一個MS2數組array的佈局就是:

|<-    array[1]     ->|<-    array[2]     ->|<- array[3] .....

__________________________________________________________
|             |       |              |      |
|     a       |   b   |      a       |   b  |.............
|             |       |              |      |
+----------------------------------------------------------
Bytes:  4         1          4           1

    當數組首地址是4字節對齊時,array[1].a也是4字節對齊,可是array[2].a呢?array[3].a ....呢?可見這種方案在定義結構體數組時無法讓數組中所有元素的字段都滿足對齊規定,必須修改成如下形式:

    當數組首地址是4字節對齊時,array[1].a也是4字節對齊,可是array[2].a呢?array[3].a ....呢?可見這種方案在定義結構體數組時無法讓數組中所有元素的字段都滿足對齊規定,必須修改成如下形式:

       ___________________________________
       |             |       |///////////|
       |     a       |   b   |//padding//|
       |             |       |///////////|
       +---------------------------------+
 Bytes:      4           1         3

    現在無論是定義一個單獨的MS2變量還是MS2數組,均能保證所有元素的所有字段都滿足對齊規定。那麼sizeof(MS2)仍然是8,而a的偏移爲0,b的偏移是4。

  typedef struct ms3
  {
     char a;
     short b;
     double c;
  } MS3;

    我想你一定能得出如下正確的佈局圖:
        
        padding 
           |
      _____v_________________________________
      |   |/|     |/////////|               |
      | a |/|  b  |/padding/|       c       |
      |   |/|     |/////////|               |
      +-------------------------------------+
Bytes:  1  1   2       4            8
          
    sizeof(short)等於2,b字段應從偶數地址開始,所以a的後面填充一個字節,而sizeof(double)等於8,c
字段要從8倍數地址開始,前面的a、b字段加上填充字節已經有4 bytes,所以b後面再填充4個字節就可以保證c字段的對齊要求了。sizeof(MS3)等於16,b的偏移是2,c的偏移是8。接着看看結構體中字段還是結構類型的情況:

  typedef struct ms4
  {
     char a;
     MS3 b;
  } MS4;

    MS3中內存要求最嚴格的字段是c,那麼MS3類型數據的對齊模數就與double的一致(爲8),a字段後面應填充7個字節,因此MS4的佈局應該是:
       _______________________________________
       |       |///////////|                 |
       |   a   |//padding//|       b         |
       |       |///////////|                 |
       +-------------------------------------+
 Bytes:    1         7             16

顯然,sizeof(MS4)等於24,b的偏移等於8。
 
 
  到了這裏,我們可以回答本文提出的最後一個問題了。結構體的內存佈局依賴於CPU、操作系統、編譯器及編譯時的對齊選項,而你的程序可能需要運行在多種平臺上,你的源代碼可能要被不同的人用不同的編譯器編譯(試想你爲別人提供一個開放源碼的庫),那麼除非絕對必需,否則你的程序永遠也不要依賴這些詭異的內存佈局。順便說一下,如果一個程序中的兩個模塊是用不同的對齊選項分別編譯的,那麼它很可能會產生一些非常微妙的錯誤。如果你的程序確實有很難理解的行爲,不防仔細檢查一下各個模塊的編譯選項。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章