在C语言中,"清零"结构体的填充位是否保证被清零?

3
这篇文章中的一句话让我感到尴尬:

C语言允许实现在结构体(但不是数组)中插入填充以确保所有字段都对目标有用的对齐方式。 如果你将一个结构体清零,然后设置其中一些字段,那么填充位是否都为零? 根据调查结果,36% 的人确定会这样,29% 的人不知道。根据编译器(和优化级别),可能会或可能不会。

这并不完全清楚,所以我查阅了标准。ISO/IEC 9899§6.2.6.1 中说明:

当一个值被存储在结构体或联合类型的对象中,包括成员对象时,与任何填充字节对应的对象表示的字节取未指定值

同样在 §6.7.2.1 中也有相关说明:

位域在一个单元内的分配顺序(从高位到低位或从低位到高位)是由实现定义的。可寻址存储单元的对齐方式未指定。

我突然想起来最近实现了某种类型的黑客,其中我使用了由位域拥有但未声明的字节部分。大致是这样的:

/* This struct is always allocated on the heap and is zeroed. */
struct some_struct {
  /* initial part ... */
  enum {
    ONE,
    TWO,
    THREE,
    FOUR,
  } some_enum:8;
  unsigned char flag:1;
  unsigned char another_flag:1;
  unsigned int size_of_smth;
  /* ... remaining part */
};

由于结构不在我控制之下,所以我无法更改它,但我急需通过它传递一些信息。因此,我计算了相应字节的地址,如下:

unsigned char *ptr = &some->size_of_smth - 1;
*ptr |= 0xC0; /* set flags */

然后我以同样的方式检查了标志。

另外,我应该提到目标编译器和平台已经定义,因此这不是跨平台的事情。但是,当前问题仍然存在:

  1. 我能否依赖于结构体(在堆中)的填充位在 memset/kzalloc/等操作之后仍将保持为零,并且在一些后续使用之后仍将保持为零?(这篇文章没有揭示标准和进一步使用结构体的保障)。那么,在栈上清零的结构体如 = {0}呢?

  2. 如果是的话,这是否意味着我可以安全地在C语言中使用“未命名”/“未声明”的位域部分在各个地方传输一些信息(不同的平台、编译器等)?(如果我确定没有人疯狂地尝试在这个字节中存储任何东西)。


你可以肯定,没有编译器会屏蔽掉字节中未使用的位域并故意将它们保持未初始化状态。或者,在写入特定位时,不管有2个还是8个位被定义,都不会有任何不同的处理方式。注意:memset会设置整个范围,而不关心其内容。 - Weather Vane
虽然不太方便,但你可以创建一个结构体和 n 个字符的联合体,其中 n 是结构体的大小。你可以进行一次 n == sizeof(struct) 的检查以确保正确性,然后在需要时将 n 字符数组清零。 - rcgldr
@WeatherVane 一般来说,你是正确的。不过,在某些情况下,你必须使用这样的“肮脏技巧”,但同时也必须确保符合标准的角度。我当然明白它会在我的特定情况下正常工作。 - z0lupka
3
规范允许优化器作者使用更快的赋值操作替换小结构体上的memset,这可能会给你留下足够的余地而被“绞死”。 - Hans Passant
没有任何保证 - “哪里”填充位将插入结构中,仅有的保证是不会在第一个成员之前(因为结构体的地址是第一个成员的地址)。除此之外,您甚至不能依赖于memset来覆盖结构体填充。虽然您可以在单个机器上使用单个编译器进行实验以确定其实现行为-但无法保证其适用于任何其他机器或编译器。 - David C. Rankin
3个回答

