1. 程式人生 > >C語言之struct大小、首地址與記憶體對齊—由結構體成員地址得到結構體首地址

C語言之struct大小、首地址與記憶體對齊—由結構體成員地址得到結構體首地址

被問到如下問題:
給定一個結構體中某個變數地址,可否得到結構體變數的地址?


答案是可以,但是對不同的場合有不同的結果;這與微處理器平臺編譯器的處理不可分割。


首先,對於處理器,大尾端、小尾端的因素必須考慮;
其次:

一、 ANSIC標準中並沒有規定,相鄰宣告的變數在記憶體中一定要相鄰。

為了程式的高效性,記憶體對齊問題由編譯器自行靈活處理,這樣導致相鄰的變數之間可能會有一些填充 位元組。對於基本資料型別(intchar),他們佔用的記憶體空間在一個確定硬體系統下有個確定的值,所以,接下來我們只是考慮結構體成員記憶體分配情況。

1、Win32平臺下的微軟C編譯器(cl.exe for 80×86)的對齊策略:

1)結構體變數的首地址能夠被其最寬基本型別成員的大小所整除
備註:編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本資料型別,然後尋找記憶體地址能被該基本資料型別所整除的位置,作為結構體的首地址。將這個最寬的基本資料型別的大小作為上面介紹的對齊模數。


2)結構體每個成員相對於結構體首地址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充位元組(internaladding
);
備註:為結構體的一個成員開闢空間之前,編譯器首先檢查預開闢空間的首地址相對於結構體首地址的偏移是否是本成員的整數倍,若是,則存放本成員,反之,則在本成員和上一個成員之間填充一定的位元組,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個位元組。


3) 結構體的總大小為結構體最寬基本型別成員大小的整數倍,如有需要,編譯器會在最末一個成員之後加上填充位元組(trailingpadding)。

備註:結構體總大小是包括填充位元組,最後一個成員滿足上面兩條以外,還必須滿足第三條,否則就必須在最後填充幾個位元組以達到本條要求。


2、GNU GCC編譯器中,遵循的準則有些區別,對齊模數不是像上面所述的那樣,根據最寬的基本資料型別來定。
在 GCC中,對齊模數的準則是:對齊模數最大隻能是4,也就是說,即使結構體中有double型別,對齊模數還是4,所以對齊模數只能是1,2,4。而且在上述的三條中,第2條裡,offset必須是成員大小的整數倍,如果這個成員大小小於等於4則按照上述準則進行,但是如果大於4了,則結構體每個成員相對於結構體首地址的偏移量(offset)只能按照是4的整數倍來進行判斷是否新增填充。

譬如:
struct id
{

char      ch;
double dd;

}T;
根據以上準則,在windows下,使用VC編譯器,sizeof(T)的大小為16個位元組;GNUGCC編譯器則得到12位元組。

二、struct的首地址即為第一個元素的首地址
如下程式,測試環境,GNU/Linux Debian, GCC4.3.2-1-1

 1#include<stdio.h>
 2 #define STRUCT_OFFSET(id, element) ((unsignedlong)&((struct id*)0)->element)
 3 struct _Test
 4 {
 5        char ch;
 6        double dd;
 7 };
 8
 9 int main(void )
10 {
11        struct _Teststru;
12
13        printf("theaddrress of first ele of struct is%x\n", &stru.ch);
14    
15        unsigned long offset= STRUCT_OFFSET(_Test, dd);
16
17        printf("the offset of dd is%x, offset =%u\n", &stru.dd, offset);
18        printf("the start addrress of struct caculated from dd is%x\n", (char *)&stru.dd- offset);
19
20        return 0;
21 }

$ ./a.out
the addrress of first ele of struct is bfb86124
the offset of dd is bfb86128, offset = 4
the start addrress of struct caculated from dd isbfb86124


其 中,整個程式中最關鍵的部分就是如何求出結構體中某個成員相對於結構體首地址的偏移量。

這裡的解決方法是:假設存在一個虛擬地址0將該地址強制轉換成為 該結構體指標型別(structid*)0。那麼地址0開始到sizeof(struct)-1長度的記憶體區域就可以視為一個結構體的記憶體。

這樣結構體中任何一個元素都可 以通過對該結構體指標解引用得到。

由於該結構體的起始地址為0,因此任何一個成員的地址應該等於其相對於結構體起始地址的偏移,這也就是計算偏移量的方 法:

#defineSTRUCT_OFFSET(id, element) ((unsignedlong)&((struct id*)0)->element)
Linux核心裡面的list_entry巨集就是這樣的。

說明:
1) 前面不是說結構體成員的地址是其大小的整數倍,怎麼又說到偏移量了呢?

因為有了第1點存在,所以我們就可以只考慮成員的偏移量,這樣思考起來簡單。想想為什麼。
結構體某個成員相對於結構體首地址的偏移量可以通過巨集offsetof()來獲得,這個巨集也在stddef.h中定義,如下:
#define offsetof(s,m) (size_t)&(((s*)0)->m)


例如,想要獲得S中c的偏移量,方法為
size_t pos = offsetof(s, dd);// pos等於4

2)基本型別是指前面提到的像char、short、int、float、double這樣的內建資料型別,這裡所說的“資料寬度”就是指其sizeof的大小。

由於結構體的成員可以是複合型別,比如另外一個結構體,所以在尋找最寬基本型別成員時,應當包括複合型別成員的子成員,而不是把複合成員看成是一個整體。

但在確定複合型別成員的偏移位置時則是將複合型別作為整體看待。

三、有一個影響sizeof的重要參量還未被提及,那便是編譯器的pack指令。

它是用來調整結構體對齊方式的,不同編譯器名稱和用法略有不同,VC6中通過#pragmapack實現,也可以直接修改/Zp編譯開關。

#pragma pack的基本用法為:#pragma pack( n ),n為位元組對齊數,其取值
為1、2、4、8、16,預設是8,如果這個值比結構體成員的sizeof值小,那麼該成員的偏移量應該以此值為準,即是說,結構體成員的偏移量應該取二者的最小值,公式如下:
offsetof( item ) = min( n, sizeof( item ) )

四、還有一點要注意,“空結構體”(不含資料成員)的大小不為0,而是1。

試想一個“不佔空間”的變數如何被取地址、兩個不同的“空結構體”變數又如何得以區分呢?於是,“空結構體”變數也得被儲存,這樣編譯器也就只能為其分配一個位元組的空間用於佔位了。
如下:
struct S { };
sizeof( S ); // 結果為1

五、含位域結構體的sizeof

位域成員不能單獨被取sizeof值,我們這裡要討論的是含有位域的結構體的sizeof,只是考慮到其特殊性而將其專門列了出來。
C99規定int、unsignedint和bool可以作為位域型別,但編譯器幾乎都對此作了擴充套件,允許其它型別型別的存在。