一次讲清楚结构体大小的计算

一次讲清楚结构体大小的计算

结构体大小的计算,可分为三类:

  • 一般结构体
  • 含有嵌套的结构体的
  • 指定字节对齐数的结构体

在说明结构体大小计算方法之前,先介绍字节对齐。

字节对齐(Byte Alignment)在计算机系统中是一种优化机制,旨在提高内存访问的效率和系统性能。计算机硬件设计者,为了简化硬件电路,提高存储速度,因而硬件设计通常假定数据是对齐的。对齐的数据可以简化硬件的设计,减少电路的复杂性和功耗。未对齐的数据访问可能需要额外的逻辑来处理,从而增加硬件复杂度。

计算机硬件寻址,一般分为字节、字、双字、四字读取,这也会导致在对于一些特殊大小的数据单元读取时,可能需要分多次读取数据,例如假设一个未对齐的4字节数据单元。

| 地址 | 数据    |
|------|--------|
| 0x00 | a      |
| 0x01 | b (低字节) |
| 0x02 | b      |
| 0x03 | b      |
| 0x04 | b (高字节) |
| 0x05 | ...    |

在这种情况下,int b的地址是0x01,而它跨越了地址0x04。

如果int b是对齐的,例如,它开始于地址0x00或者0x04,那么处理器可以一次性读取完整的4字节数据。

如果int b是不对齐的,处理器需要执行以下步骤:

  1. 第一次读取:从地址0x01读取3个字节(0x01, 0x02, 0x03)。
  2. 第二次读取:从地址0x04读取1个字节(0x04)。

然后,处理器需要将这两次读取的数据组合起来,才能得到完整的int b的值。这会导致更多的内存访问操作和额外的处理开销,从而降低性能。因此需要内存对齐,但是这也会导致对于一些复杂数据类型,例如结构体、类,又该如何对齐呢?接下来详细分析

一般结构体的大小

计算原则

  1. 结构体变量的首地址,必须是结构体中“最大基本类型大小”的整数倍。
  2. 结构体中每个成员相对于首地址的偏移量,都是该成员大小的整数倍。
  3. 结构体的总大小,为结构体“最大基本类型”的整数倍。

样例分析

s1

struct s1{
  char ch1;
  char ch2;
  int i_val;
};

cout << "sizeof(s1) = " << sizeof(s1) << endl;
sizeof(s1) = 8

s1的大小:按照对齐原则,ch1占有1字节,ch2占有1字节,共2字节,然而i_val的出现,导致i_val的地址位置不满足条件2,因此在ch2后添加padding,以保证字节对齐。如下:

0x00 ch1    // 默认从0地址开始,可以视为相对偏移量,方便计算
0x01 ch2
0x02 padding
0x03 padding
0x04 i_val(i_val占有0x04-0x07)
0x05 i_val
0x06 i_val
0x07 i_val

一共8字节。

s2

struct s2{
  char ch1;
  int i_val;
  char ch2;
};
cout << "sizeof(s2) = " << sizeof(s2) << endl;
sizeof(s2) = 12

按照对齐原则,分析 s2 的内存布局:

  1. ch1 占 1 字节,偏移量 0x00。
  2. i_val 占 4 字节,需要对齐到 4 的整数倍,因此在 ch1 后面要填充 3 个字节的 padding。
0x00 ch1
0x01 padding
0x02 padding
0x03 padding
0x04 i_val (0x04 - 0x07)
0x05 i_val
0x06 i_val
0x07 i_val
0x08 ch2
0x09 padding
0x0A padding
0x0B padding

因为 s2 的最大基本类型是 int,它的大小必须是 4 的倍数。此时,s2 的大小为 12 字节。

s3

struct s3{
  char ch1;
  int i_val;
  char ch2;
};
cout << "sizeof(s3) = " << sizeof(s3) << endl;
sizeof(s3) = 8

按照对齐原则,分析 s3 的内存布局:

  1. i_val 占 4 字节,偏移量 0x00。
  2. ch1 占 1 字节,偏移量 0x04。
  3. ch2 占 1 字节,偏移量 0x05。
  4. 为了满足 4 字节对齐的要求,在 ch2 后面添加 2 个字节的 padding。
0x00 i_val (0x00 - 0x03)
0x01 i_val
0x02 i_val
0x03 i_val
0x04 ch1
0x05 ch2
0x06 padding
0x07 padding

因为 s3 的最大基本类型是 int,它的大小必须是 4 的倍数。此时,s3 的大小为 8 字节。

