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