以下C联合访问模式是否为未定义行为?

时间:2018-09-12 08:11:23

标签: c language-lawyer undefined-behavior unions type-punning

以下不是现代C语言中未定义的行为:

union foo
{
    int i;
    float f;
};
union foo bar;
bar.f = 1.0f;
printf("%08x\n", bar.i);

并打印1.0f的十六进制表示形式。

但是以下是未定义的行为:

int x;
printf("%08x\n", x);

那呢?

union xyzzy
{
    char c;
    int i;
};
union xyzzy plugh;

这应该是未定义的行为,因为尚未写入plugh的成员。

printf("%08x\n", plugh.i);

那呢。这是未定义的行为吗?

plugh.c = 'A';
printf("%08x\n", plugh.i);

当今大多数C编译器将使用sizeof(char) < sizeof(int),其中sizeof(int)是2或4。这意味着在这种情况下,plugh.i的最多50%或25%将被使用。写入,但读取剩余字节将读取未初始化的数据,因此应为未定义的行为。基于此,整个读取的行为是否不确定?

5 个答案:

答案 0 :(得分:7)

Defect report 283: Accessing a non-current union member ("type punning")涵盖了这一点,并告诉我们如果存在陷阱表示,则存在未定义的行为。

缺陷报告问:

  

在与6.5.2.3#5相对应的段落中,C89包含此内容   句子:

     
    

除了一个例外,如果在将值存储在对象的其他成员之后访问并集对象的成员,则     行为是由实现定义的。

  
     

与该句子相关的是这个脚注:

     
    

标量类型的“字节顺序”对于不沉迷于punning类型(例如,通过     分配给工会的一名成员并检查     访问另一个成员,该成员是适当的数组     字符类型),但必须符合     外部施加的存储布局。

  
     

C99 is 6.2.6.1#7中唯一的对应词:

     
    

当值存储在联合类型的对象的成员中时,与该对象表示形式不符的对象表示形式的字节     成员,但对应于其他成员,但未指定值,     联合对象的值不应因此成为陷阱     表示。

  
     

目前尚不清楚C99单词是否具有相同的含义   暗示为C89字。

缺陷报告添加了以下脚注:

  

在6.5.2.3#3中的“命名成员”一词上添加新的脚注78a:

     

78a 如果用于访问并集对象内容的成员与上次用于在对象中存储值的成员不同,则按照6.2.6中的描述(一种有时称为“类型调整”的过程)将“新对象”重新解释为新类型的对象表示。 这可能是陷阱的表示形式。

C11 6.2.6.1 General告诉我们:

  

某些对象表示不必表示对象类型的值。如果对象的存储值具有这样的表示形式,并且由不具有字符类型的左值表达式读取,则行为是不确定的。如果这种表示形式是由修改后的副作用产生的 50) 这样的表示形式称为陷阱表示。 >

答案 1 :(得分:4)

来自6.2.6.1§7:

  

当值存储在联合类型的对象的成员中时,与该成员不对应但与其他成员对应的对象表示形式的字节采用未指定的值。

因此,在设置plugh.i之后,plugh.c的值将无法指定。

从脚注到6.5.2.3§3:

  

如果用于读取联合对象的内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分将重新解释为对象中的对象表示。 6.2.6中描述的新类型(有时称为“类型调整”的过程)。这可能是陷阱的表示形式。

这表示特别允许使用punning类型(如您在问题中所断言的那样)。但这可能会导致陷阱表示,在这种情况下,根据6.2.6.1§5读取值具有未定义的行为:

  

某些对象表示不必表示对象类型的值。如果对象的存储值具有这种表示形式,并且由不具有字符类型的左值表达式读取,则该行为是不确定的。如果这样的表示是由副作用产生的,该副作用通过不具有字符类型的左值表达式修改对象的全部或任何部分,则该行为是不确定的。 50)这种表示称为   陷阱表示。

