内存对齐及结构体大小--最通俗易懂的讲解

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. 对齐值

  1. 数据类型自身的对齐值:为指定平台上基本类型的长度。对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
  2. 结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
  3. 指定对齐值:#pragma pack (value)时的指定对齐值value。
  4. 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。

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

你可能感兴趣的:(【C++】)