【问题标题】:What constitutes as padding in a union?什么构成联合中的填充?
【发布时间】:2023-03-29 05:35:01
【问题描述】:

我正在尝试解释 C11 standard 在未显式初始化时关于联合的静态(和线程本地)初始化。

第 6.7.9 节 10(第 139 页)规定如下:

如果具有自动存储持续时间的对象未显式初始化,则其值是不确定的。如果具有静态或线程存储持续时间的对象未显式初始化,则:

——如果是指针类型,则初始化为空指针;

——如果是算术类型,则初始化为(正或无符号)零;

——如果是聚合,每个成员都根据这些规则进行初始化(递归),任何填充都初始化为零位;

——如果是联合,则根据这些规则(递归地)初始化第一个命名的成员,并将任何填充初始化为零位;

假设我们在 amd64 架构上,给定以下语句:

static union { uint32_t x; uint16_t y[3]; } u;

u.y[2] 可以包含非零值,还是因为被视为填充而将其初始化为零?

我已经浏览了 C11 标准,但几乎没有解释什么是联合中的填充。在C99 standard (pg 126) 中没有提到填充,所以在这种情况下u.y[2] 可以是非零的。

【问题讨论】:

  • 你的意思是u.y[3] 我的理解是如果联合需要一些填充,它将被初始化为0。假设你需要在你的架构上进行4字节对齐并且 y[3] 是 6 个字节并且需要 2 个字节对齐,那么最终对齐需要是 8 的倍数,因此您将有 2 个字节填充(基本上在 uy[3])。这将是 0。
  • @PSkocik:我错了,联合可以在任何成员之外的末尾有填充,当一个成员是一个对齐不太严格的数组时,它不会填充更多所需的空间- 另一个成员的严格对齐(并且数组也比另一个成员大,因此其他成员本身不会填充所需的空间)。
  • 在联合本身(而不是其成员)中的填充标准中唯一明确提及的是可能位于末尾的填充,如示例中的 u.y[3]这个问题。作为 C 实现的消费者,我必须将这个问题询问的段落解释为告诉我尾随填充已初始化为零,而不是告诉我 y 中的字节而不是 x 中的字节(因此@ 987654330@) 被初始化为零。作为实现开发人员,我可能会采取预防措施,将所有字节初始化为零。

标签: c language-lawyer c11


【解决方案1】:

y 使用的、x 未使用的额外空间被视为填充。 C11 standard 的第 6.7.2.1p17 节关于“结构和联合说明符”状态:

结构或联合的末尾可能有未命名的填充

y 在您的示例中使用的、x 未使用的字节仍被命名,因此不是填充。

您的示例很可能确实有这个未命名的填充,因为最大的成员占用 6 个字节,但其中一个成员是 uint32_t,它通常需要 4 个字节对齐。事实上,在 gcc 4.8.5 上,这个联合的大小是 8 个字节。所以这个 union 的内存布局是这样的:

            -----  --|       ---|
         0  | 0 |    |          |
            -----    |          |-- y[0]
         1  | 0 |    |          |
            -----    |-- x   ---|
         2  | 0 |    |          |            
            -----    |          |-- y[1]
         3  | 0 |    |          |
            -----  --|       ---|
         4  | 0 |               |
            -----               |-- y[2]
         5  | 0 |               |
            -----            ---|
         6  | 0 |  -- padding
            -----
         7  | 0 |  -- padding
            -----

因此,严格阅读标准,对于没有显式初始化程序的此联合的静态实例:

  • 字节 0 - 3,对应于 x(即第一个命名成员),初始化为 0,导致 x 为 0。
  • 字节 4 - 5,对应于 y[2],保持未初始化并且具有不确定值
  • 字节 6 - 7,对应填充,初始化为 0。

我在 gcc 4.8.5、clang 3.3 和 MSVC 2015 上对此进行了测试,在各种优化设置下,它们都将 all 字节设置为 0。但是,通过严格阅读标准,行为并不能得到保证,因此这些编译器的不同优化设置、它们的不同版本或完全不同的编译器可能会做不同的事情。

从实用的角度来看,编译器只需将静态对象的所有字节设置为 0 即可满足此要求。这当然是假设整数类型没有填充,浮点类型是 IEEE754,NULL 指针的数值为 0。在大多数人可能遇到的大多数系统上,情况都是如此。不是这种情况的系统可能更有可能将这些字节设置为 0 以外的值。所以同样,虽然这些字节可能设置为 0,但不能保证。

要记住的重要一点是,根据 6.7.2.1p16,联合一次只能存储一个成员:

联合的大小足以容纳其最大的成员。 at 的值 大多数成员都可以随时存储在联合对象中。 指向 union 对象,经过适当转换,指向它的每个成员(或者如果一个成员是位 域,然后到它所在的单元),反之亦然。

因此,如果具有静态存储持续时间的union 未初始化,则只有访问 first 成员是安全的,因为它是隐式初始化的成员。

