转载自https://www.zhihu.com/question/27862634/answer/208895189
字节对齐主要是为了提高内存的访问效率,比如intel 32位cpu,每个总线周期都是从偶地址开始读取32位的内存数据,如果数据存放地址不是从偶数开始,则可能出现需要两个总线周期才能读取到想要的数据,因此需要在内存中存放数据时进行对齐。 因为处理器读写数据,并不是以字节为单位,而是以块(2,4,6,8,16字节)为单位进行的,如果不进行内存对齐,本来只需要一次进行的访问,可能要好几次才能完成。
我们假设处理器一次从内存中取四个字节,显然没有经过内存对齐的话,要取两次才能得到i。当结构体足够复杂的时候,效率明显降低。
通常我们说字节对齐很多时候都是说struct结构体的内存对齐,比如下面的结构体:
struct A{
char a;
int b;
short c;
}
在32位机器上char 占1个字节,int 占4个字节,short占2个字节,一共占用7个字节.但是实际真的是这样吗?
我们先看下面程序的输出:
#include <stdio.h>
struct A{
char a;
int b;
short c;
};
int main(){
struct A a;
printf("A: %ld\n", sizeof(a));
return 0;
}
测试输出的结果是A: 12, 比计算的7多了5个字节。这个就是因为编译器在编译的时候进行了内存对齐导致的。
内存对齐主要遵循下面三个原则:
- 结构体变量的起始地址能够被其最宽的成员大小整除
- 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节
- 结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节
其实这里有点不严谨,编译器在编译的时候是可以指定对齐大小的,实际使用的有效对齐其实是取指定大小和自身大小的最小值,一般默认的对齐大小是4。
再回到上面的例子,如果默认的对齐大小是4,结构体a的起始地址为0x0000,能够被最宽的数据成员大小(这里是int, 大小为4,有效对齐大小也是4)整除,姑char a的从0x0000开始存放占用一个字节即0x0000~ 0x0001,然后是int b,其大小为4,故要满足2,需要从0x0004开始,所以在char a后填充三个字节,因此a对齐后占用的空间是0x0000~ 0x0003,b占用的空间是 0x0004~ 0x0007, 然后是short c其大小是2,故从0x0008开始占用两个字节,即0x0008~ 0x0009。 此时整个结构体占用的空间是0x0000~ 0x0009, 占用10个字节,10%4 != 0, 不满足第三个原则,所以需要在后面补充两个字节,即最后内存对齐后占用的空间是0x0000~0x000B,一共12个字节。
struct A {
char a;
char b;
char c;
};
struct B {
int a;
char b;
short c;
};
struct C {
char b;
int a;
short c;
};
#pragma pack(2)
struct D {
char b;
int a;
short c;
};
根据三条法则我们来推断一下每个结构体的大小
A起始地址为0,最宽的成员变量大小就是1,可以被1整除。分别放在012的位置。
B起始地址为0,最宽成员大小为4,可以被4整除,int放在0123的位置,char从4开始,4可以被当前char的大小整除,short从5开始,5不能被short(2)整除,所以short从6开始,放在67。总的结构体大小为8,可以被最宽的4整除。
C起始地址为0,最宽的成员大小为4,可以被4整除。char放在0的位置,int本来应该从1开始,但是根据法则2,当前成员变量的起始地址应该可以整除当前成员变量大小,不能的话要在前一个成员变量后面补字节,所以在char后面补字节,一直补到3。int从4开始,占据4567,short从8开始,满足法则2,占据89。最后大小为10,不满足法则3总大小是最宽成员大小的整数倍,再补2,一共是12。
D#pragma pack(n)就是手动定义内存对齐系数,D结构体自定义为2,
如果n位对齐,那么n位以下的所有基本类型,地址必须为本类型长度的整数倍;
n位以上的基本类型,保持n位对齐。
所以D的起始地址为0,char占据的是0。int从1开始是不行的,因为int=4大于2,所以是n位以上的按照n位对齐,int从2开始,占据2345。最后short=2从6开始,占据67。最后大小是8.