对 C++ 基类布局感到困惑

Posted

技术标签:

【中文标题】对 C++ 基类布局感到困惑【英文标题】:Confused about c++ base class layout 【发布时间】:2020-08-14 07:59:18 【问题描述】:

这是我的代码

#include <bits/stdc++.h>


class A
    int val;
    char c;
;
class B:public A
    char val;
;

struct C
    int val;
    char c;
;
struct D:public C
    char val;
;


int main()

    std::cout<<sizeof(B)<<std::endl; //8
    std::cout<<sizeof(D)<<std::endl; //12


为什么classstruct 有不同的对齐方式


*** Dumping AST Record Layout
   0 | class A
   0 |   int val
   4 |   char c
     | [sizeof=8, dsize=5, align=4
     |  nvsize=5, nvalign=4]


*** Dumping AST Record Layout
   0 | class B
   0 |   class A (base)
   0 |     int val
   4 |     char c
   5 |   char val
     | [sizeof=8, dsize=6, align=4
     |  nvsize=6, nvalign=4]


*** Dumping AST Record Layout
   0 | struct C
   0 |   int val
   4 |   char c
     | [sizeof=8, dsize=8, align=4
     |  nvsize=8, nvalign=4]


*** Dumping AST Record Layout
   0 | struct D
   0 |   struct C (base)
   0 |     int val
   4 |     char c
   8 |   char val
     | [sizeof=12, dsize=9, align=4
     |  nvsize=9, nvalign=4]

【问题讨论】:

欢迎来到 ***.com。请花点时间阅读the help pages,尤其是名为"What topics can I ask about here?" 和"What types of questions should I avoid asking?" 的部分。另请参阅tour 并阅读How to Ask,以及this question checklist。 在不相关的注释上,请阅读Why should I not #include <bits/stdc++.h>? 您可能应该指定您的编译器和平台,因为 C++ 仅对可能的结果指定了一些限制,但确切的结果由目标 ABI 定义。 如果您没有为同一结构的不同数据成员使用相同的名称val,这将不会那么混乱,请更改并更新问题 您能否尝试将您的class 成员设置为public,然后打印出大小。 【参考方案1】:

struct 的情况下考虑这个程序:

void f(C& cx)

    cx.c = 'x';


int main()

    D d;
    d.D::val = 'y';
    f(d);
    std::cout << d.D::val << '\n';

此代码必须输出y

在您的系统上,AC 结构的大小为8,因为有一个大小为4 和一个字符的成员,并且该结构必须与其最大成员正确对齐。这些结构体有 4 个字节的 int、1 个字节的 char 和 3 个填充字节。

赋值cx.c = 5;允许修改填充(任何结构体赋值都可以修改结构体填充)。因此,该填充不能用于存储基类元素。

但是,AB 不可能有类似的示例,因为 A 的数据成员是私有的。不可能有函数void f(A&amp; ax) ax.c = 'x'; ,因此不会出现这种问题,编译器可以使用A 的填充区域来存储派生类成员。


注意:由于基类和派生类中都有数据成员,这两个类都不是 standard layout。

【讨论】:

这很有趣,如果我可以说,这也是一个好消息,即使不是主要针对公共数据成员的。请注意,这不仅适用于私有数据成员,也适用于受保护数据成员,简而言之,非公共数据成员。【参考方案2】:

添加到@MM 答案,看起来即使您有A 类的公共constructorsetter 成员函数,编译器仍将类B 数据成员存储在类A 的填充区域中(我试图强制编译器不要使用 A 类的尾部填充,但无法成功)。

可以在class.mem/19 中找到一条注释:

[ 注意:具有相同访问控制和非零大小([intro.object])的(非联合)类的非静态数据成员被分配,以便后面的成员在类对象中具有更高的地址。未指定具有不同访问控制的非静态数据成员的分配顺序。实现对齐要求可能会导致两个相邻的成员不能立即分配;管理虚拟功能([class.virtual])和虚拟基类([class.mi])的空间要求也是如此。 ——尾注]

从this添加更多答案:

该标准要求在内存中将具有相同访问控制的成员分组在一起。该分组决定了对象如何被填充,因此改变它可以/将会改变对象的大小。

更多来自this的回答:

这些类型的 dsize、nvsize 和 nvalign 被定义为它们的普通大小和对齐方式。这些属性仅对用作基类的非空类类型很重要。我们忽略 POD 的尾部填充,因为该标准的早期版本不允许我们将它用于其他任何事情,而且它有时允许更快地复制类型。

因此,在您的第一个示例中,A 不是用于布局目的的 POD,其尾部填充可用于 B::val,但在您的第二个示例中,它是 POD,其尾部填充不能重复使用。

#include <iostream>


class A 
    int val;
    char c;
public:
    A(int a, char b): val(a), c(b)
    

    
public:
    void setC(int a)
    
        c = a;
    
    char getC(void) const
    
        return c;
    
;

class B: public A 
    char val;
public:
    B(void): A(1,'2'), val('2')
    

    
public:
    char getVal(void) const
    
        return val;
    
;

struct C 
    int val;
    char c;
;
struct D: public C 
    char val;
;


int main()

    B a;
    a.setC(2370);
    std::cout << a.getVal() << " & " << a.getC() << std::endl;
    std::cout << sizeof(B) << std::endl; // 8
    std::cout << sizeof(D) << std::endl; // 12
    return 0;

输出:

2 & B
8
12 

要了解memory order and alignment 的课程,请参阅this。

【讨论】:

你的第二个报价是错误的,尽管有 17 个赞成票 :(

以上是关于对 C++ 基类布局感到困惑的主要内容,如果未能解决你的问题,请参考以下文章

C++虚基类表指针字节对齐模型

将默认构造函数添加到基类会更改sizeof()派生类型[duplicate]

声明指向基类和派生类的指针

如何让一个向量接受具有相同基类的 2 个不同的类?

来自孩子的PHP私有变量访问

Python继承-在子类中调用基类方法?