如果它不是陷阱表示,那么标准中似乎没有任何东西可以使这种未定义的行为发生,因为从4§3中,我们得到:

  

在所有其他方面都是正确的,使用正确的数据运行,包含未指定行为的程序,应该是正确的程序,并按照5.1.2.3进行操作。

答案 2 :(得分:3)

其他答案解决了以下主要问题:在未初始化plugh.i且仅分配了plugh时,读取plugh.c是否会产生未定义的行为。简而言之:否,除非plugh.i的字节在读取时构成陷阱表示。

但是我想直接谈谈这个问题的初步断言:

  

当今大多数C编译器将具有sizeof(char) < sizeof(int),其中包括   sizeof(int)是2或4。这意味着在这些情况下,   plugh.i中的大多数50%或25%将被写入

问题似乎是假设为plugh.c分配一个值将使plugh的那些与c不对应的字节不受干扰,但是标准绝不支持主张。实际上,它明确否认任何此类保证,因为其他人已经指出:

  

当值存储在联合类型的对象的成员中时,   与该对象表示形式不符的字节   成员,但确实与其他成员相对应,但未指定值

({C2011, 6.2.6.1/7;已添加重点)

尽管这不能保证这些字节采用的未指定值与赋值之前的值不同,但明确规定它们可能是不同的。在某些实现中它们经常会是完全合理的。例如,在仅支持对字大小进行写入的内存的平台上,或者在这种写入比字节大小的写入更有效的平台上,很可能plugh.c的分配是通过字大小的写入来实现的,而无需首先加载plugh.i的其他字节,以保留其值。

答案 3 :(得分:1)

C11§6.2.6.1p7说:

  

当值存储在联合类型的对象的成员中时,   与该对象表示形式不符的字节   成员,但确实与其他成员相对应,采用未指定值。

因此,plugh.i未指定。

答案 4 :(得分:0)

如果有用的优化可能导致程序执行的某些方面以与标准不一致的方式运行(例如,连续两次读取同一字节会产生不一致的结果),则标准通常会尝试刻画可能影响此类结果的情况被观察,然后将这些情况归类为“调用未定义行为”。它不花太多精力来确保其特征不会“诱捕”某些行为,这些行为的行为显然应该可以预测地进行处理,因为它希望编译器编写者避免在这种情况下表现得很钝。

不幸的是,在某些极端情况下,这种方法确实行不通。例如,考虑:

struct c8 { uint32_t u; unsigned char arr[4]; };
union uc { uint32_t u; struct c8 dat; } uuc1,uuc2;

void wowzo(void)
{
  union uc u;
  u.u = 123;
  uuc1 = u;
  uuc2 = u;
}

我认为很明显,该标准不要求uuc1.dat.arruuc2.dat.arr中的字节包含任何特定值,并且对于四个字节i =的每个字节,都允许编译器进行操作。 = 0..3,将uuc1.dat.arr[i]复制到uuc2.dat.arr[i],将uuc2.dat.arr[i]复制到uuc1.dat.arr[i],或者将uuc1.dat.arr[i]uuc2.dat.arr[i]写入匹配的值。我不知道标准是否打算要求编译器选择这些操作之一,而不是仅仅让这些​​字节保留它们所要保留的字节。

很明显,如果没有人观察到uuc1.dat.arruuc2.dat.arr的内容,那么代码应该具有完全定义的行为,也没有建议检查这些数组的调用UB。此外,没有定义的手段可以通过u.dat.arr的值在对uuc1uuc2的分配之间进行更改。这表明uuc1.dat.arruuc2.dat.arr应该包含匹配值。另一方面,对于某些类型的程序,将毫无意义的数据存储到uuc1.dat.arr和/或uuc1.dat.arr中几乎不会起到任何有用的作用。我认为标准的作者并不是特别打算要求这样的存储,但是说字节采用“未指定”值使它们成为必需。我希望这样的行为保证会被弃用,但我不知道有什么可以替代它。