带数组的结构体

多提一嘴,带有数组的结构体大小计算,你可以将str展开为3个char数组,之后按照之前的方式计算,熟练之后可以不拆开。

struct s4{
  int i_val;
  char str[3];
};

等价于:

struct s4{
  int i_val;
  char str1;
  char str2;
  char str3;
};

计算后,结果为8字节大小,因为为了满足条件3,因此在str3后面添加了一字节的padding。

带嵌套结构体的大小计算

计算规则

规则和一般结构体的计算方式有一些不同,对于带嵌套的结构体,需要将子结构体展开在计算,接下来通过样例分析。

样例分析

struct s5
{  
      short i_val;  
      struct s6
      {  
           char ch;  
           int j_val;  
      } ss;   
      short k_val;  
} ;
sizeof(s5) = 16

首先,确定每个成员的大小和对齐要求:

  • short 类型占 2 字节,对齐要求是 2 字节。
  • char 类型占 1 字节,对齐要求是 1 字节。
  • int 类型占 4 字节,对齐要求是 4 字节。

其次,确定 struct s6 的内存布局:

  • ch 占 1 字节,偏移量 0x00。
  • j_val 占 4 字节,需要对齐到 4 的整数倍,因此在 ch 后面要填充 3 个字节的 padding。

struct s6的内存布局如下

0x00 ch
0x01 padding
0x02 padding
0x03 padding
0x04 j_val (0x04 - 0x07)
0x05 j_val
0x06 j_val
0x07 j_val

接下来,确定 struct s5 的内存布局:

  1. i_val 占 2 字节,偏移量 0x00。
  2. ss 占 8 字节,需要对齐到 4(因为ss中最大成员为j_val,按照规则1,因此ss结构体需要对齐到4字节) 的整数倍,因此在 i_val 后面要填充 2 个字节的 padding。
  3. k_val 占 2 字节,偏移量 0x0C。
0x00 i_val (0x00 - 0x01)
0x01 i_val
0x02 padding
0x03 padding
0x04 ss.ch   // s6开头
0x05 padding
0x06 padding
0x07 padding
0x08 ss.j_val (0x08 - 0x0B)
0x09 ss.j_val
0x0A ss.j_val
0x0B ss.j_val // s6结束
0x0C k_val (0x0C - 0x0D)
0x0E padding
0x0F padding

struct s5 的最大基本类型是 int,所以它的大小必须是 4 的倍数。此时,s5 的大小为 16 字节。

对于嵌套结构体的计算,我建议先算内部,然后在算外部,子结构体需要对齐的字节数,按照其中最大的的基本类型来确定,也就是规则1,同时子结构体本身的大小也要遵循规则3,不足的在子结构体之后添加padding。下面来看第二个例子:

struct s7
{
    short i_val;
    struct s8
    {
        char ch1;
        int j_val;
        char ch2;
    } ss;
    char a;
    char b;
    char c;
    char d;
    char e;
    char f;
};
sizeof(s7) = 24

首先,确定每个成员的大小和对齐要求:

  • short 类型占 2 字节,对齐要求是 2 字节。
  • char 类型占 1 字节,对齐要求是 1 字节。
  • int 类型占 4 字节,对齐要求是 4 字节。

其次,确定 struct s8 的内存布局:

  • ch1 占 1 字节,偏移量 0x00。
  • j_val 占 4 字节,需要对齐到 4 的整数倍,因此在 ch1 后面要填充 3 个字节的 padding。
  • ch2 占 1 字节,偏移量 0x08。
0x00 ch1
0x01 padding
0x02 padding
0x03 padding
0x04 j_val (0x04 - 0x07)
0x05 j_val
0x06 j_val
0x07 j_val
0x08 ch2
0x09 padding
0x0A padding
0x0B padding

为了满足规则3,需要在ch2后面填充3个1字节的padding,因此struct s8 的大小为 12 字节(最大基本类型是 int,所以大小是 4 的倍数)。

接下来,确定 struct s7 的内存布局:

  1. i_val 占 2 字节,偏移量 0x00。
  2. ss 占 12 字节,需要对齐到 4 的整数倍,因此在 i_val 后面要填充 2 个字节的 padding。
  3. a 占 1 字节,偏移量 0x10。
  4. b 占 1 字节,偏移量 0x11。
  5. c 占 1 字节,偏移量 0x12。
  6. d 占 1 字节,偏移量 0x13。
  7. e 占 1 字节,偏移量 0x14。
  8. f 占 1 字节,偏移量 0x15。
