这一切要从机器字长和存储字长说起。机器字长是CPU每次处理的二进制的位数;存储字长是内存中一个存储单元的包含二进制位数,或一次内存读写操作的位数,也可以理解成数据线的根数。
下面以机器字长和存储字长都是32位的机器为例,来说明为什么要内存对齐。我们知道内存是以字节(byte)来编址的,32位也就是4个字节。由于内存的读写单位是存储单元,所以CPU对内存进行读写时,发送给它的地址必须是存储单元长度的倍数,即存储字长的倍数。我们这里的存储字长是4 bytes,所以这个地址必须是4n。假设现有一个大小为4 bytes的int类型的值,如不考虑对齐,它在内存中的存储有以下4种情况:
图 1
case 1时,处理器只需要读一个存储单元,就可以直接得到int值; case 2、3、4时,就需要读取两个内存单元,并且要经过一系列处理后才能得到所需的值,这种情况下效率是很低的。而这种糟糕情况,对于int类型来说,发生的概率是75%。若果不对齐,CPU对内存的读写是很低效的。
在讨论对齐规则之前,先把我用来测试的环境说一下。编译器VC6.0,操作系统Win7,机器字长、存储字长32(4bytes)。此环境下基本数据的大小为:sizeof(char)=1;sizeof(short)=2; sizeof(int)=4; sizeof(double)=8。
struct T
{
int a;
int b;
};
T的对齐大小为sizeof(int)=4,存储大小是2*4=8。
以下是内存对其的规则,规则一:数据类型的起始地址必须为其对齐大小的整数倍。
规则二:复合类型的的存储大小为其对齐大小的整数倍。
规则三:复合类型包含复合类型的时候,子结构按一个整体对待,存储大小不变。
在基本数据类型的大小都是2整数幂的前提下,这个规则满足基本原则的要求。幸运的是C++的基本数据类型确实全都是2的整数幂(bool类型的提升为一个字节处理),VC6.0在默认情况下,也确实用的这一规则。这一个规则的好处是,不必关心存储字长W,就可以满足16、32、64乃至128位存储字长下,内存对齐的要求。以下通过例子进行说明。
例1.
#include<iostream>
using std::cout;
using std::endl;
struct A
{
char a;
int b;
short c;
};
int main()
{
A testA;
cout<<sizeof(A)<<endl;
cout<<(void*)&testA.a<<endl;//将char*转成void*是必要的,不然会当成字符串输出
cout<<&testA.b<<endl;
cout<<&testA.c<<endl;
return 0;
}
程序运行结果:
可以看出类型A占12 bytes,下面三行是各个数据成员的起始地址。每次运行得到的这些地址可能不同,不过无妨,我们只关心它们之间的偏移。若是把这些地址都减去最小的那个,我们得到数据在内存中存放的示意图,如下:
图 2
在0位置写入char a后,这一存储单元还剩3 bytes,接下来是int b。 int的对齐大小为4 bytes,所以他的起始位置必须为4n,a后面3 bytes的内存不符合这个条件,于是跳过(这个空间会由系统填充的)。而紧随其后的4位置满足了4的倍数的条件,便可以将b写入此处。下一个要存储的是shortc,而b后的位置8满足规则一,可以写入c。三个元素都写完了,用掉了10bytes。我们知道A中元素最大的对齐大小为4,因此A的对齐大小便是4,10bytes不满足规则二,于是补上两个凑足12(=3*4)bytes。
以下例子中,为了节省篇幅,突出重点,我只粘贴程序中主要的部分,相信关心内存对齐问题的朋友,可以明白我在说什么。
例2.
struct B
{
char a;
short b;
int c;
};
B testB;
cout<<sizeof(B)<<endl;
cout<<(void*)&testB.a<<endl;
cout<<&testB.b<<endl;
cout<<&testB.c<<endl;
运行结果:
类型B和例1中的A其实是一样的,只是改变了内部元素的声明顺序,得到的结果便发生了改变,最明显的就是它节省了4 bytes。根据输出信息,我们得到B的数据在内存中存放的示意图:
根据规则一,把数据元素排列后,用去8 bytes。这个大小正好满足规则二,也就不再需要补充了,所以B的存储大小是8 bytes。
例 3.struct C
{
char a;
double b;
char c;
int d;
char e;
};
C testC;
cout<<sizeof(C)<<endl;
cout<<(void*)&testC.a<<endl;
cout<<&testC.b<<endl;
cout<<(void*)&testC.c<<endl;
cout<<&testC.d<<endl;
cout<<(void*)&testC.e<<endl;
运行结果:
数据在内存中存放的示意图:
与以上例子一样,先根据规则一将数据安排好,由于此结构中最大元素doubleb的大小为8 bytes,按8的倍数补齐,C的大小即为32 bytes。不过仔细一数,发现实际用到的字节数只有15 bytes,而浪费掉的却有17 bytes之多。现在内存容量都比较大,也许不需要太计较,但作为一个以勤俭节约为传统美德的民族的子民,看到这耀眼的空白,总有点诚惶诚恐的感觉。根据结构A和B的经验,我们调整一下结构。
例4.
struct D
{
char a;
char b;
char c;
int d;
double e;
};
D testD;
cout<<sizeof(D)<<endl;
cout<<(void*)&testD.a<<endl;
cout<<(void*)&testD.b<<endl;
cout<<(void*)&testD.c<<endl;
cout<<&testD.d<<endl;
cout<<&testD.e<<endl;
运行结果:
数据在内存中存放的示意图:
图5
与图4相比,这个是不是看上去好了很多。不错,它确实节省了一半的内存。通过这几个例子我们可以得到一个经验,在设计新复合类型的时候,我们应尽量把相同类型的数据放在一块;不同类型的数据之间按存储大小递增或递减的方式排列,这样就可以保证大多数情况下浪费的空间最少。
下面说说结构中包含结构的情况。例5.
struct subA
{
char a;
short b;
short c;
};
struct subB
{
char a;
double b;
};
subA testsubA;
subB testsubB;
cout<<"sizeof(subB):"<<sizeof(subA)<<endl;
cout<<"address of testsubA.a:"<<(void*)&testsubA.a<<endl;
cout<<"address of testsubA.b:"<<(void*)&testsubA.b<<endl;
cout<<"address of testsubA.c:"<<(void*)&testsubA.c<<endl;
cout<<"\nsizeof(subA):"<<sizeof(subB)<<endl;
cout<<"address of testsubB.a:"<<(void*)&testsubB.a<<endl;
cout<<"address of testsubB.b:"<<(void*)&testsubB.b<<endl;
运行结果:
由此可到subA和subB的数据在内存中的示意图:
图 6
在此基础上定义
struct E
{
subA a;
char b;
int c;
subB d;
};
E testE;
cout<<"sizeof(testE):"<<sizeof(testE)<<endl;
cout<<"address of testE.a.a:"<<(void*)&testE.a.a<<endl;
cout<<"address of testE.a.b:"<<(void*)&testE.a.b<<endl;
cout<<"address of testE.a.c:"<<(void*)&testE.a.c<<endl;
cout<<"\naddress of testE.b:"<<(void*)&testE.b<<endl;
cout<<"address of testE.c:"<<(void*)&testE.c<<endl;
cout<<"\naddress of testsubB.d.a:"<<(void*)&testE.d.a<<endl;
cout<<"address of testsubB.d.b:"<<(void*)&testE.d.b<<endl;
运行结果:
E在内存中的结构示意图为:
图 7
可以看出,给E分配存储空间时,先在0位置安放subA a,占去了6 bytes。接下来安排char b和int c,在安排subB之前共用去了12 bytes。根据规则三,subB按整体对待。由于它的对齐大小是8,起始地址必须是8n,于是跳过4bytes在位置16安放,占去sizeof(subB)=16个字节。至此,E占用了32bytes, E的对齐大小等于subB的对齐大小为8,32=4*8,满足规则二,不需要补齐,这就得到了E的存储大小32bytes。
到这里,VC6.0的默认对齐方式就算说完了。也许你发现,subB的存储浪费了将近50%的内存,而我们也确实没办法进一步优化它。如果你的程序注重空间而不太要求效率的话,这确实会让你很难受。幸运的是,好的编译器总会提供你更多的选择。预编译命令#pragma pack(n)可以帮你解决这一问题,它允许你设置数据的对齐大小。编译器建议你这个n值取2的整数幂,不然它会给你一个警告,然后不理你的设置。即便按要求设置了n,编译器也不一定会用,它真正使用的值是,你设置的n和类型默认对齐大小中较小的一个,即min{n,类型的默认对齐大小}。是不是看着很绕,没关系,先看个例子。
例6.
#include<iostream>
using std::cout;
using std::endl;
#pragma pack(2)//语句1
struct F
{
short a;
int b;
char c;
};
int main()
{
F testF;
cout<<sizeof(F)<<endl;
cout<<&testF.a<<endl;
cout<<&testF.b<<endl;
cout<<(void*)&testF.c<<endl;
return 0;
}
不加语句1时运行的结果:
加上语句1后运行的结果:
两种情况在内存中的示意图如下:
设置对齐大小前的就不解释了。设置了对齐大小为n=2后,先在0位置分配了short a。之后分配int b,此时min{n=2,sizeof(int)=4}=2,所以int的对齐大小为2,因为位置2满足了规则一,所以可以在此存储b。然后分配char,min{n=2,sizeof(char)=1}=1,也就是说char的对齐大小没有改变,直接存储就行,最后补成2的倍数,便得sizeof(F)=8.
OK,这就我理解的全部内容。鉴于本人水平有限,且对底层不甚了解,文中难免出现这样那样的错误,还望高手雅正。