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 的倍数,理应填充,但运行结果却没填充,所以我认为,类对象作为成员计算长度时,并不应该计算整个类的长度,而是把类的成员拆开来看取其中最长的那个
posted @ 2022-02-10 16:38  幼麟  阅读(237)  评论(0编辑  收藏  举报