关于填充和继承,类和结构之间的区别

时间:2019-07-19 14:04:42

标签: c++ c++11 gcc

在GCC 9.1上,使用-O3在x86-64中使用Compiler Explorer完成以下所有操作。

我有此代码:

struct Base {
    Base() {}
    double foo;
    int bar;
};

struct Derived : public Base {
    int baz;
};

int main(int argc, char** argv)
{
    return sizeof(Derived);
}

https://godbolt.org/z/OjSCZB

正如我期望的那样,它正确返回16foo为8个字节,bar为4个字节,baz为4个字节。这仅是因为Derived继承自Base,所以由于bar是同时包含Derived和{ {1}}个元素。

我有两个问题,如下所示:

第一个问题

如果删除Base的显式构造函数,它将开始返回Derived,而不是Base() {}。即在2416之后添加填充。

https://godbolt.org/z/0gaN5h

我无法解释为什么拥有显式默认构造函数与具有隐式默认构造函数有何不同。

第二个问题

如果我随后将bar的{​​{1}}更改为baz,它将变回返回struct。我也不能解释这一点。为什么访问修饰符会更改结构的大小?

https://godbolt.org/z/SCYKwL

2 个答案:

答案 0 :(得分:24)

这全部归结为您的类型是否为聚合类型。与

struct Base {
    Base() {}
    double foo;
    int bar;
};

struct Derived : public Base {
    int baz;
};

Base由于构造函数而不是聚合的。删除构造函数时,您将Base设为一个汇总,每个Adding a default constructor to a base class changes sizeof() a derived type表示gcc不会“优化”空间,并且派生对象将不会使用基准的尾部填充。

将代码更改为

class Base {
    double foo;
    int bar;
};

struct Derived : public Base {
    int baz;
};

foobar现在是私有的(因为默认情况下类具有私有可访问性),这又意味着Base不再是聚合,因为不允许聚合具有私有成员。这意味着我们回到第一种情况的工作方式。

答案 1 :(得分:5)

在您的Base类中,您将获得4个字节的尾部填充,而与Derived类中的填充相同,这就是为什么对于24 bytes的大小,它通常应总计Derived的原因。

它变为16个字节,因为您的编译器能够执行tail padding reuse

然而,POD types(所有成员都是公共成员,默认构造函数等)的尾巴填充重用是有问题的,因为它打破了程序员会做出的常见假设。 (因此,基本上任何明智的编译器都不会对Pod类型进行尾部填充重用)

让我们假装的编译器将tail padding reuse用于POD类型:

struct Base {
    double foo;
    int bar;
};

struct Derived : Base {
    int baz;
};

int main(int argc, char** argv)
{
    // if your compiler would reuse the tail padding then the sizes would be:
    // sizeof(Base) == 16
    // sizeof(Derived) == 16

    Derived d;
    d.baz = 12;
    // trying to zero *only* the members of the base class,
    // but this would zero also baz from derived, not very intuitive
    memset((Base*)&d, 0, sizeof(Base));

    printf("%d", d.baz); // d.baz would now be 0!
}

在向基类中添加显式构造函数或将struct关键字更改为class时,Derived类不再满足POD定义,因此尾部填充重用不会发生。