一次讲清楚结构体大小的计算
结构体大小的计算,可分为三类:
- 一般结构体
- 含有嵌套的结构体的
- 指定字节对齐数的结构体
在说明结构体大小计算方法之前,先介绍字节对齐。
字节对齐(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
是不对齐的,处理器需要执行以下步骤:
- 第一次读取:从地址0x01读取3个字节(0x01, 0x02, 0x03)。
- 第二次读取:从地址0x04读取1个字节(0x04)。
然后,处理器需要将这两次读取的数据组合起来,才能得到完整的int b
的值。这会导致更多的内存访问操作和额外的处理开销,从而降低性能。因此需要内存对齐,但是这也会导致对于一些复杂数据类型,例如结构体、类,又该如何对齐呢?接下来详细分析
一般结构体的大小
计算原则
- 结构体变量的首地址,必须是结构体中“最大基本类型大小”的整数倍。
- 结构体中每个成员相对于首地址的偏移量,都是该成员大小的整数倍。
- 结构体的总大小,为结构体“最大基本类型”的整数倍。
样例分析
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
的内存布局:
ch1
占 1 字节,偏移量 0x00。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
的内存布局:
i_val
占 4 字节,偏移量 0x00。ch1
占 1 字节,偏移量 0x04。ch2
占 1 字节,偏移量 0x05。- 为了满足 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
的内存布局:
i_val
占 2 字节,偏移量 0x00。ss
占 8 字节,需要对齐到 4(因为ss
中最大成员为j_val
,按照规则1,因此ss
结构体需要对齐到4字节) 的整数倍,因此在i_val
后面要填充 2 个字节的 padding。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
的内存布局:
i_val
占 2 字节,偏移量 0x00。ss
占 12 字节,需要对齐到 4 的整数倍,因此在i_val
后面要填充 2 个字节的 padding。a
占 1 字节,偏移量 0x10。b
占 1 字节,偏移量 0x11。c
占 1 字节,偏移量 0x12。d
占 1 字节,偏移量 0x13。e
占 1 字节,偏移量 0x14。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 字节。
指定对齐字节数的结构体大小计算
计算规则
对一般结构体的计算规则稍做修改:
- 结构体变量的首地址,必须是结构体中“min(最大基本类型大小,对齐数)”的整数倍。
- 结构体中每个成员相对于首地址的偏移量,都是该“min(成员大小,对齐数)”的整数倍。
- 结构体的总大小,为结构体“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
的内存布局如下:
ch
占 1 字节,偏移量 0x00。int i
需要对齐到 4 字节的整数倍,所以在ch
后添加 3 字节的 padding。float f
也需要对齐到 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
的内存布局如下:
ch
占 1 字节,偏移量 0x00。int i
需要对齐到 4 字节的整数倍,所以在ch
后添加 3 字节的 padding。float f
也需要对齐到 4 字节的整数倍。double d
原本需要对齐到 8 字节的整数倍,但由于使用#pragma pack(10)
,它只需对齐到 10 字节的整数倍即可。#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 字节。