为什么在我添加成员后sizeof(BaseClass) == sizeof(DerivedClass)?

40

从下面的代码可以看出,sizeof(Base) == 24,而且sizeof(Derived) == 24

为什么它们的大小相等?

Base类中有3个成员,在Derived类中我们又添加了一个成员。

class Base
{
private:
    double d;
protected:
    long l;
public:
    int i;
};

class Derived : public Base
{
private:
    float f;
};

11
Base的最后4个字节为空,因此Derived可以在那里放置一些东西。 - Marc Glisse
1
基础:8字节用于d,8字节用于l,4字节用于i,4个未使用的字节。 - Marc Glisse
3
很奇怪:http://ideone.com/qF1GIQ -- 为什么 alignof(Base) == 4,但是 alignof(double) == 8 - leemes
这取决于编译器。对我来说,它将是16和24。 - herohuyongtao
1
@leemes 看起来跟这个问题有关:https://dev59.com/wGbWa4cB1Zd3GeqPcPI9,clang和英特尔都认同gcc。 - Marc Glisse
显示剩余4条评论
4个回答

51

恰好你的类 Base 需要 8 字节对齐,但是它的最后一个成员大小为 4。这会导致在 Base 的内存布局末尾添加了一个空的填充区域。当你单独实例化 Base 类的对象时,这个额外的填充起到了作用,被称为所谓的“最派生对象”。

Base b; // <- a most-derived object
Base a[10]; // <- an array of most-derived objects

然而,当您将Base作为基类“嵌入”到类Derived中时,嵌入式Base子对象末尾的额外填充是不必要的。

Derived d; // <- object `d` contains an embedded sub-object of type `Base`

一个智能的编译器将尝试重用该区域,通过将类Derived的额外字段放置在Base中用于填充的布局区域中。在您的情况下,额外的字段Derived::f恰好具有4字节大小,即它完美地适合那里。最终结果是类的总大小不会增加。

一种非常相似(性质相似)的效果称为“空白基类优化”。在C ++中,任何类型的sizeof都保证大于0,这意味着空类的sizeof始终大于零。但是,当您从一个空的基类派生另一个类时,您可能会观察到基类对派生类的大小贡献恰好为0字节。例如:

struct A {};
struct B {};
struct C {};
struct D {};

struct F : A, B, C, D {
  int i;
}

int main() {
  std::cout << sizeof(A) << std::endl << sizeof(B) << std::endl << 
               sizeof(C) << std::endl << sizeof(D) << std::endl;
  std::cout << sizeof(F) << std::endl;
}

尽管每个基类的sizeof大于零,但是sizeof(F)通常仍然会评估为sizeof(int),就好像基类子对象根本不存在一样。

换句话说,正如这些示例所显示的那样,基类子对象在其内存布局方面遵循明显更宽松的规则,比最派生对象更宽松。 这些宽松的规则可能很容易导致基类的 sizeof 仅部分地贡献于派生类的 sizeof


1
请注意,重用该区域的决定是在ABI中进行的,编译器通常对此没有太多选择。 - Marc Glisse
3
@Marc Glisse:ABI 是一个不同领域的故事。谁说有 ABI 呢?就语言本身而言,并不存在所谓的 ABI。更相关的评论可能是,这些问题实际上可以由用户通过操作不同的实现相关编译器工具来指示编译器,从而允许用户覆盖默认的对齐/填充规则。 - AnT stands with Russia
2
对齐要求是如何计算的?例如,基于什么?是基于数据总线大小吗? - Koushik Shetty
@Koushik:这里涉及到许多因素。大多数硬件的典型情况是,大小为N的基本类型应该在第N个字节边界上对齐。这要么是硬件要求(如果违反要求,代码将崩溃),要么是性能指南(如果违反要求,代码将表现更差)。除此之外,您还有自己的高级考虑因素:缓存性能问题可能会使您更喜欢更大的对齐值,内存消耗问题可能会使您更喜欢更小的对齐值(如果可用)。等等。 - AnT stands with Russia

9

由于double和long的sizeof都等于8,这通常意味着alignof(double)也等于8。这意味着如果Base存储在数组中,它必须在8字节边界上进行大小对齐,并且它在末尾生成4字节的填充。Derived去掉填充以放置f。


7
填充(padding)与 alignof(double) 更相关,而不是与 sizeof(double) 相关。 - Marc Glisse
1
@MarcGlisse,为了更准确,感谢您的评论。 - galop1n

5
使用pahole来解决这个问题:
class Base {
private:

    double                     d;                    /*     0     8 */
protected:

    long int                   l;                    /*     8     8 */
    int                        i;                    /*    16     4 */


    /* size: 24, cachelines: 1, members: 3 */
    /* padding: 4 */
    /* last cacheline: 24 bytes */
};
class Derived : public Base {
public:

    /* class Base                <ancestor>; */      /*     0    24 */

    /* XXX last struct has 4 bytes of padding */
private:

    /* Bitfield combined with next fields */

    float                      f;                    /*    20     4 */


    /* size: 24, cachelines: 1, members: 2 */
    /* paddings: 1, sum paddings: 4 */
    /* last cacheline: 24 bytes */
};

2

由于对齐需要而产生的填充:

虽然编译器(或解释器)通常在对齐边界上分配单个数据项,但数据结构经常具有具有不同对齐要求的成员。为了保持适当的对齐,翻译器通常插入额外的未命名数据成员,以便每个成员都正确对齐。此外,整个数据结构可能会用最终的未命名成员进行填充。这允许结构数组的每个成员都得到适当的对齐。

更多信息请参见:
http://en.wikipedia.org/wiki/Data_structure_alignment#Data_structure_padding


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接