C语言struct内存占用问题

本文编辑整理自:
http://hi.baidu.com/pine515/blog/item/28f41f496042e8ee83025c4e.html
http://blog.sina.com.cn/s/blog_4913c1f301000bip.html
一、 ANSI C标准中并没有规定,相邻声明的变量在内存中一定要相邻。
为了程序的高效性,内存对齐问题由编译器自行灵活处理,这样导致相邻的变量之间可能会有一些填充字节。
对于基本数据类型(比如int,char),他们占用的内存空间在一个确定硬件系统下有个确定的值,所以,接下来我们只是考虑结构体成员内存分配情况。

1.1Win32平台下的微软C编译器(cl.exe for 80×86)的对齐策略
Win32平台下的微软C编译器(cl.exe for 80×86)的对齐策略如下:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding)
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。
备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。
   为了提高CPU的存储速度,VC对一些变量的起始地址做了“对齐”处理。在默认情况下,VC规定各成员变量存放的起始地址相对于结构体的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节
请看下面的结构:
struct MyStruct
{
double dda1;
char dda;
int type
};

对结构MyStruct采用sizeof会出现什么结果呢?sizeof(MyStruct)为多少呢?也许你会这样求:
sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13
但是当在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为16。你知道为什么在VC中会得出这样一个结果吗?
其实,这是VC对变量存储的一个特殊处理。为了提高CPU的存储速度,VC对一些变量的起始地址做了“对齐”处理。在默认情况下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。下面列出常用类型的
对齐方式(vc6.0,32位系统)。

类型      对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
char      偏移量必须为sizeof(char)即1的倍数
short     偏移量必须为sizeof(short)即2的倍数
int   偏移量必须为sizeof(int)即4的倍数
float 偏移量必须为sizeof(float)即4的倍数
double 偏移量必须为sizeof(double)即8的倍数

各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。
下面用前面的例子来说明VC到底怎么样来存放结构的。
struct MyStruct
{
double dda1;
char dda;
int type
};

为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式,先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(double)的倍数),该成员变量占用sizeof(double)=8个字节;接下来为第二个成员dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把dda存放在偏移量为8的地方满足对齐方式,该成员变量占用 sizeof(char)=1个字节;接下来为第三个成员type分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof (int)=4的倍数,为了满足对齐方式对偏移量的约束问题,VC自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是sizeof(int)=4的倍数,所以把type存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节;这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。所以整个结构的大小为:sizeof(MyStruct)=8+1+ 3+4=16,其中有3个字节是VC自动填充的,没有放任何有意义的东西。
下面再举个例子,交换一下上面的MyStruct的成员变量的位置,使它变成下面的情况:
struct MyStruct
{
char dda;
double dda1;
int type
};

这个结构占用的空间为多大呢?在VC6.0环境下,可以得到sizeof(MyStruc)为24。结合上面提到的分配空间的一些原则,分析下VC怎么样为上面的结构分配空间的。(简单说明)
struct MyStruct
{
char dda; //偏移量为0,满足对齐方式,dda占用1个字节;
double dda1; //下一个可用的地址的偏移量为1,不是sizeof(double)=8 的倍数,需要补足7个字节才能使偏移量变为8(满足对齐方式),因此VC自动填充7个字节,dda1存放在偏移量为8的地址上,它占用8个字节。
int type; //下一个可用的地址的偏移量为16,是sizeof(int)=4的倍数,满足int的对齐方式,所以不需要VC自动填充,type存放在偏移量为16的地址上,它占用4个字节。
};

