如果a未初始化,那么a^a或a-a是未定义的行为吗?

78

考虑这个程序:

#include <stdio.h>

int main(void)
{
    unsigned int a;
    printf("%u %u\n", a^a, a-a);
    return 0;
}

这是未定义行为吗?

乍一看,a 是一个未初始化的变量。因此,这指向了未定义行为。但是,对于所有的 a 值,a^aa-a 都等于 0,至少我认为是这样的。是否可能有某种方式来证明这个行为是良好定义的?


我期望这个问题能够被明确定义,因为a的值是未知但固定的,而且不应该改变。问题在于编译器是否会分配a的空间,并随后从那里读取垃圾。如果没有,那么行为就是未定义的。 - martin
只要这个变量没有被标记为“volatile”,那么我认为这是定义良好的行为。 "a ^= a"与"a = 0"完全等效。 - fileoffset
30
@martin: 它并非固定的。该值允许改变。这是一个非常实际的考虑因素。一个变量可以被分配到CPU寄存器,但在它未初始化时(即它的有效价值寿命尚未开始),同一CPU寄存器可能被其他变量占用。那个其他变量的更改将被视为此未初始化变量的“不稳定”值。这是在实践中经常观察到的未初始化变量的情况。 - AnT stands with Russia
1
没事了,我找到了,是我的错误:http://stackoverflow.com/questions/20300665/output-of-the-expression-36aa-in-c-language,而且它实际上是针对C语言的。 - Thomas
@Thomas 是的,那些都看起来非常相似。但是大部分讨论都在评论中进行,而问题是关于 ^ 的含义,UB 只是意外和附带的问题。这里问题的重点都在于 UB。 - David Heffernan
显示剩余5条评论
3个回答

77
在C11中:
  • 如果a没有被取地址,它将根据6.3.2.1/2明确未定义(如下所引用)
  • 它可能是陷阱表示(访问时会导致UB)。 根据6.2.6.1/5:

某些对象表示不需要表示对象类型的值。

无符号整数可以具有陷阱表示(例如,如果它有15位精度和1个奇偶校验位,则访问a可能会导致奇偶校验错误)。

6.2.4/6表示初始值是“未确定的”,其在3.19.2下的定义为“未指定的值或陷阱表示”。

进一步地,在C11 6.3.2.1/2中,正如Pascal Cuoq指出的:

如果lvalue指定了具有可以声明为寄存器存储类别(从未取其地址)的自动存储期对象,并且该对象未初始化(未使用初始化程序声明且在使用之前未对其进行分配),则其行为未定义。

这没有字符类型的例外,因此该条款似乎取代了前面的讨论; 即使不存在陷阱表示,访问x也立即未定义。 C11添加了此条款以支持具有注册表陷阱状态的Itanium CPU。


没有陷阱表示的系统:但是,如果我们添加&x;,使6.3.2.1/2的异议不再适用,并且我们处于已知没有陷阱表示的系统上,则该值为“未指定的值”。

3.19.3中未指定值的定义有些模糊,但DR 451对此进行了澄清,得出以下结论:
  • 在所描述的情况下,未初始化的值可能会看起来改变其值。
  • 对不确定值执行的任何操作都将具有不确定值作为结果。
  • 当对不确定值使用库函数时,库函数将表现出未定义的行为。
  • 这些答案适用于所有没有陷阱表示的类型。

根据此解决方案,int a; &a; int b = a - a;使得b仍然具有不确定值。

请注意,如果不确定值未传递给库函数,则仍处于未指定行为的范围内(而非未定义行为)。结果可能很奇怪,例如if ( j != j ) foo();可能会调用foo,但恶魔必须保持在鼻腔中。


假设我们知道没有陷阱值,那么我们能够推断出定义良好的行为吗? - David Heffernan
16
@DavidHeffernan,你最好把访问不确定数据视为未定义行为,因为即使没有陷阱值,你的编译器也可能这样做。请参阅http://blog.frama-c.com/index.php?post/2013/03/13/indeterminate-undefined。 - Pascal Cuoq
@Pascal 我明白了。那是安德烈回答的最后一段话。 - David Heffernan
@DavidHeffernan 的示例只说明 2 * j 是奇数的情况,这比 Andrey 回答中的图片稍微差一些,但你可以理解其中的意思。 - Pascal Cuoq
当C89标准被编写时,预计实现将指定许多标准未涉及的内容,标准的作者们认为没有必要详细说明在指定某些内容的实现中应该被视为已定义操作的所有情况(例如,“unsigned int”没有陷阱表示的事实),但在不指定这些内容的实现中是未定义的(例如,将不确定的位模式读取为“unsigned int”可能会产生陷阱表示)。 - supercat

33

是的,这是未定义行为。

