C新手知識:結構體內存對齊

結構體是C語言中比較重要的一種數據類型。一些高級數據結構如鏈表、樹、圖都是基於結構體實現的。但對於結構體的知識你又瞭解多少?看看下面的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include
 
struct testType{
    char first;
    int second;
    float third;
};
int main()
{
    testType mytest;
    printf("mytest地址是:%x\n",&mytest);
    printf("char變量地址是:%x\n",&mytest.first);
    printf("int變量地址是:%x\n",&mytest.second);
    printf("float變量地址是:%x\n",&mytest.third);
 
    return 0;
}

輸出結果是:

1
2
3
4
mytest地址是:22ff30
char變量地址是:22ff30
int變量地址是:22ff34
float變量地址是:22ff38

按照我的理解char變量佔一個字節,那麼int的地址就是mytest+1。但從運行結果來看,int的地址卻是成了mytest+4。這時候我馬上想到了一個詞 “內存對齊”。內存對齊指的是:

對於大部分程序員來說,“內存對齊”對他們來說都應該是“透明的”。“內存對齊”應該是編譯器的“管轄範圍”。編 譯器爲程序中的每個“數據單元”安 排在適當的位置上。但是C語言的一個特點就是太靈活,太強大,它允許你干預“內存對齊”。如果你想 瞭解更加底層的祕密,“內存對齊”對你就不應該再透明瞭。

那麼爲什麼要“對齊內存”呢?有下面兩個原因:

1、 平臺原因(移植原因):不是所有的硬件平臺 都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
2、 性能原因:數據結構(尤其是棧)應該儘可能 地在自然邊界上對齊。原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。

Win32平臺下的微軟C編譯器(cl.exe for 80×86)的對齊策略:
1) 結構體變量的首地址能夠被其最寬基本類型成員的大小所整除;
備註:編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本數據類型,然後尋找內存地址能被該基本數據類型所整除的位置,作爲結構體的首地址。將這個最寬的基本數據類型的大小作爲上面介紹的對齊模數。
2) 結構體每個成員相對於結構體首地址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充字節(internal adding);
備註:爲結構體的一個成員開闢空間之前,編譯器首先檢查預開闢空間的首地址相對於結構體首地址的偏移是否是本成員的整數倍,若是,則存放本成員,反之,則在本成員和上一個成員之間填充一定的字節,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個字節。
3) 結構體的總大小爲結構體最寬基本類型成員大小的整數倍,如有需要,編譯器會在最末一個成員之後加上填充字節(trailing padding)。
備註:結構體總大小是包括填充字節,最後一個成員滿足上面兩條以外,還必須滿足第三條,否則就必須在最後填充幾個字節以達到本條要求。

而在GNU GCC編譯器中,遵循的準則有些區別,對齊模數不是像上面所述的那樣,根據最寬的基本數據類型來定。

在GCC中,對齊模數的準則是:對齊模數最大隻能是4,也就是說,即使結構體中有double類型,對齊模數還是4,所以對齊模數只能是 1,2,4。而且在上述的三條中,第2條裏,offset必須是成員大小的整數倍,如果這個成員大小小於等於4則按照上述準則進行,但是如果大於4了,則 結構體每個成員相對於結構體首地址的偏移量(offset)只能按照是4的整數倍來進行判斷是否添加填充。

看了上面的對齊規則,我繼續執行了一個代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
struct testType{
    char first;
    double third;
};
int main()
{
    testType mytest;
    printf("mytest地址是:%x\n",&mytest);
    printf("char變量地址是:%x\n",&mytest.first);
    printf("double變量地址是:%x\n",&mytest.third);
    printf("結構體的大小:%d\n",sizeof(struct testType));
 
    return 0;
}

執行結果是:

1
2
3
4
mytest地址是:22ff30
char變量地址是:22ff30
double變量地址是:22ff38
結構體的大小:16

我們看到結構體只有兩個變量,一個char,一個double。但是char和double之間隔了8個字節。這就是編譯器“對齊內存”的結果。

對齊模數的選擇只能是根據基本數據類型,所以對於結構體中嵌套結構體,只能考慮其拆分的基本數據類型。而對於對齊準則中的第2條,確是要將整個結構體看成是一個成員,成員大小按照該結構體根據對齊準則判斷所得的大小。
類對象在內存中存放的方式和結構體類似,這裏就不再說明。需要指出的是,類對象的大小隻是包括類中非靜態成員變量所佔的空間,如果有虛函數,那麼再另外增加一個指針所佔的空間即可。

1. 內存對齊與編譯器設置有關,首先要搞清編譯器這個默認值是多少

2. 如果不想編譯器默認的話,可以通過#pragma pack(n)來指定按照n對齊

3. 每個結構體變量對齊,如果對齊參數n(編譯器默認或者通過pragma指定)大於該變量所佔字節數(m),那麼就按照m對齊,內存偏移後的地址是m的倍數,否則是按照n對齊,內存偏移後的地址是n的倍數。也就是最小化長度規則

4. 結構體總大小: 對齊後的長度必須是成員中最大的對齊參數的整數倍。最大對齊參數是從第三步得到的。

5. 補充:如果結構體A中還要結構體B,那麼B的對齊方式是選它裏面最長的成員的對齊方式

學習C語言可以瞭解很多底層的基礎知識,這對我們以後寫程序是有很大好處的。“千里之行始於足下”。

(全文完)

參考資料:
1>.http://blog.csdn.net/xing_hao/article/details/6678048
2>.http://baike.baidu.com/view/4786260.htm

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