C语言之struct大小、首地址与内存对齐

被问到如下问题:

给定一个结构体中某个变量地址,可否得到结构体变量的地址?

答案是可以,但是对不同的场合有不同的结果;这与微处理器平台、编译器的处理不可分割。

首先,对于处理器,大尾端、小尾端的因素必须考虑;

其次:

一、 ANSI C标准中并没有规定,相邻声明的变量在内存中一定要相邻。

为了程序的高效性,内存对齐问题由编译器自行灵活处理,这样导致相邻的变量之间可能会有一些填充 字节。对于基本数据类型(int char),他们占用的内存空间在一个确定硬件系统下有个确定的值,所以,接下来我们只是考虑结构体成员内存分配情况。

1、Win32平台下的微软C编译器(cl.exe for 80×86)的对齐策略:

1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。

2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。

3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。

备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

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个字节;GNU GCC编译器则得到12字节。

二、struct的首地址即为第一个元素的首地址

如下程序,测试环境,GNU/Linux Debian, GCC 4.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 _Test stru;

12

13 printf("the addrress of first ele of struct is %x\n", &stru.ch);

14

15 unsignedlong 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) ((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中通过#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可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。