首先,任何未初始化的变量都可能具有“损坏”的(也称为“陷阱”)表示。即使只有一次尝试访问该表示,也会触发未定义行为。此外,即使是非陷阱类型(如unsigned char)的对象仍然可以获取特殊的平台相关状态(例如在Itanium上的NaT-Not-A-Thing),这可能会出现作为它们的“不确定值”的表现。

其次,未初始化的变量不能保证具有稳定的值。对同一未初始化变量进行两次连续访问可以读取完全不同的值,这就是为什么即使a-a中的两次访问都是“成功的”(不会出现陷阱),也不能保证a-a将计算为零。


1
你有最后一段的引用吗?如果是这样,那么我们甚至不需要考虑陷阱。 - David Heffernan
2
@Matt McNabb:这可能是在不同版本的语言规范中以不同方式解决的问题。但DR#260(http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_260.htm)的解决方案明确指出,具有不确定值的变量可以“自行”任意更改。 - AnT stands with Russia
4
@Matt McNabb: DR#451在2013年10月和2014年4月分别重新确认了与DR#260基本相同的决定。http://www.open-std.org/Jtc1/sc22/WG14/www/docs/dr_451.htm。针对DR#451的委员会回复明确指出:“这一立场重申了C99 DR260的立场”。 - AnT stands with Russia
1
@hyde,你可能最接近 trap representation 的方式是使用 signaling NaNs。http://en.wikipedia.org/wiki/NaN#Signaling_NaN 否则,你需要使用带有显式奇偶校验位的计算机,一个将 -0 视为陷阱值的符号-幅度计算机,或者其他同样奇特的东西。 - Pascal Cuoq
1
@chux:不对。没有任何限制未定义行为只能是“按你所想的方式执行,如果不行,则陷入错误”。实际上,任何行为都是允许的。 - Ben Voigt
显示剩余5条评论

2

如果一个对象具有自动存储期并且其地址没有被获取,尝试读取它将导致未定义行为。取该对象的地址并使用类型为“unsigned char”的指针读取它的字节,则标准保证将产生“unsigned char”类型的值,但并不是所有编译器都遵循该标准。例如,ARM GCC 5.1在给定以下内容时:

  #include <stdint.h>
  #include <string.h>
  struct q { uint16_t x,y; };
  volatile uint16_t zz;
  int32_t foo(uint32_t x, uint32_t y)
  {
    struct q temp1,temp2;
    temp1.x = 3;
    if (y & 1)
      temp1.y = zz;
    memmove(&temp2,&temp1,sizeof temp1);
    return temp2.y;
  }

如果y为零,将生成返回x的代码,即使x超出0-65535的范围。 标准明确指出,对于不确定值的无符号字符读取保证产生一个在unsigned char范围内的值,并且memmove的行为被定义为等同于一系列字符读取和写入。 因此,temp2应该有一个值,可以通过一系列字符写入存储到其中,但是gcc决定用一个赋值替换memmove,并忽略代码取得temp1和temp2地址的事实。

在任何可接受任意值的情况下,强制编译器将变量视为持有其类型的任意值的方法将会很有帮助,但标准没有指定清洁的方法来实现这一点(除了存储某些特定值,这可能是不必要的缓慢)。 即使是应该逻辑上强制变量保存可以表示为某些位组合的值的操作也不能保证在所有编译器上都有效。 因此,这种变量的任何有用信息都无法保证。


@BeeOnRope:如果标准的作者们包含了一种将不确定值解析为最多未指定值的方法,那么在将否则不确定的值传递给库函数之前要求使用这种方法是合理的。鉴于缺乏这样的手段,我能从他们的决定中读出的唯一信息是,他们更关心使语言“易于优化”,而不是最大化其实用性。 - supercat
1
我认为,他们可能引入了一种T std::freeze(T v)方法,将“摇摆不定”的不确定值转换为未指定但稳定的值。它将具有“三阶”实用性:使用不确定值已经很模糊且很少使用,因此添加一个特殊结构来巩固这些值似乎只是进一步深入标准中已经是一个模糊角落的兔子洞,而且它必须在许多编译器的核心转换/优化阶段得到支持。 - BeeOnRope
尽管在大多数实际硬件上,“std::memory_order_relaxed”可以确切地得到您想要的结果。 - BeeOnRope
@BeeOnRope 关于"wobbly"的问题:赞成。请参考相关的 DRs: [451] (https://www.open-std.org/Jtc1/sc22/WG14/www/docs/dr_451.htm),以及(额外的)[260](https://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_260.htm)。 - pmor
@pmor:标准中最基本的问题,也是导致大多数争议的原因,是它未能容纳可能导致行为与顺序执行模型不一致的优化思想,或者认识到某些任务需要比其他任务更强的行为保证。将行为被定义为“任何事情都可能发生”的操作分类为UB只允许更多的优化,而在程序无法做任何事情的情况下允许更有限的自由度会更好。 - supercat
显示剩余8条评论

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