3
你第一个问题的简短回答是“不”。
虽然适当调用memset(),例如memset(&some_struct_instance, 0, sizeof(some_struct))会将结构体中的所有字节设置为零,但在“某些用法”(例如设置其中任何成员)后,该更改不需要持久存在于some_struct_instance中。
因此,例如,没有保证some_struct_instance.some_enum = THREE(即存储成员的值)会使some_struct_instance中的任何填充位保持不变。标准中唯一的要求是结构体其他成员的值不受影响。但是,编译器可以(在发出的目标代码或机器指令中)使用某些位运算来实现赋值,并且可以采取捷径方式,以一种不保留填充位的方式(例如,通过不发出本应确保填充位不受影响的指令来实现)。
更糟糕的是,像some_struct_instance = some_other_struct_instance这样的简单赋值(根据定义,是将值存储到some_struct_instance中)不保证填充位的值。不能保证some_struct_instance中的填充位将被设置为与some_other_struct_instance中的填充位相同的位值,也不能保证some_struct_instance中的填充位将保持不变。这是因为编译器允许以任何它认为最“有效”的方式实现赋值(例如,逐个成员分配一组内存、一组成员分配或其他方式),但是由于赋值后填充位的值未指定,因此不需要确保填充位不变。
如果你幸运的话,并且调整填充位可以达到你的目的,那不是因为C标准的支持,而是因为编译器供应商的好意(例如,选择发出一组机器指令来确保填充位不变)。从实际角度来看,没有保证编译器供应商会继续以相同的方式进行操作——例如,在更新编译器、选择不同的优化设置或其他情况下,依赖此类事情的代码可能会出现问题。
既然你第一个问题的答案是“不”,那么就没有必要回答你的第二个问题了。然而,从哲学上讲,如果试图将数据存储在结构体的填充位中,则有理由断言其他人——无论是否疯狂——可能会尝试使用一种混乱的方法来传递你正在尝试传递的数据。

1

从标准规范的第一句话开始:

C允许实现向结构体(但不包括数组)中插入填充以确保所有字段具有有用的对齐方式...

这意味着为了优化(可能是为了优化速度,但也是为了避免数据/地址总线上的架构限制),编译器可以利用隐藏的未使用的位或字节。它们是未使用的,因为禁止或花费昂贵来寻址。

这也意味着这些字节或位不应从编程角度可见,并且尝试访问这些隐藏数据应被视为编程错误。

关于这些添加的数据,标准规定其内容为“未指定”,并没有更好的方法来说明实现可以对它们做什么。想想那些位域声明,您可以声明任何位宽的整数:没有正常的硬件会允许在内存中读取/写入小于8位的块,因此CPU将始终读取或写入至少8位(有时甚至更多)。为什么编译器(实现)要关心那些程序员指定他不关心的其他位的有用性呢?这是无意义的:程序员没有给某个内存地址命名,但是他想操作它?

字段之间的填充字节与以前基本相同:这些添加的字节是必要的,但程序员不感兴趣 - 他不应该在以后改变主意!

当然,可以研究实现并得出一些结论,比如“填充字节将始终为零”之类的。这很冒险(您确定它们总是会被清零吗?),但更重要的是,它完全没有用处:如果需要在结构中添加更多数据,只需声明即可!您将永远不会遇到任何问题,即使将源代码移植到不同的平台或实现。


1
完全同意您的观点。然而,在这种情况下,没有改变这个数据结构的可能性。 - z0lupka
@z0lupka 说实话,我也想过这些情况。但是我决定保持沉默... :-) - linuxfan says Reinstate Monica

0

从期望标准正确实现的角度出发是合理的。您正在寻找特定架构的进一步保证。就个人而言,如果我能找到有关该特定架构的详细文档信息,我会感到放心;如果没有,我会持谨慎态度。

所谓“谨慎”取决于我需要多大的信心。例如,构建一个详细的测试集并定期在目标架构上运行可以给我足够的信心,但这完全取决于您想承担多少风险。如果非常重要,请坚持标准保证;如果不那么重要,请进行测试并查看是否可以获得足够的信心以满足您的需求。


2
总结一下。 我们没有从标准中得到任何保证。因此,不幸的是,必须清楚地研究编译器的行为。并且每次升级编译器版本时都要重新检查编译器文档。 我说得对吗? - z0lupka
2
你绝对不能依赖编译器发出修改填充位的代码,当相邻字段被修改时,因为这比保留位的值更快。我认为任何数量的测试都没有帮助,因为没有办法知道什么样的情况可能导致这样的优化是可行的。 - rici
@rici “你绝对不能依赖……我认为任何数量的测试都没有用”这取决于您需要多么确切。如果您需要绝对、完全的确信,那么您是正确的——您不能依赖它。如果您能够承受不绝对确信的情况,并且您准备将其视为一种风险,而您又能够管理这种风险,那么适当的测试将帮助您获得所需的信心水平。如果您采取这种方法,比如说将执行成本减半,并且测试显示在一万次操作中有一次会出现错误的颜色,或者产生稍微长一点的行程(接下来) - Tim
对于你的卫星导航系统,你可能会决定冒那个计算出来的风险是值得的。 - Tim

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