C语言用0指针求结构提的首地址
本文转自: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可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。