概念及主要作用
内存对齐是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。那么何为适当的位置,这里就要简单介绍其出现的主要作用(本文就第二个原因进行主要讲解)
1、 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
例如:现在有一个int类型的数据存储与0x0003位置处,那么系统一次性就取到了这个数据,而如果你在0x0002位置处,系统一定会先取0x0000到0x0003拿到一半数据然后再从 0x0003后取出后半个数据,比之第一种方案要多一次数据获取,因此当我们的数据在进行存取的是后直接进行了与内存的对齐,那么我们在获取数据的时候就会得到更高的效率。
对齐规则
1、数据的对齐
在X86,32位系统下基于Microsoft、Borland和GNU的编译器,有如下数据对齐规则: char(占用1-byte)、short(占用2-byte)、int(占用4-byte)、long(占用4-byte)、float(占用4-byte)、double(占用8-byte)变量以8-byte对齐、long double(占用12-byte)、任意指针(占用4-byte)。
而在64位系统下,与上面规则对比有如下不同: long(占用8-byte)、double(占用8-byte)、long double(占用16-byte)、任意指针(占用8-byte)。
2、结构的对齐
在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,其对齐遵守以下原则
1)结构体变量的首地址是其最长基本类型成员的整数倍;
备注:编译器在给结构体开辟空间时,首先找到结构体中最大的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。并将这个最大的基本数据类型的大小作为上面介绍的对齐数。
2)结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节。
下面进行通过实例验证:
struct test
{
int a;
char b;
short c;
char d[6];
};
int main()
{
cout << "int:" << sizeof(int) << endl;
cout << "char:" << sizeof(char) << endl;
cout << "short:" << sizeof(short) << endl;
cout << "char[6]:" << sizeof(char[6]) << endl;
cout <<"struct test:"<< sizeof(test) << endl;
return 0;
}
这是我们得到了输出:
我们至少可以直接看到结构体整体的大小确实是大于内部类型大小直接相加的,当我们对这个结构体进行实例化并将所有成员都赋值为1,打开内存可以发现:
我们是知道这个结构体大小为16B的,那么上图的前四行都是结构体的内存。其中前4个字节为整型a,同时也是这个结构体中最大的元素(数组按照数组元素计算),所以首地址是4的整数倍,这是第一点;其次第二行中包括了char成员b、一个填充、一个short成员c,由于short需要自己所在的位置是自己大小的整数倍,因此进行了填充,这是第二点;最后一行中当6个char放置完成之后,由于整个结构体的大小需要为对其数(最大类型的大小)的整数倍,因此填充了两个B的大小,整个数组的大小16由此而来。