本文轉自:http://blog.csdn.net/hilyoo/article/details/4464448
給定一個結構體中某個變量地址,可否得到結構體變量的地址?
答案是可以,但是對不同的場合有不同的結果;這與微處理器平台、編譯器的處理不可分割。
首先,對於處理器,大尾端、小尾端的因素必須考慮;
其次:
一、ANSIC標准中並沒有規定,相鄰聲明的變量在內存中一定要相鄰。
為了程序的高效性,內存對齊問題由編譯器自行靈活處理,這樣導致相鄰的變量之間可能會有一些填充字節。對於基本數據類型(int char),他們占用的內存空間在一個確定硬件系統下有個確定的值,所以,接下來我們只是考慮結構體成員內存分配情況。
GNU GCC編譯器中,遵循的准則有些區別,對齊模數不是像上面所述的那樣,根據最寬的基本數據類型來定。
在GCC中,對齊模數的准則是:對齊模數最大只能是4,也就是說,即使結構體中有double類型,對齊模數還是4,所以對齊模數只能是1,2,4。而且在上述的三條中,第2條里,offset必須是成員大小的整數倍,如果這個成員大小小於等於4則按照上述准則進行,但是如果大於4了,則結構體每個成員相對於結構體首地址的偏移量(offset)只能按照是4的整數倍來進行判斷是否添加填充。
二、struct的首地址即為第一個元素的首地址
如下程序,測試環境,GNU/Linux Debian, GCC 4.3.2-1-1
1 #include <stdio.h>
2 #define STRUCT_OFFSET(id, element) ((unsigned long) &((struct id*)0)->element)
3 struct _Test
4 {
5 char ch;
6 double dd;
7 };
8
9 int main(void )
10 {
11 struct _Test stru;
12
13 printf("the addrress 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 is bfb86124
其中,整個程序中最關鍵的部分就是如何求出結構體中某個成員相對於結構體首地址的偏移量。
這里的解決方法是:假設存在一個虛擬地址0,將該地址強制轉換成為該結構體指針類型(struct id*)0。那么地址0開始到sizeof(struct)-1長度的內存區域就可以視為一個結構體的內存。
這樣結構體中任何一個元素都可以通過對該結構體指針解引用得到。
由於該結構體的起始地址為0,因此任何一個成員的地址應該等於其相對於結構體起始地址的偏移,這也就是計算偏移量的方法:
#define STRUCT_OFFSET(id, element) ((unsigned long) &((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中通過#pragma pack實現,也可以直接修改/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、unsigned int和bool可以作為位域類型,但編譯器幾乎都對此作了擴展,允許其它類型類型的存在。