所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,不是结构的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof (double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为 sizeof(double)=8的倍数。 所以该结构总的大小为:sizeof(MyStruc)为1+7+8+4+4=24。其中总的有7+4=11个字节是VC自动填充的,没有放任何有意义的东西。
1.2GNU GCC编译器中,遵循的准则有些区别,对齐模数不是像上面所述的那样。
在 GCC中,对齐模数的准则是:
根据最宽的基本数据类型来确定对齐模数且对齐模数最大只能是 4,也就是说,即使结构体中有double类型,对齐模数还是4,所以对齐模数只能是124。而且在上述的三条中,第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
#include <stdio.h>
#define STRUCT_OFFSET(id, element) ((unsignedlong) &((structid*)0)->element)
struct _Test
{
    char ch;
    double dd;
 };
int main(void)
{
    struct _Test stru;
    printf("the addrress of first ele of struct is %x\n", &stru.ch);
    unsigned long offset = STRUCT_OFFSET(_Test, dd);
    printf("the offset of dd is %x, offset = %u\n", &stru.dd, offset);
    printf("the start addrress of struct caculated from dd is %x\n", (char*)&stru.dd - offset);
    return 0;
}

执行结果
$ ./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) &((structid*)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 (n),编译器将按照n个字节对齐;
使用伪指令#pragma pack (),取消自定义字节对齐方式。

#pragma pack的基本用法为:#pragma pack( n ),n为字节对齐数,其取值
1、2、4、8、16,默认是8,如果这个值比结构体成员的sizeof值小,那么该成员的偏移量应该以此值为准,即是说,结构体成员的偏移量应该取二者的最小值,公式如下:
offsetof( item ) = min( n, sizeof( item ) )

注意:如果#pragma pack (n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。
   VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。
VC 中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏 移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条 件,分下面两种情况
如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数
下面举例说明其用法。
#pragma pack(push) //保存对齐状态
#pragma pack(4)//设定为4字节对齐
struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop)//恢复对齐状态

以上结构的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节。接着开始 为 m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于n),m4占用8个字节。接 着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。如 果把上面的#pragma pack(4)改为#pragma pack(16),那么我们可以得到结构的大小为24。
四、“空结构体”(不含数据成员)的大小不为0,而是1。
试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢?于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。
如下:
struct S { };
sizeof( S ); // 结果为1
经过 验证,在VC2008上的确如此。
五、含位域结构体的 sizeof
位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构体的sizeof,只是考虑到其特殊性而将其专门列了出来。
C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在
六、一些面试题
 Intel、微软等公司曾经出过一道类似的面试题:
1. #include <iostream.h>
2. #pragma pack(8)
3. struct example1
4. {
5. short a;
6. long b;
7. };
8. struct example2
9. {
10. char c;
11. example1 struct1;
12. short e;    
13. };
14. #pragma pack()

15. int main(int argc, char* argv[])
16. {
17. example2 struct2;
18. cout << sizeof(example1) << endl;
19. cout << sizeof(example2) << endl;
20. cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) 
<< endl;
21. return 0;
22. }
问程序的输入结果是什么? 答案是:
8
16
4
   程序中第2行#pragma pack (8)虽然指定了对界为8,但是由于 struct example1 中的成 员最大size为4(long变量size为4),故struct example1仍然按4字节对界, struct example1 的size为8,即第18行的输出结果;
   struct example2 中包含了struct example1,其本身包含的简单数据成员的最大size 为2(short变量e),但是因为其包含了struct example1,而struct example1中的最大成 员size为4,struct example2也应以4对界,#pragma pack (8)中指定的对界对struct ex
ample2也不起作用,故19行的输出结果为16;
  由于struct example2中的成员以4为单位对界,故其char变量c后应补充3个空,其后才是成员struct1的内存空间,20行的输出结果为4。
七天、补充
有朋友对上面的内容提出了一下质疑:
质疑1
2012-01-03 01:08 | 回复
我 在自己的机子上测试了一下,发现linux上用 gcc(4.6.1)编译后struct的内存对齐和vs2010里的cl.exe一样,都是以最大成员长度为准,不像文章所说的gcc里最大是4个字 节。另外在linux上可以定义空struct,用sizeof获取长度为0,这也和文章所说的不一致,我在vs2010里没法定义空struct,所以 没有得到结果。 
  
个人觉得可能是新版本的编译器已经把这部分的规则统一了。

结束!作了扩展,允许其它类型类型的存在

你可能感兴趣的:(c,struct,gcc,存储,语言,编译器)