为什么要进行内存对齐?
这是因为CPU的读取总是对齐的。举个例子,假设CPU是32位的,那么CPU每次读取的4字节数据的首地址都是4的倍数,也就是说,内存中数据首地址为4的倍数时,CPU一次操作就可以完成数据读取:
假设有一个int型四字节大小的变量存放在0~3地址单元中,那么CPU只需要读取0~3这32位即可,而如果这个变量存放在2~5地址单元中,那么CPU就需要先取出0~3地址单元,去掉不需要的0~1,再取出4~7地址单元,去掉不需要的6~7,最后把剩下的2~3和4~5组合在一起才是需要的变量值,这样一来效率就降低了。
换句话说,如果操作1字节的数据,可以是任意地址,如果是操作2字节的数据,如果开始地址在偶数地址,一次就可以取2字节,如果开始地址在奇数,就要2次内存操作才能完成;如果操作4字节的数据,最好开始地址在能被4整除的数值上,这样可以用一条32位的内存操作指令完成。
也就是说,如果对齐了,可以提高读取效率。内存对齐是为了配合CPU更效率的进行数据读取,往往都是由编译器来完成:对于一般的变量(不仅仅是结构体变量和对象),都会让它们的首地址放在对齐边界上(在VS中可以看到,对于char型单字节大小的变量,其地址是任意的;对于short型双字节大小的变量,其地址只可能是偶数;而对于int型四字节大小的变量,其地址则只可能是4的倍数),而对于结构体成员和类的成员变量,也会把它们放到对齐边界上。
怎样进行内存对齐?
内存对齐原则:由数据成员本身大小以及#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),然后运行程序后查看,如图所示。