为什么要进行内存对齐?
比如CPU是32位的,那么就有32根地址线,32根数据线,取数时,CPU的32根据地址线与内存的0-3号地址对齐,CPU的32位的数据线也同样,一个读取周期只能取这0-3地址的3个字节。如果你是取3-4地址的数据,CPU会自动把它分解成2次取数据操作,一次取8位的3单元和一次取8位4单元数据。只有开始地址是0、4、8...的32位的数据操作才能一次操作完成,内存不支持从1号单元开始的4字节读,CPU和内存的数据线必须相应数据线对齐才行。
换句话说,如果操作1字节的数据,可以是任意地址,如果是操作2字节的数据,如果开始地址在偶数地址,一次就可以取2字节,如果开始地址在奇数,就要2次内存操作才能完成;如果操作4字节的数据,最好开始地址在能被4整除的数值上,这样可以用一条32位的内存操作指令完成。同样,8字节的开始位置最好的能被8整除的数值上,这样可以用一条64位的内存操作指令完成。
也就是说,如果对齐了,一次就可以完成,不对齐,就可能多次才能完成。
另一方面,编译器编译程序处理时也会有对齐处理,一般的结构体和对象等都有对齐的处理(把结构体或对象的开始位置定在边界上),这样,为了让结构体或者对象中的数据能够快速读取,编译器就会对这些数据进行内存对齐。
怎样进行内存对齐?
内存对齐原则:由数据成员本身大小以及#parama pack(n)的n值决定。有以下3个原则:
①第一个数据成员的偏移地址地址为最长数据成员长度的整数倍,即对齐长度为最长数据成员长度;(如果是含有虚函数的类,那么会在开头放置虚表指针,然后才是第一个数据成员)
②每一个数据成员相对于首地址的偏移量为该数据成员长度的整数倍,即对齐长度为该数据成员长度;(由于最长数据成员长度必定是每个数据成员长度的整数倍,因此②是兼容①的)
③所有数据成员对齐后,还要保证整个类/结构体/联合体的长度为最长数据成员长度的整数倍,即对齐长度为最长数据成员大小;
如果设置了#pragma pack(n),那么这3个原则将取各自原对齐长度和n中的较小值作为对齐长度。
内存对齐举例
class A
{
int x;
char y;
int z;
double p;
virtual void f();
};
在类A中声明了一个虚函数,因此类A的开头会放置一个虚表指针,大小为4个字节,如果不对齐,那么第一个数据成员x的偏移量就为4,但是这样就违背了对齐原则①,因此会在虚表指针后补齐4个字节,让第一个数据成员x的首地址偏移为8,因此x就占据了8~11;
此时第二个数据成员y的偏移就是12了,y的大小为1,因此是符合对齐原则的,y就占据了12;
此时第三个数据成员z的偏移就是13了,z的大小为4,不符合对齐原则②,因此需要补齐3个字节,使得z的偏移量为16,z就占据了16~19;
此时第四个数据成员p的偏移就是20了,p的大小为8,不符合对齐原则②,因此需要补齐4个字节,使得p的偏移量为20,p就占据了20~27;
此时所有数据相对于首地址来说占据的内存地址为0~27,大小为28个字节,根据对齐原则③,类中最大数据成员大小为8,因此还需要在最后补齐4个字节,因此实际类占据的内存地址为0~31,大小为32个字节。
关于此处的类A的内存分布可在VS-项目-属性-命令行中输入/d1 reportSingleClassLayoutA (查看全部分布情况用/d1 reportAllClassLayout),然后运行程序后查看,如图所示。