为什么结构体 b 和结构体 d 的大小不同?

Why are the sizes of struct b and struct d different?

提问人:haojie zhou 提问时间:9/2/2023 最后编辑:Adrian Molehaojie zhou 更新时间:9/2/2023 访问量:95

问:

#include <iostream>
using namespace std;

struct A{
    virtual void f(){};
    int a;
    char ch;
};

struct B : public A{
    char d;
};

struct C{
    double dd;
    int a;
    char ch;
};

struct D : public C{
    char d;
};

int main(){
    B b;
    D d;
    cout<<"b="<<sizeof(b)<<endl;
    cout<<"d="<<sizeof(d)<<endl;
}

为什么 b 和 d 的结果不同?实际结果如下:

b=16
d=24

我认为每个成员相对于结构变量地址的偏移量正好是该成员数据类型大小的倍数,最终大小是成员中最大数据类型大小的倍数。在存在继承的情况下,基类成员始终出现在派生类成员之前。此外,即使使用字节对齐方式,派生类成员也不会占用为基类保留的填充字节。换言之,一旦确定了基类的大小,这些字节就归基类独占所有,不能由派生类的成员使用。那么为什么 sizeof(b) 是 16 而不是 24?

C++ 虚拟函数 内存对齐虚拟 继承

评论

2赞 Martin Rosenau 9/2/2023
确切的行为取决于编译器和 CPU 架构;但是,使用 GCC 时,确实填充(占用)基类的“保留”字节,而不会。我可以在 x86-64 和 m68k CPU 上看到这种行为。我不知道为什么要这样做。struct Bstruct D
0赞 Wais Kamal 9/2/2023
结构体 D 用双精度代替了结构体 B 的函数,它们的大小应该如何相等?
1赞 Homer512 9/2/2023
这回答了你的问题吗?C++ + gcc:尾部填充重用和 POD
0赞 Homer512 9/2/2023
由于采用虚拟方法,并且不是标准布局(或POD,使用较旧的术语)。我没有检查 C++98 标准,但根据引用的答案,填充无法在 POD(例如和)中重用。C++20 不包含这样的语言,但当然我们不能简单地更改 ABIABCD
1赞 Adrian Mole 9/2/2023
为了改变我的答案以使其更相关(或者,甚至可能删除它),我认为您应该澄清您正在使用的编译器和平台。

答:

3赞 Adrian Mole 9/2/2023 #1

你说的关于继承基类的结构的大小可能是正确的1,尽管我不确定你所做的每个断言对于“非 POD”基类都是正确的,比如(因为成员函数)。但是,您尚未执行(并且应该执行)的是检查两个(不同)基类的大小。A

我可以在重现您的大小值的平台上编译您给定的代码(即 MSVC,面向 32 位 Windows);在这种情况下,我添加了几行来显示基类的大小:

// Earlier code as yours ...

int main() {
    B b;
    D d;
    cout << "b=" << sizeof(b) << endl;
    cout << "d=" << sizeof(d) << endl;

    cout << "A=" << sizeof(A) << endl;
    cout << "C=" << sizeof(C) << endl;
    return 0;
}

输出:

b=16
d=24
A=12
C=16

在这里,我们看到了问题所在!有三个字段:指向类“vtable”的指针(这里显然是 32 位)、一个(也是 4 个字节)和一个(最后一个字段填充为 4 个字节);3 x 4 字节 = 12 字节;为派生类的 (padded) 字段添加 4 个字节,您就有 16 个字节。struct Aintcharchar

但是,尽管也有三个字段,但第一个字段(可能符合 IEEE 标准)的大小为 8 个字节,这会将该结构的大小增加到 16 个字节,并将从中派生到的 24 字节。struct Bdouble

当我运行为 64 位系统构建的相同代码时,我得到的 和 对的大小相同,因为指针的大小与 .bdACdouble


编辑:上面的解释适用于我使用的特定编译器/平台,可能是对您观察的解释。然而,正如评论中指出的(这里和问题),GNU g++ 编译器——当面向 64 位 Windows 时——为两个基类提供了相同的大小(正如预期的那样),但仍然为派生类提供了不同的大小[在编译器资源管理器上查看]

这里的问题显然是不同的,正如评论和其中链接的各种其他 Stack Overflow 帖子中提到的,可能是因为 g++ 已经认识到它不是“POD 类型”,因此,在派生类中使用了其“尾部填充”中的空格。struct A


1 据我所知,C++ 标准并不禁止在派生类中重用尾部填充空间,即使对于“POD”类型也是如此。但是,编译器可能会避免在 POD 上这样做(就像链接示例中的 g++ 一样),因为在基类和派生类之间使用内存复制操作时,它可能会阻止优化和/或简化(此类复制操作不应用于具有虚函数的类,因为它们会涉及覆盖 vtable 条目)。

进一步的讨论在这里:C++ + gcc:尾部填充重用和POD(以及链接的帖子)。

评论

0赞 interjay 9/2/2023
在 x86-64 上使用 GCC 时,A 和 B 的大小均为 16:godbolt.org/z/3j7xqYKo9。顺便说一句,包含一个 vtable 指针,而不是函数指针。A
0赞 Adrian Mole 9/2/2023
@interjay 编辑了指针引用。我还检查了编译器资源管理器上的代码,发现了相同的代码。我将对此进行编辑......