为什么将此 POD 结构用作基类会很危险

Why can it be dangerous to use this POD struct as a base class?

本文关键字:基类 危险 POD 结构 为什么      更新时间:2023-10-16

我和一位同事进行了这次对话,结果很有趣。假设我们有以下 POD 类

struct A { 
  void clear() { memset(this, 0, sizeof(A)); } 
  int age; 
  char type; 
};

clear旨在清除所有成员,设置为0(按字节(。如果我们使用 A 作为基类,可能会出错?这里有一个微妙的错误来源。

编译器可能会向 A 添加填充字节。因此,sizeof(A)超出了char type(直到填充的末尾(。但是,在继承的情况下,编译器可能不会添加填充的字节。因此,对 memset 的调用将覆盖子类的一部分。

除了其他注释之外,sizeof 是一个编译时运算符,因此clear()不会将派生类添加的任何成员清零(除非由于填充奇怪而注明(。

这没有什么真正"微妙"的; memset在C++中使用是一件可怕的事情。在极少数情况下,你真的可以用零填充内存并期望理智的行为,并且你真的需要用零填充内存,并且通过初始化项列表以文明的方式零初始化所有内容在某种程度上是不可接受的,请使用std::fill代替。

理论上,编译器可以以不同的方式布置基类。 C++03 §10 第 5 段说:

基类子对象的布局 (3.7( 可能与相同类型的大多数派生对象的布局不同。

正如 StackedCrooked 所提到的,当基类作为自己的对象存在时,编译器可能会在基类的末尾添加填充A发生这种情况,但编译器可能不会在基类时添加该填充。 这将导致A::clear()覆盖子类成员的前几个字节。

然而,在实践中,我无法在GCC或Visual Studio 2008中做到这一点。 使用此测试:

struct A
{
  void clear() { memset(this, 0, sizeof(A)); }
  int age;
  char type;
};
struct B : public A
{
  char x;
};
int main(void)
{
  B b;
  printf("%d %d %dn", sizeof(A), sizeof(B), ((char*)&b.x - (char*)&b));
  b.x = 3;
  b.clear();
  printf("%dn", b.x);
  return 0;
}

并且修改AB或两者以"打包"(VS 中#pragma pack,GCC 中__attribute__((packed))(,无论如何我都无法覆盖b.x。 已启用优化。 为尺寸/偏移打印的 3 个值始终为 8/12/8、8/9/8 或 5/6/5。

类的 clear 方法将仅设置类成员的值。

根据对齐规则,允许编译器插入填充,以便下一个数据成员将出现在对齐的边界上。 因此,在type数据成员之后会有填充。 后代的第一个数据成员将占据这个插槽并且不受memset的影响,因为基类sizeof不包括后代的大小。 父级的大小 != 子级的大小(除非子级没有数据成员(。 请参阅切片

结构包装不是语言标准的一部分。 希望有一个好的编译器,打包结构的大小不会在最后一个字节之后包含任何额外的字节。 即便如此,从打包父级继承的打包后代也应产生相同的结果:parent 仅设置父级中的数据成员。

简而言之:在我看来,唯一的一个潜在问题是我在 C89、C2003 标准中找不到有关"填充字节"保证的任何信息......它们是否有一些非凡的易失性或只读行为 - 我什至找不到标准术语"填充字节"是什么意思......

详细

对于 POD 类型的对象,C++2003 标准保证:

    当您将对象
  • 的内容内存为字符数组或无符号字符数组,然后将内容内存回对象时,该对象将保留其原始值
  • 保证 POD 对象的开头不会有填充

  • 可以违反有关以下内容C++规则:转到语句,生存期

对于C89,还存在一些关于结构的保证:

  • 当用于联合结构的混合时,如果结构具有相同的开头,则第一个组合具有完美的数学

  • sizeof结构在C中等于存储所有组件的内存量,组件之间的填充位置,将填充放在以下结构下

  • 在 C 语言中,结构的组件被赋予地址。可以保证地址的组成部分按升序排列。并且第一个组件的地址与结构的起始地址一致。无论运行程序的计算机是哪个字节序

所以在我看来,这样的规则也适合C++,一切都很好。我真的认为在硬件级别没有人会限制您为非常量对象写入填充字节。