0x00 i_val (0x00 - 0x01)
0x02 padding
0x03 padding
0x04 ss.ch1     // s8开头
0x05 padding
0x06 padding
0x07 padding
0x08 ss.j_val (0x08 - 0x0B)
0x09 ss.j_val
0x0A ss.j_val
0x0B ss.j_val
0x0C ss.ch2
0x0D padding
0x0E padding
0x0F padding   // s8结尾
0x10 a
0x11 b
0x12 c
0x13 d
0x14 e
0x15 f
0x16 padding
0x17 padding

struct s7 的最大基本类型是 int,所以它的大小必须是 4 的倍数。需要在f后面添加2字节大小的padding,此时,s7 的大小为 24 字节。

指定对齐字节数的结构体大小计算

计算规则

对一般结构体的计算规则稍做修改:

  1. 结构体变量的首地址,必须是结构体中“min(最大基本类型大小,对齐数)”的整数倍。
  2. 结构体中每个成员相对于首地址的偏移量,都是该“min(成员大小,对齐数)”的整数倍。
  3. 结构体的总大小,为结构体“min(最大基本类型,对齐数)”的整数倍。

说白了,每条规则中比对的对象,都要和对齐数进行比较,然后找最小的那一个来对齐。

样例分析

#pragma pack(4)  //指定向4对齐 最大是8
struct s9{
  char ch;
  int i;
  float f;
  double d;
};

当使用 #pragma pack(4) 指定结构体按 4 字节对齐时,意味着每个成员的偏移量都是 4 的倍数,但不会超过 4 字节。如果某个成员的对齐要求超过 4 字节(比如 double),那么它也将被按 4 字节对齐。分析 struct s6 的内存布局如下:

  1. ch 占 1 字节,偏移量 0x00。
  2. int i 需要对齐到 4 字节的整数倍,所以在 ch 后添加 3 字节的 padding。
  3. float f 也需要对齐到 4 字节的整数倍。
  4. double d 原本需要对齐到 8 字节的整数倍,但由于使用 #pragma pack(4),它只需对齐到 4 字节的整数倍即可。

因此,struct s6 的内存布局为:

0x00 ch
0x01 padding
0x02 padding
0x03 padding
0x04 i (0x04 - 0x07)
0x05 i
0x06 i
0x07 i
0x08 f (0x08 - 0x0B)
0x09 f
0x0A f
0x0B f
0x0C d (0x0C - 0x13)
0x0D d
0x0E d
0x0F d
0x10 d
0x11 d
0x12 d
0x13 d

因此,struct s6 的总大小是 0x14,即 20 字节。

sizeof(s9) = 20

例子二:

#pragma pack(10)
struct s10{
  char ch;
  int i;
  float f;
  double d;
};

使用 #pragma pack(10) 指定结构体按 10 字节对齐时,意味着每个成员的偏移量都是 10 的倍数,但不会超过 10 字节。如果某个成员的对齐要求超过 10 字节(比如 double),那么它也将被按 10 字节对齐。分析 struct s10的内存布局如下:

  1. ch 占 1 字节,偏移量 0x00。
  2. int i 需要对齐到 4 字节的整数倍,所以在 ch 后添加 3 字节的 padding。
  3. float f 也需要对齐到 4 字节的整数倍。
  4. double d 原本需要对齐到 8 字节的整数倍,但由于使用 #pragma pack(10),它只需对齐到 10 字节的整数倍即可。
  5. #pragma pack(10) 实际上会将结构体的总大小对齐到 10 的倍数,所以在 double d 后面添加 6 字节的 padding。

因此,struct s10 的内存布局为:

0x00 ch
0x01 padding
0x02 padding
0x03 padding
0x04 i (0x04 - 0x07)
0x05 i
0x06 i
0x07 i
0x08 f (0x08 - 0x0B)
0x09 f
0x0A f
0x0B f
0x0C d (0x0C - 0x13)
0x0D d
0x0E d
0x0F d
0x10 d
0x11 d
0x12 d
0x13 d
0x14 padding
0x15 padding
0x16 padding
0x17 padding
0x18 padding
0x19 padding

因此,struct s10 的总大小是 0x1A,即 26 字节。

作者:WuQiling
文章链接:https://www.wqlblog.cn/一次讲清楚结构体大小的计算/
文章采用 CC BY-NC-SA 4.0 协议进行许可,转载请遵循协议
暂无评论

发送评论 编辑评论


				
默认
贴吧
上一篇
下一篇