gcc 4.4.7 的 C++ 对象模型:基 class 子对象填充占用派生 class 对象

C++ object model by gcc 4.4.7: base class subobject padding occupied in derived class object

我正在阅读 -- 3.4 继承和数据成员。它说 "the language guarantee of the integrity of the base class subobject within the derived class." 还给出了一个例子如下:

class Concrete1
{
private:
    int val;
    char bit1;
};

class Concrete2 : public Concrete1
{
private:
    char bit2;
};

class Concrete3 : public Concrete2
{
private:
    char bit3;
};

Concrete1 对象模型是:

4 bytes for val, 
1 byte for bit1,
3 bytes padding. 
// 8 bytes in total.

Concrete2 对象模型是:

8 bytes for Concrete1 subobject, 
1 byte for bit2,
3 bytes padding. 
// 12 bytes in total. 

同理,Concrete3对象模型为16字节。

Concrete2对象成员bit2没有使用Concrete1子对象的填充部分,所以是12字节。但是当我尝试 gcc 4.4.7 中的示例时,Concrete2 对象和 Concrete3 对象与 Concrete1 对象的大小相同——8 个字节。所以我猜 gcc 使用 Concrete1 对象的填充部分来存储 bit2 和 bit3。我称它们为 "not-use-padding" 方式,简称为 "use-padding" 方式。

为了解释为什么不用padding部分,书上给出了如下代码:

Concrete2 *pc2; 
Concrete1 *pc1_1, *pc2_2; 
pc1_1 = pc2;
*pc1_1 = *pc2_2; // pc1_1 may point to a Concrete2 or Concrete3 object. 

在"not-use-padding"方式中,pc2_2指向的Concrete1对象会被复制到pc1_1指向的Concrete2/Concrete3对象的Concrete1子对象中。这本书还说它是一个 "memberwise" 副本,但它看起来更像是一个 "object" 副本,因为它暗示填充部分也被复制了。

在"use-padding"方式中,书上说它会覆盖bit2成员,因为*pc2_2对应的字节是一个填充字节。再一次,我用 gcc 4.4.7 试了一下,结果 bit2 成员没有被覆盖。所以我猜副本是真正的 "memberwise" 副本,只是复制了 val 和 bit1。

所以我的问题是: 1. 我对 gcc 活动的看法是否正确:"use-padding" 方式和真正的 "memberwise" 副本? 2. 派生class不能访问基class的私有成员,但是派生class对象包含其基class子对象中的所有私有成员(val和 bit1) 在对象模型中。为什么它被设计成包含基本 class 私有成员,即使它们甚至不能在派生 class 对象中访问?仅用于像 *pc1_1 = *pc2_2 这样的复制操作; ?

谢谢

关于你的第一个问题:

我试图用不同的编译器在 ideone.com 重现您描述为 "use-padding" 的行为,但失败了。 Clang、gcc 4.3 和 gcc 5.1 似乎没有使用 base class 的填充。但是从 this Question 的答案可以看出 gcc 确实使用了这个填充。

标准引入了POD-types的概念,但明确指出

9 Classes [class]
(...)
7 A class S is a standard-layout class if it: (...)
(7.6) has all non-static data members and bit-fields in the class and its base classes first declared in the same class (...)

所以这不适用于您的 class,因为您在基础和派生 class 中都有数据成员。我找不到任何关于非 POD 类型需要如何布局的参考资料。链接问题中的一个答案指出,这已经发生变化。

继承的重要之处在于 - 使用指针或引用 - 您可以像使用基类对象一样使用派生对象 class。这意味着您可以像使用 Concrete1* 一样使用 Concrete3*。但是无论你是否使用填充都是独立的,只要你在添加 Concrete2Concrete3.[=19= 的数据成员时不更改 Concrete1 的布局即可]


关于你的第二个问题:

标准规定:

11 Member access control [class.access]
1 A member of a class can be

  • (1.1) private; that is, its name can be used only by members and friends of the class in which it is declared.
  • (1.2) protected; that is, its name can be used only by members and friends of the class in which it is declared, by classes derived from that class, and by their friends (see 11.4).
  • (1.3) public; that is, its name can be used anywhere without access restriction.

如您所见,它只是指定了名称可以在何处使用。例如。您仍然可以 return 引用或指向私有成员的指针。这意味着它们必须存在于内存中的某个地方。事实上,对象的状态由其成员的状态定义 - private 或 public 只是允许您在代码中直接访问它们的位置的问题。