1. 内存对齐方式
虽然所有的变量最后都会保存在特定地址的内存中,但相应的内存空间必须满足内存对齐的要求。主要出于两个方面的原因:
(1) 平台原因:
不是所有的硬件平台(特别是嵌入式系统中使用的低端处理器)都能访问任意地址上的任意数据,某些硬件平台只能访问对齐的地址,否则会出现硬件异常。
(2) 性能原因:
如果数据存放在未对齐的内存空间中,则处理器访问变量时需要做两次内存访问,而对齐的内存访问仅需一次访问。
在32位微处理器中,处理器访问内存都是按照32位进行的,即一次读取或写入都是4个字节,比如地址0x0~0xF这16字节的内存,对于微处理器来说,不是将其看做16个单一字节,而是4个块,每块4个字节。如下图
显然,只能从0x0、0x4、0x8、0xC等地址为4的整数倍的内存中一次取出4个字节,并不能从任意地址开始一次读取4个字节。假定将一个占用4个字节的int类型的数据存放在地址0开始的4字节内存中,其示意图如下
由于int类型数据存放在块0中,因此CPU仅需一次内存访问即可完成对该数据的读取或写入。反之,如果将int类型数据存放在地址1开始的4字节内存空间中,其示意图如下
此时,数据存放在块0和块1两个块中,若要完成对该数据的访问,必须经过两次内存访问,先访问块0得到数据的3个字节,再通过访问块1得到该数据的1个字节,最后通过运算,将这几个字节合并为一个完整的int型数据。由此可见,若数据存储在未对齐的内存空间中,将大大降低CPU的效率。但在某些特定的微处理器中,它根本不愿意干这种事情,这种情况下,就出现系统异常,直接崩溃了。
2. 结构体的存储
我们知道,数组是相同类型有序数据的集合,但很多时候需要将不同类型的数据捆绑在一起作为一个整体对待,使程序设计更方便。在C语言中,这样一组数据被称为结构体。其内存对齐的规则如下:
(1) 结构体各个成员变量的内存空间的首地址必须是“对齐系数”和“变量实际长度”中较小者的整数倍。“对齐系数”是【#pragma pack指定的数值】、【未指定#pragma pack时,系统默认的对齐模数(32位系统为4字节,64位为8字节)】。假设要求变量的内存空间要求按照4字节对齐,则内存空间的首地址必须是4的整数倍,满足条件的地址为0x0, 0x4, 0x8, 0xC…
(2) 对于结构体,在其各个数据成员都完成对齐后,结构体本身也需要对齐,即结构体占用的总大小应该为“对齐系数”和“最大数据成员长度”中较小值的整数倍。
如下的结构体,在32位机器上编译,其成员数据的总长度为4+2+3+4+1+8 = 22(字节)
#pragma pack(4)
struct TEST
{
long a; //4
short b; //2
char c[3]; //3
float d; //4
char e; //1
double f; //8
}test;
有如下的测试程序
int main(void)
{
short len = sizeof(test);
printf("test length is %d.\n", len);
return 0;
}
下面是输出结果,结果表明结构体占用28个字节的内存,大于22字节。
其实,正是由于内存对齐的原因造成了这种现象,下图是每个变量在内存的分布图,其中空白部分是内存弃用部分,这些浪费空间的前面,存放的都是char型数据,由于char型数据只占用一个字节,往往使得其紧接着的空间不能被其它长度更长的数据使用。
为了降低内存浪费的概率,应该在char型数据之后,存放长度较小的成员。即在定义结构体时,应该按照长度递增的顺序依次定义各个成员。优化后的实例代码为
#pragma pack(4)
struct TEST
{
char e; //1
char c[3]; //3
short b; //2
long a; //4
float d; //4
double f; //8
}test;
运行测试程序,输出结果为
可见,通过调整结构体的成员顺序,达到了优化内存的目的。各个成员变量在内存中的分布图为
结构体只浪费了2个字节的空间,内存使用率达到92%。显然通过优化结构体成员的定义顺序,在同样满足内存对齐的要求下,可以大大减小内存的浪费。