1. 什么是字节对齐?
字节(Byte)是计算机信息技术用于计量存储容量和传输容量的一种计量单位,一个字节等于8位二进制数,在UTF-8编码中,一个英文字符等于一个字节。字节按照一定规则在空间上排列就是字节对齐。
CPU在读取内存地址的时候,一定按照一定的偏移量去读取,不知道你发现了没有,我们没有看到一个变量的大小是 3 个字节的,都是 1 个字节,2个字节,4个字节,8个字节,16个字节,32个字节。
为什么会这样呢?因为CPU设计的时候,没有一个 3 、5、7、9这样的模子,因为设计这样的模子非常费劲。
2. 字节对齐的作用及原因
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。
之前网上有一个一个例子,如果一个变量int 的起始地址偏移是3,那么CPU要取这个地址上的数据,需要取两次,为什么呢?
假设一个变量 在内存的位置 从地址 1开始存放数据,因为这个是int类型,它占用4个字节的内存空间。
我们用一个int 的模子「int模子是4个字节」来卡这个数据,实际上是这样操作的,第一次卡模子,只能从0开始
第二次卡模子,再从3位置开始
从图片上可以明显看出来,我们需要CPU卡两次模子,才取到在内存里面的 int 变量
如果int 是按照内存对齐的方式存放的呢?
很明显,我们只需要卡一次模子就可以取到数据了。
3. 对齐值
- 数据类型自身的对齐值:为指定平台上基本类型的长度。对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
- 结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
- 指定对齐值:#pragma pack (value)时的指定对齐值value。
- 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
4. 对齐原则
- 标准数据类型:它的地址只要是它的长度的整数倍就可。
- 数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
- 联合 :按其包含的长度最大的数据类型对齐。
- 结构体: 结构体中每个数据类型都要对齐。
当数据类型为结构体时,编译器可能需要在结构体字段的分配中插入间隙,以保证每个结构元素都满足它的对齐要求。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放(对于非对齐成员需要在其前面填充一些字节,保证其在对齐位置上),结构体本身也要根据自身的有效对齐值圆整(就是结构体总长度需要是结构体有效对齐值的整数倍),此时可能需要在结构末尾填充一些空间,以满足结构体整体的对齐—-向结构体元素中最大的元素对齐。
通过上面的分析,对结构体进行字节对齐,我们需要知道四个值:
- 指定对齐值:代码中指定的对齐值,记为packLen;
- 默认对齐值:结构体中每个数据成员及结构体本身都有默认对齐值,记为defaultLen;
- 成员偏移量:即相对于结构体起始位置的长度,记为offset;
- 成员长度:结构体中每个数据成员的长度(注结构体成员为补齐之后的长度),记为memberLen。
及两个规则:
- 对齐规则: offset % vaildLen = 0,其中vaildLen为有效对齐值(很重要)vaildLen = min(packLen, defaultLen);
- 填充规则: 如成员变量不遵守对齐规则,则需要对其补齐;在其前面填充一些字节保证该成员对齐。需填充的字节数记为pad
一般地,可以通过下面的方法来改变缺省的对界条件:
- 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
- 使用伪指令#pragma pack (),取消自定义字节对齐方式。
5. 怎么求struct结构体的大小?
总述:
求有效对齐值,如成员变量不遵守对齐规则,则需要对其补齐;在其前面填充一些字节保证该成员对齐。
第一步:求结构体内成员变量的有效对齐值
- 结构体成员变量的有效对齐值为:min(代码中指定的对齐值,结构体成员变量的默认对齐值)
如果代码中未指定对齐值,则按默认的,一般Linux64位8字节对齐,Windows是4字节对齐。
成员变量的默认对齐值,也和不同环境也不一样,一般情况下是,Linux64的long为8字节,Windows为4字节;int,char,short,Windows和Linux一样,其它自测。
- 对齐规则:相对于结构体起始位置的长度 % 有效对齐值 = 0。
对每个结构体成员求有效对齐值,然后根据对齐规则,不满足就在前面填充直到满足即可。
第二步:求结构体的有效对齐值
- 求结构体的有效对齐值为:min(代码中指定的对齐值,结构体成员变量中最大的默认对齐值)
如果代码中未指定对齐值,则按默认的,一般Linux64位8字节对齐,Windows是4字节对齐。
成员变量的默认对齐值,也和不同环境也不一样,一般情况下是,Linux64的long为8字节,Windows为4字节;int,char,short,Windows和Linux一样,其它自测。
- 对齐规则:相对于结构体起始位置的长度 % 有效对齐值 = 0。
对结构体求有效对齐值,然后根据对齐规则,不满足就在后面填充直到满足即可。
最好自己写代码实践一下看看。
6. 什么时候需要设置对齐?
在网络协议编程中,经常会处理不同协议的数据报文。一种方法是通过指针偏移的方法来得到各种信息,但这样做不仅编程复杂,而且一旦协议有变化,程序修改起来也比较麻烦。在了解了编译器对结构空间的分配原则之后,我们完全可以利用这一特性定义自己的协议结构,通过访问结构的成员来获取各种信息。这样做,不仅简化了编程,而且即使协议发生变化,我们也只需修改协议结构的定义即可,其它程序无需修改,省时省力。
7. 结构体举例
7.1 栗子1
struct test {
char a;
short b;
int c;
short d;
};
首先要求的就是有效对齐值(重点):
未指定对齐值,在linux64位下缺省按8字节对齐。
- char a:1字节,默认对齐值为1字节,所以min(8,1)为1,即char a的有效对齐值为1字节,放在结构体的起始地址,根据对齐规则 1 % 1 = 0,所以不用填充。
- short b:2字节,默认对齐值为2字节,所以min(8,2)为2,即short b的有效对齐值为2字节,根据对齐规则 (1+2) % 2 != 0,所以在short b 前面填充1字节。
- int c:4字节,默认对齐值为4字节,所以min(8,4)为4,即int c的有效对齐值为4字节,根据对齐规则 (4+4) % 4 = 0,无需填充。
- short d:2字节,默认对齐值为2字节,所以min(8,2)为2,即short d的有效对齐值为2字节,根据对齐规则 (8 + 2) % 2 = 0,无需填充。
- test结构体:10字节,最大成员是int c占4字节,所以默认对齐值为4字节,所以min(8,4)为4,即test结构体的有效对齐值为4字节,根据对齐规则 10 % 4 != 0,需要填充2字节。最终结构体大小为12字节
编译后结构struct test的布局如下:
运行程序结果为:
size of test = 12
7.2 栗子2
struct test2 {
int a;
long b;
char c;
};
未指定对齐值,在linux64位下缺省按8字节对齐。
- int a:4字节,默认对齐值为4字节,所以min(8,4)为4,即int a的有效对齐值为4字节,放在结构体的起始地址,根据对齐规则 4 % 4 = 0,所以不用填充。
- long b:8字节,默认对齐值为8字节,所以min(8,8)为8,即long b的有效对齐值为8字节,根据对齐规则 (4+8) % 8 != 0,所以在long b 前面填充4字节。
- char c:1字节,默认对齐值为1字节,所以min(8,1)为1,即char c的有效对齐值为1字节,根据对齐规则 (16+1) % 1 = 0,无需填充。
- test2结构体:17字节,最大成员是long b占8字节,所以默认对齐值为8字节,所以min(8,8)为8,即test结构体的有效对齐值为8字节,根据对齐规则 17 % 8 != 0,需要填充7字节。最终结构体大小为24字节
注意:成员变量对齐后,还要考虑结构体本身
其实如果就这一个就来说它已将满足字节对齐了,因为它的起始地址是0,因此肯定是对齐的,之所以在后面补充7个字节,是因为编译器为了实现结构数组的存取效率,试想如果我们定义了一个结构test2的数组,那么第一个结构起始地址是0没有问题,但是第二个结构呢?按照数组的定义,数组中所有元素都是紧挨着的,如果我们不把结构的大小补充为8的整数倍,那么下一个结构体显然不能满足结构的地址对齐了,因此我们要把结构补充成有效对齐大小的整数倍。
其实诸如:对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,这些已有类型的自身对齐值也是基于数组考虑的,只是因为这些类型的长度已知了,所以他们的自身对齐值也就已知了。
在linux64位centos(默认按8位对齐)上编译编译后结构struct test2的布局如下:
运行程序结果为:
size of test2 = 24
7.3 栗子3
不妨将结构体struct test2里面成员的顺序重新排列一下:
struct test3 {
char c;
int a;
long b;
};
在64位centos上编译编译后结构struct test3的布局如下:
运行结果为:
size of test3 = 16
可见适当地编排结构体成员地顺序,可以在保存相同信息地情况下尽可能节约内存空间。
7.4 栗子4
#pragma pack (2) /*指定按2字节对齐*/
struct test4 {
char a;
int b;
short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
指定对齐值2字节。
- char a:1字节,默认对齐值为1字节,所以min(2,1)为1,即char a的有效对齐值为1字节,放在结构体的起始地址,根据对齐规则 1 % 1 = 0,所以不用填充。
- int b:4字节,默认对齐值为4字节,所以min(2,4)为2,即int b的有效对齐值为2字节,根据对齐规则 (1 + 4) % 2 != 0,需要填充1字节。
- short c:2字节,默认对齐值为2字节,所以min(2,2)为2,即short d的有效对齐值为2字节,根据对齐规则 (6 + 2) % 2 = 0,无需填充。
- test4结构体:8字节,最大成员是int b占4字节,所以默认对齐值为4字节,所以min(2,4)为2,即test结构体的有效对齐值为2字节,根据对齐规则 8 % 2 = 0,无需填充。最终结构体大小为8字节。
7.5 栗子5
#pragma pack(4)
struct test5
{
char a; //1
char b[3]; //3
char c; //1
};
#pragma pack()
很多人认为最后结构体大小为8,我们来按步骤求一下就知道了。
首先指定对齐值4字节。
- char a:1字节,默认对齐值为1字节,所以min(4,1)为1,即char a的有效对齐值为1字节,放在结构体的起始地址,根据对齐规则 1 % 1 = 0,所以不用填充。
- char b[3]:3字节,默认对齐值为1字节,所以min(4,1)为1,即char b[3]的有效对齐值为1字节,根据对齐规则 4 % 1 = 0,所以不用填充。
- char c:1字节,默认对齐值为1字节,所以min(4,1)为1,即char c的有效对齐值为1字节,根据对齐规则 (4 + 1) % 1 = 0,无需填充。
- test5结构体:5字节,最大成员是所占1字节,所以结构体默认对齐值为1字节,所以min(4,1)为1,即test结构体的有效对齐值为1字节,根据对齐规则 5 % 1 = 0,无需填充。最终结构体大小为5字节。
可以看到当#pragma pack的值等于或超过最长数据成员的长度的时候,这个值的大小将不产生任何效果。所以上面的#pragma pack(4)是没有意义的。
数组类型可以看成多个类型的叠加,比如char b[3]可以看成:char x,char y, char z,三个变量。
test5结构体的成员变量可以看成是char a[5]一个变量。
7.6 栗子6
struct test6 {
int a;
long b;
};
struct test7 {
char a;
test6 b;
int c;
};
结构体内含有结构体变量时,还是按上面的步骤就行,可以把结构体变量当成一个新的数据类型即可(不会将结构体的内容展开计算,是当成一个整体)。
先看test6:
未指定对齐值,在linux64位下缺省按8字节对齐。
- int a:4字节,默认对齐值为4字节,所以min(8,4)为4,即int a的有效对齐值为4字节,放在结构体的起始地址,根据对齐规则 4 % 4 = 0,无需填充。
- long b:8字节,默认对齐值为8字节,所以min(8,8)为8,即long b的有效对齐值为8字节,根据对齐规则 (4 + 8) % 8 != 0,需填充4字节。
- test6结构体:16字节,最大成员是long b占8字节,所以默认对齐值为8字节,所以min(8,8)为8,即test6结构体的有效对齐值为8字节,根据对齐规则 16 % 8 = 0,无需填充。最终结构体大小为16字节。
再看test7:
未指定对齐值,在linux64位下缺省按8字节对齐。
- char a:1字节,默认对齐值为1字节,所以min(8,1)为1,即char a的有效对齐值为1字节,放在结构体的起始地址,根据对齐规则 1 % 1 = 0,无需填充。
- test6结构体:16字节,默认对齐值为16字节,所以min(8,16)为8,即test6结构体的有效对齐值为8字节,根据对齐规则 (16+1) % 8 != 0,需填充7字节。
- int c:4字节,默认对齐值为4字节,所以min(8,4)为4,即int c的有效对齐值为4字节,根据对齐规则 (24+4) % 4 = 0,无需填充。
- test7结构体:28字节,最大成员是test6 b占16字节,所以默认对齐值为16字节,所以min(8,16)为8,即test7结构体的有效对齐值为8字节,根据对齐规则 24 % 8 != 0,需填充4字节。最终结构体大小为32字节。
在64位centos上编译编译后结构struct test6的布局如下:
在64位centos上编译编译后结构struct test7的布局如下:
参考:
https://blog.csdn.net/cclethe/article/details/79659590#fn:5
https://mp.weixin.qq.com/s?__biz=MzA5NTM3MjIxMw==&mid=2247485668&idx=1&sn=a65c63a03ecca1cd304b52e6a35fd1a0&chksm=90411e3ea73697285843f44debca6ff26a2629d01c61a518c2d5cfca1dd6e4c632f634763687&token=35003462&lang=zh_CN#rd