C新手知识:结构体内存对齐

结构体是C语言中比较重要的一种数据类型。一些高级数据结构如链表、树、图都是基于结构体实现的。但对于结构体的知识你又了解多少?看看下面的代码:

帮助
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include
 
struct testType{
     char first;
     int second;
     float third;
};
int main()
{
     testType mytest;
     printf( "mytest地址是:%x\n" ,&mytest);
     printf( "char变量地址是:%x\n" ,&mytest.first);
     printf( "int变量地址是:%x\n" ,&mytest.second);
     printf( "float变量地址是:%x\n" ,&mytest.third);
 
     return 0;
}

输出结果是:

帮助
1
2
3
4
mytest地址是:22ff30
char变量地址是:22ff30
int变量地址是:22ff34
float变量地址是:22ff38

按照我的理解char变量占一个字节,那么int的地址就是mytest+1。但从运行结果来看,int的地址却是成了mytest+4。这时候我马上想到了一个词 “内存对齐”。内存对齐指的是:

对于大部分程序员来说,“内存对齐”对他们来说都应该是“透明的”。“内存对齐”应该是编译器的“管辖范围”。编 译器为程序中的每个“数据单元”安 排在适当的位置上。但是C语言的一个特点就是太灵活,太强大,它允许你干预“内存对齐”。如果你想 了解更加底层的秘密,“内存对齐”对你就不应该再透明了。

那么为什么要“对齐内存”呢?有下面两个原因:

1、 平台原因(移植原因):不是所有的硬件平台 都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、 性能原因:数据结构(尤其是栈)应该尽可能 地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

Win32平台下的微软C编译器(cl.exe for 80×86)的对齐策略:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。
备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

而在GNU GCC编译器中,遵循的准则有些区别,对齐模数不是像上面所述的那样,根据最宽的基本数据类型来定。

在GCC中,对齐模数的准则是:对齐模数最大只能是4,也就是说,即使结构体中有double类型,对齐模数还是4,所以对齐模数只能是 1,2,4。而且在上述的三条中,第2条里,offset必须是成员大小的整数倍,如果这个成员大小小于等于4则按照上述准则进行,但是如果大于4了,则 结构体每个成员相对于结构体首地址的偏移量(offset)只能按照是4的整数倍来进行判断是否添加填充。

看了上面的对齐规则,我继续执行了一个代码:

帮助
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
struct testType{
     char first;
     double third;
};
int main()
{
     testType mytest;
     printf( "mytest地址是:%x\n" ,&mytest);
     printf( "char变量地址是:%x\n" ,&mytest.first);
     printf( "double变量地址是:%x\n" ,&mytest.third);
     printf( "结构体的大小:%d\n" ,sizeof(struct testType));
 
     return 0;
}

执行结果是:

帮助
1
2
3
4
mytest地址是:22ff30
char变量地址是:22ff30
double变量地址是:22ff38
结构体的大小:16

我们看到结构体只有两个变量,一个char,一个double。但是char和double之间隔了8个字节。这就是编译器“对齐内存”的结果。

对齐模数的选择只能是根据基本数据类型,所以对于结构体中嵌套结构体,只能考虑其拆分的基本数据类型。而对于对齐准则中的第2条,确是要将整个结构体看成是一个成员,成员大小按照该结构体根据对齐准则判断所得的大小。
类对象在内存中存放的方式和结构体类似,这里就不再说明。需要指出的是,类对象的大小只是包括类中非静态成员变量所占的空间,如果有虚函数,那么再另外增加一个指针所占的空间即可。

1. 内存对齐与编译器设置有关,首先要搞清编译器这个默认值是多少

2. 如果不想编译器默认的话,可以通过#pragma pack(n)来指定按照n对齐

3. 每个结构体变量对齐,如果对齐参数n(编译器默认或者通过pragma指定)大于该变量所占字节数(m),那么就按照m对齐,内存偏移后的地址是m的倍数,否则是按照n对齐,内存偏移后的地址是n的倍数。也就是最小化长度规则

4. 结构体总大小: 对齐后的长度必须是成员中最大的对齐参数的整数倍。最大对齐参数是从第三步得到的。

5. 补充:如果结构体A中还要结构体B,那么B的对齐方式是选它里面最长的成员的对齐方式

学习C语言可以了解很多底层的基础知识,这对我们以后写程序是有很大好处的。“千里之行始于足下”。

(全文完)

参考资料:
1>.http://blog.csdn.net/xing_hao/article/details/6678048
2>.http://baike.baidu.com/view/4786260.htm

你可能感兴趣的:(结构体,内存对齐)