出于速度和空间的考量,编译器在实现过程中均会采用对struct内的变量进行内存对齐的操作,虽然会有一定的空间浪费,却可以减少在读取数据时候的读取操作。
先看下面的例子
struct A
{
char a;
int b;
};
int main()
{
cout << "size of A:"<<sizeof(A)<<"bytes" << endl;
return 0;
}
输出的结果是8字节。
对于这样的结构体,在内存中有两种存储方式。
|| 1B || 2B || 3B || 4B || 5B || 6B || 7B || 8B ||
1. || a || ------------b----------- ||
2. || a || || ------------b----------- ||
对于第一种存储方案,6、7、8三个字节的存储空间还可以留给后续使用。但注意,当我们要读取b的值的时候,32位的CPU会先读取1-4字节,再读取5-8字节,然后进行拼接得到b的值。而第二种方案,虽然会浪费掉2-4这三个字节,但读取b的值的时候,只需要一次读取操作就可以完成。
了解了内存对齐的基本思想,我又产生了三个新的疑问。一是,内存对齐与结构体内变量声明的顺序是否有关。二是,进行内存对齐的对齐标准是什么,是怎样确定的对齐标准。三是,如果对于64位机器,那么上面的第一种方案无疑是可取的,但实验表明,64位机器上(64位编译器)仍然采取了方案二进行对齐。
对于第一个疑问,看代码:
struct A
{
char a,c;
int b;
};
struct B
{
char a;
int b;
char c;
};
int main()
{
cout << "size of A:"<<sizeof(A)<<"bytes" << endl;
cout << "size of B:"<<sizeof(B)<<"bytes" << endl;
return 0;
}
输出的结果是A为8字节,B为12字节。这说明,在结构体内声明变量的顺序会影响结构体的内存分配。编译器在处理时,是按顺序进行内存分配和对齐的。
对于疑问二,在别人的博文中(http://blog.csdn.net/kokodudu/article/details/11918219),我看到了一个关于对齐的概念:
这里面有四个概念值:
1)数据类型自身的对齐值:基本数据类型的自身对齐值。
对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小的那个值。
概念看着比较多,但对于我们实际使用来说,可以进行一个简单不精确的概括。首先,平时写代码时,很少会用到指定对齐值,所以不用关注概念2和概念4 。而对于概念1和概念3,可以简单的理解为,结构体会按照其内部所包含的最长的基本类型成员(包括成员结构体的成员)的长度进行对齐。
下面看一个例子。
struct A
{
char a[5];
};
struct B
{
A a;
int b;
};
int main()
{
cout << "size of A:"<<sizeof(A)<<"bytes" << endl;
cout << "size of B:"<<sizeof(B)<<"bytes" << endl;
return 0;
}
输出的结果是A是5字节,B是12字节。
分析起来很简单,因为A中最长的基本数据类型是char,所以按1字节进行对齐。B中最长的基本数据类型是int,所以按4字节进行对齐。
但是这个概念里忽略了几个基本数据类型,枚举、指针。下面我们就来测试一下这几个类型的对齐长度。
enum e{};
struct A
{
e a;
};
struct B
{
int* a;
};
int main()
{
cout << "size of A:"<<sizeof(A)<<"bytes" << endl;
cout << "size of B:"<<sizeof(B)<<"bytes" << endl;
return 0;
}
输出结果对于A,结果是4字节。对于B,32位编译器的结果是4字节,64位编译器的结果是8字节。
对于疑问三,实际上这个疑问是有问题的。因为虽然在一个存取周期内CPU把数据读入了CPU之中(假设读入到寄存器RAX中),我们需要对RAX进行移位操作才能使得EAX寄存器中恰好取得int变量b的值。
最后,概括起来,借用《Windows核心编程》里的话来总结结构体进行对齐的根本原因:当CPU访问正确对齐的数据时,它的运行效率最高,当数据大小的数据模数的内存地址是0时,数据是对齐的。