先说如何对齐,再讲讲其背后的原理。对齐规则在网上和书上都很容易找到。无非就是以下几点。
规则
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
(VS中默认的值为8)
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍
处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数
倍。
5、结构体中定义变量时最好占用字节数从小到大依此定义,可以使得结构体占用空间最小。
用几个例子来解释几条规则:
struct st{
int8_t b;
int32_t a;
char c;
float d;
char e;
};
根据第一条规则,int8从偏移量为0的地址开始存。
根据第二条规则,int32是四个字节大小,所以对齐数是4。所以他开始的地址应该是4的整数倍,依次存储的话,那就只能从4位置开始了,中间有三个空格用于对齐。接着存char,char占一个字节,所以对齐数是1,前面已经用了8个字节了,8是1的倍数,所以紧接着存放char.最后存float,float占4个字节,最大对齐数4,abc三个变量物理存储已经连续占用了了9(1+3+4+1)个字节,9不是4的倍数,最近的12是4的倍数,所以float从偏移量12位置开始存。最后一个char占一个字节,对齐数1,目前已经连续占用的物理空间是(1+3+4+1+3+4)16个,16是1的倍数,所以紧接着存char。
根据第三条规则,目前已经连续占用的物理空间是(1+3+4+1+3+4)17个字节。st结构体中最大对齐数是4,17不是4的倍数,最近的20是4的倍数。所以该结构体最后又占用了3个空间。
最后总结,一共占用了20个字节。
下图一个空格表示一个字节
接下来讲嵌套
struct sstt{
char a;
struct {
char b;
int c;
} pp;
float d;
};
先看pp结构体占用几个字节,b占用1个字节,占用4个字节,但是1不是4的整数倍,所以中间空了3个字节,一共8个字节,这个结构体中最大对齐数是int c(4个字节),8是4的倍数。最后得出pp一共占用8个字节。
根据第四条规则、然后再看sstt,char a占一个字节,但是pp从哪里开始呢。pp结构体中最大对齐数是int c(4个字节),所以要从4的倍数开始存,那就是char a后面再空3个空格存pp,此时1+3+8,到目前为止一共占用了12个字节,接着存float,float占4个字节,即接着存float.所以一共是16个字节。最后整结构体包括嵌套结构体中最大的对齐数是4,而16是4的倍数,正好。
最后总结,一共占用了16个字节。
如果结构体中的成员包括数组或者其他结构体,在数据对其时,要以结构体中最深层的基本数据类型为准。即看数组存的什么数据类型。
借此机会我们一起把第五条规则讨论一下。
struct sstt{
char a;
int b[2][3];
char c;
};
struct sstt1{
int b[2][3];
char a;
char c;
};
sstt结构体,char占一个字节,b基本类型是int占4个字节,所以要空3个字节再存b,b一个是2*3*4即24个字节,目前为止一共1+3+24=28个字节,c占1个字节,正好是28倍数,所以紧接着再存c,目前一共占有29个字节。由于 最大对齐数是4,即一个int大小,29不是4的倍数,补3个空字节。最2后得出占29+3=32个字节。
sstt1结构体,b占2*3*4=24字节,a占1个字节,24是1的倍数,所以紧接着存a,那目前位置一共占用1+24=25字节。同理再存c。所以目前为止占26个字节,但是最大对对齐数是4,即一个int大小,所以要再补2个字节,最有一共占28个字节。
所以得出第五条规则,按从小到大定义变量,占用字节最少。
为什么???????要这么浪费空间?????
因为在32位操作系统(虽然64位操作系统,但是为了保证兼容性,编程仍然主要考量32位)中,数据总线是32位,地址总线是32位。地址总线是32位,意味着寻址空间是按4递增的;数据总线32位意味着一次可读写4byte。
想要具体了解什么是地址总线和数据总线可以看下面链接
(44条消息) 8086/8088CPU内部结构,引脚图,物理地址与逻辑地址_Always Fighting的博客-CSDN博客_8088cpu
简单介绍一下, 以8086CPU为例,注意这是一个16位的芯片,而我们大多数使用的是32或64位cpu
地址/数据分时复用引脚AD15-AD0(39,2-16):传送地址时单向输出,传送数据时双向输入或输出。地址和数据公用引脚,分时复用。这16个引脚是一起工作的。
视线拉回我们32位cpu,32位/8位=4字节,所以cpu一次工作可以取到4个字节的数据。那以读取的角度排布我们内存的话可以像下面这样。一次cpu读一行数据。
如果下面的结构体不对齐会发生什么事情,就像上面中间那张图,如果需要读取到完整的i需要读取两次(或者说两行)然后拼接再一起,I/O操作是很耗时的,这是很浪费时间的。如果需要更快读到数据,那一个数据最好是存在一整行,像上面第三张图那样。
struct stu1
{
char c1;
int i;
char c2;
}
可见如果想要存一个int类型的话,那int变量的偏移量必须是4的倍数,如果是一个double或者 long long型那偏移量一定是8的倍数。即向下面的图。int类型变量可以从0、4、8、12位置开始存,short int类型可以从0、2、4、6、8、10、12、14开始存,double可以从0,8位置开始存。可能你对double类型有疑问,为什么4不行,反正double(8位)占两行,在哪都在占,我想这可能是考虑到64位机器,64位一次可以读8个字节,如果存4位置肯定会遇到换行,像下面第二张图。
你可能还在想为什么不直接全部按4的倍数偏移,那可以看下面两张图。这两个方案你会选哪个。很明显是B,一次就可以读出s和c两个数据,而A读取s和c两个数据需要两次。不仅省内存还省时间。
总结对齐原因
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据
的;某些硬件平台只能 在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在
于,为了访问未对齐的 内存,处理器需要作两次内存访问;而对齐的内存访问
仅需要一次访问。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。优点是提高了可移植性和cpu性能