我和一位同事进行了这次谈话,结果很有趣。假设我们有以下 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 %d\n", sizeof(A), sizeof(B), ((char*)&b.x - (char*)&b));
b.x = 3;
b.clear();
printf("%d\n", b.x);
return 0;
}
并且将 , 或两者都修改A
为B
“打包”(#pragma pack
在 VS 和__attribute__((packed))
GCC 中),无论如何我都无法b.x
被覆盖。启用了优化。为尺寸/偏移量打印的 3 个值始终为 8/12/8、8/9/8 或 5/6/5。
基类的clear
方法只会设置类成员的值。
根据对齐规则,允许编译器插入填充,以便下一个数据成员将出现在对齐的边界上。type
因此数据成员之后会有填充。后代的第一个数据成员将占据这个槽并且不受 的影响memset
,因为sizeof
基类不包括后代的大小。父级的大小!= 子级的大小(除非子级没有数据成员)。 见切片。
结构的打包不是语言标准的一部分。希望通过一个好的编译器,压缩结构的大小不包括最后一个字节之后的任何额外字节。即便如此,从打包父级继承的打包后代应该产生相同的结果:父级仅设置父级中的数据成员。
简而言之:在我看来,唯一的一个潜在问题是我在 C89、C2003 标准中找不到任何关于“填充字节”保证的信息......他们是否有一些非同寻常的易失性或只读行为 - 我找不到甚至标准的术语“填充字节”是什么意思......
详细:
对于 POD 类型的对象,C++2003 标准保证:
保证在 POD 对象的开头不会有填充
可以打破 C++ 规则:goto 语句、生命周期
对于 C89,还存在一些关于结构的保证:
当用于联合结构的混合时,如果结构具有相同的开头,则第一个组件具有完美的数学运算
C中的sizeof结构体等于存储所有组件的内存量,组件之间的padding下的地方,下面的结构体下的padding
在 C 中,结构的组件被赋予了地址。保证地址的组成部分是按升序排列的。并且第一个组件的地址与结构的起始地址一致。无论程序运行的计算机是哪个字节序
所以在我看来,这样的规则也适用于 C++,一切都很好。我真的认为在硬件级别没有人会限制你为非常量对象写入填充字节。