以下所有操作均将在GCC 9.1上使用x86-64中的Compiler Explorer在上完成-O3。
我有以下代码:
struct Base {
Base() {}
double foo;
int bar;
};
struct Derived : public Base {
int baz;
};
int main(int argc, char** argv)
{
return sizeof(Derived);
}
Run Code Online (Sandbox Code Playgroud)
16如我所料,它正确地返回的8个字节foo,的4个字节bar和的4个字节baz。这仅是因为Derived继承自Base,因此bar由于Derived是同时包含Base和Derived元素的单一类型而不必填充。
我有两个问题,如下所示:
第一个问题
如果我删除的显式构造函数Base() {},它将开始返回24,而不是16。也就是说,它在bar和之后添加了填充baz。
我无法解释为什么拥有显式默认构造函数与具有隐式默认构造函数有什么不同。
第二个问题
如果然后我更改struct为classfor Base,它又变回了16。我也不能解释这一点。为什么访问修饰符会更改结构的大小?
Nat*_*ica 36
这一切都归结为您的类型是否为聚合。用
struct Base {
Base() {}
double foo;
int bar;
};
struct Derived : public Base {
int baz;
};
Run Code Online (Sandbox Code Playgroud)
Base由于构造函数而不是合计。删除构造函数时,将生成Base一个聚合,该聚合通过向基类中添加默认构造函数来更改sizeof()派生类型,这意味着gcc不会“优化”空间,派生对象也不会使用基尾填充。
当您将代码更改为
class Base {
double foo;
int bar;
};
struct Derived : public Base {
int baz;
};
Run Code Online (Sandbox Code Playgroud)
foo并且bar现在是私有的(因为类默认情况下具有私有可访问性),这又意味着Base不再是聚合,因为不允许聚合具有私有成员。这意味着我们回到第一种情况的工作方式。
Tur*_*ght 10
使用您的Base类,您将获得4个字节的尾部填充,而与Derived类相同,这就是为什么通常应将其24 bytes总计为的大小的原因Derived。
它变为16个字节,因为您的编译器能够进行尾部填充重用。
但是,尾部填充重用对于POD类型(所有成员都是公共成员,默认构造函数等)而言是有问题的,因为它打破了程序员会做出的常见假设。(因此,基本上任何明智的编译器都不会对pod类型进行尾部填充重用)
让我们假设编译器将tail padding reusePOD类型用于:
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!
}
Run Code Online (Sandbox Code Playgroud)
在向基类添加显式构造函数时,或通过将struct关键字更改为class,Derived该类不再满足POD定义,因此不会发生尾部填充重用。