Horbynz hub

C/C++ 结构体(struct)对齐问题

Word count: 1.4kReading time: 5 min
2022/02/26

引入

以前只记得结构体对齐,是对齐最长的那个成员,但现在发现并不是这样,看以下两个示例(64位 g++ 9.3.0 编译)

示例一

1
2
3
4
5
6
7
8
9
10
11
class B
{
public:
char b; // 8
virtual void fun() {}; // 8
static int c; // 0
static int d; // 0
static int f; // 0
};

cout<<sizeof(B)<<endl; // 16

值得一提的是静态成员不占空间
这里虚指针 = 8B,char b = 1B。最宽成员是虚指针,所以char b对齐为 8B,所以总 16B,可能一开始大家都是这么理解的(但实际上不对,不能这么说,后文会指正)

示例二

1
2
3
4
5
6
7
8
9
10
11
12
13
class A
{
public:
char a; // 4
int b; // 4
};
class C
{
A a; // 8
char c; // 4???
};

cout<<sizeof(C)<<endl; // 12

类 C 中char c成员实际结果是 4B。
如果按原来的理解,类 A = 8B,类 C 继承后最宽成员是 A 类,那char c不应该对齐为 8B 吗?

实际情况

参考 LeetCode某讨论

假定是gcc,用默认的对齐系数 4 (编译器决定)
对齐值是对齐系数和结构体各成员长度最大值的较小值
对齐要求各成员起始偏移量是对齐值和它的长度的较小值的倍数

一句话概括就是

  • 对齐值 = min{对齐系数,最长成员}
  • [补]对齐系数用#pragma pack(n)指定,其中 n = 1, 2, 4, 8…等二次幂;默认情况下,貌似 32-bit 机是 4,64-bit 机是 8
  • 起始偏移 = min{对齐值,成员自身长度},再取整数倍
  • 最后整个结构体的大小要补足为对齐值的倍数

举个例子(还是拿上面这个讨论作例子)

参考 LeetCode某讨论

1
2
3
4
5
6
struct student {
int m_id; // 4
char m_name[10]; // 10
bool m_sex; // 1
int m_age; // 4
};

这里先给出分析结构体长度的一般方法:

  • 得出对齐值
  • 分析各成员的偏移地址
  • 再分析结构体占用空间大小

第一步,得出对齐值:

  • 对齐值 = min{对齐系数(4), 最长成员(10)} = 4(假设是 32 位机)

第二步,分析各成员偏移地址:

  • 成员m_id自身长度 4,对齐值 4,因此 min = 4。作为第一个成员,偏移地址为 0,而 0 是 4 的倍数,因此偏移 0,占用空间 0-3
  • 成员m_name[]自身长度 10,对齐值 4,因此 min = 4。续上一个成员结尾偏移地址为 4,而 4 是 4 的倍数,因此偏移 4,占用空间 4-13
  • 成员m_sex自身长度 1,对齐值 4,因此 min = 1。续上一个成员结尾偏移地址为 14,而 14 是 1 的倍数,因此偏移 14,占用空间 14
  • 成员m_age自身长度 4,对齐值 4,因此 min = 4。续上一个成员结尾偏移地址为 15,而 15 不是 4 的倍数,所以要补位(补至恰好是 min 倍数即可,这里 4 * 3 < 15 而 4 * 4 > 15 所以补至 16),因此偏移 16,占用空间 16-19

第三步,分析结构体大小

  • 目前整个结构体占用空间 0-19 即 20B,20 是对齐值 4 的倍数,所以不需填充,所以最终大小是 20B

回到示例一

再把例子重新放上来吧,以免往上翻

1
2
3
4
5
6
7
8
9
10
11
class B
{
public:
char b; // 8
virtual void fun() {}; // 8
static int c; // 0
static int d; // 0
static int f; // 0
};

cout<<sizeof(B)<<endl; // 16

之前说char b对齐 8B 是因为对齐最宽的虚指针长度,其实是不对的。我测试了对齐值分别为 4 和 8 两种情况,测试结果不相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ======================== v1 =============================
#pragma pack(4) // 指定对齐值 4
class B
{
public:
char b; // 4
virtual void fun() {}; // 8
static int c; // 0
static int d; // 0
static int f; // 0
};

cout<<sizeof(B)<<endl; // 12

// ================== v2(同示例一) ========================
#pragma pack(8) // 指定对齐值 8,64-bit 貌似默认 8,这也是一开始示例一的默认对齐值
class B
{
public:
char b; // 8
virtual void fun() {}; // 8
static int c; // 0
static int d; // 0
static int f; // 0
};

cout<<sizeof(B)<<endl; // 16

可以发现两种结构体char b对齐 4B 和 8B,因此可以得出结论简单地说对齐最长成员是不对的

回到示例二

这里我也测试了 4 和 8 两种对齐值的情况,但让人意外的是,测试结果稍微有点超出我的预期。所以我目前还不敢确定自己的看法是否正确,仅作参考,也欢迎指正

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ======================== v1 =============================
#pragma pack(4) // 指定对齐值 4
class A
{
public:
char a; // 4
int b; // 4
};
class C
{
A a; // 8
char c; // 4
};
cout<<sizeof(C)<<endl; // 12

// ======================== v2 =============================
#pragma pack(8) // 指定对齐值 8
class A
{
public:
char a; // 4
int b; // 4
};
class C
{
A a; // 8
char c; // 4
};
cout<<sizeof(C)<<endl; // 12

是不是和预期不同?

  • 第一个版本对齐值 4,min = min{对齐系数(4),最长成员(8)} = 4,结构体长度简单累加起来是 12B,12B 是 4 的倍数,所以不用填充。这没问题,符合我们上面的结论
  • 但第二个版本对齐值 8,min = min{对齐系数(8),最长成员(8)} = 8,结构体长度简单累加起来是 12B。按我们上面的结论 12B 并不是 8 的倍数,理应填充,但运行结果却没填充,所以我认为,类对象作为成员计算长度时,并不应该计算整个类的长度,而是把类的成员拆开来看取其中最长的那个
CATALOG
  1. 1. 引入
  2. 2. 示例一
  3. 3. 示例二
  4. 4. 实际情况
  5. 5. 举个例子(还是拿上面这个讨论作例子)
  6. 6. 回到示例一
  7. 7. 回到示例二