C/C++ 结构体(struct)对齐问题
事情起因是这样的
以前只记得结构体对齐,是对齐最长的那个成员,但现在发现并不是这样,看以下两个示例(64位 g++ 9.3.0 编译)
示例一
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,可能一开始大家都是这么理解的(但实际上不对,不能这么说,后文会指正)
示例二
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某讨论
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 的倍数,因此偏移 0,占用空间 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
回到示例一
再把例子重新放上来吧,以免往上翻
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 两种情况,测试结果不相同
// ======================== 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 两种对齐值的情况,但让人意外的是,测试结果稍微有点超出我的预期。所以我目前还不敢确定自己的看法是否正确,仅作参考,也欢迎指正
// ======================== 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 的倍数,理应填充,但运行结果却没填充,所以我认为,类对象作为成员计算长度时,并不应该计算整个类的长度,而是把类的成员拆开来看取其中最长的那个