唯一的例外是如果联合包含具有一组公共初始成员的结构,在这种情况下,您可以访问内部结构的任何公共元素。这在第 6.5.2.3p6 节中有详细说明:

一个特殊的保证是为了简化联合的使用:如果联合包含 几个结构共享一个共同的初始序列(见下文),如果联合 对象当前包含这些结构之一,允许检查常见的 它们中任何一个的初始部分,任何地方的完整类型的声明 可见。两个结构共享一个 公共初始序列 如果相应的成员 对于一个或多个序列具有兼容的类型(并且对于位域,具有相同的宽度) 初始成员。

【讨论】:

  • 联合的字节在典型的现实生活系统中最初可能全为零的另一个原因:当静态存储持续时间的对象有效初始化为全零字节时(在大多数常见系统上,这将包括指针和浮点类型,而不仅仅是整数),它可能会被放置在“BSS”数据段中。 (不确定线程​​持续时间。)
【解决方案2】:

Can u.y[2] contain non-zero values or is it initialised to zero because it is regarded as padding?

u.y[2] 不被视为填充。它是数组y 的一个元素,它是联合u 的成员。

联合体的大小仅能容纳其最大成员(为了alignment 的目的,还可以添加其他未命名的尾随填充)。

来自 C 标准#6.7.2.1p17

17 结构或联合的末尾可能有未命名的填充。

联合 u 的最大成员是 uint16_t y[3];。因此,如果联合 u 中有任何填充,那么它将在 uint16_t y[3]; 成员 1) 之后。

根据 C11 标准,具有静态或线程存储持续时间且未显式初始化的联合对象,编译器应初始化第一个命名成员(递归)并将任何填充位为零。因此,您不应对 u.y[2] 值做任何假设,因为编译器只会初始化 union2)first named member,在您的示例中为 uint32_t x,以及任何零位填充 (#6.7.9p10)。

C 标准没有提及有关数据段(已初始化/未初始化)、堆栈、堆等的任何内容。这些都是特定于体系结构/平台的。对于对象初始化,C 标准仅指定将什么初始化为0,什么不初始化,并没有指定哪个存储持续时间对象进入哪个段。标准规范是针对编译器的,一个好的编译器应该遵循它们。通常,0 初始化的静态数据进入 .BSS(由符号开始的块),非 0 初始化的数据进入 .DATA(数据段)。因此,您可能会发现 u.y[2]0 但情况并非总是如此。


1) 每个现代编译器都会根据架构自动使用数据结构填充。一些编译器甚至支持警告标志-Wpadded,它会生成有关结构填充的有用警告。这些警告有助于程序员在需要更有效的数据结构布局时进行手动操作。

-填充

如果结构中包含填充,则发出警告,以对齐结构的元素或对齐整个结构。有时发生这种情况时,可以重新排列结构的字段以减少填充,从而使结构更小。

所以,如果您的编译器支持警告标志-Wpadded,请尝试使用它编译您的代码。这将帮助您理解编译器包含的填充。

例如

#include <inttypes.h>

int main() {
        static union { uint32_t x; uint16_t y[3]; } u;
}

让我们用-Wpadded 选项编译它。我的编译器是clang 版本clang-1000.10.44.4

# clang -Wpadded p.c

p.c:4:16: warning: padding size of 'union (anonymous at p.c:4:16)' with 2 bytes to alignment boundary [-Wpadded]
        static union { uint32_t x; uint16_t y[3]; } u;
               ^
1 warning generated.

2) 需要注意的一点——如果你显式初始化一个联合对象,除非它被指定初始化,那么联合的第一个成员也会被初始化(C11标准#6.7.9p17)。

【讨论】:

    【解决方案3】:

    如果存储是自动的,它可能包含任何值,因为它没有被初始化。 如果存储是静态的,它将初始化为零。

    填充不会影响您的联合,因为它不属于结构或联合的任何成员。

    例如,如果在您的实现中将数据填充到 8 字节边界,则根本不会添加任何填充。这个联合和下一个对象之间会有 2 个字节的间隙。

    【讨论】:

    • “例如,如果在您的实现中将数据填充到 8 字节边界,则根本不会添加填充。此联合和下一个对象之间将有 2 字节间隙。” - 这根本不是真的。在您引用的情况下,联合实例的大小将为8See it live。填充是实例的一部分;没有在一些虚空中被推到他们之间。所以要么你的陈述不准确,要么没有准确传达,需要一些润色。
    • @WhozCraig 但它不是标准意义上的填充。这个留待实施。 ideone.com/krfqGr
    猜你喜欢
    • 2018-02-10
    • 1970-01-01
    • 2017-03-14
    • 2018-05-08
    • 2021-10-15
    • 2018-06-06
    • 2013-05-20
    • 2014-06-07
    • 2018-01-20
    相关资源
    最近更新 更多