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


76

考虑以下程序:

它是不确定的行为吗?

表面上a是一个未初始化的变量。因此,这指向未定义的行为。但是a^aa-a等于0的所有值a,至少我认为是这种情况。是否有某种方法可以证明行为已被明确定义?


我希望可以很好地定义它,因为a的值是未知的但固定的,并且不应更改。问题是,编译器是否会为a坐在那里的垃圾分配空间并随后从中读取。如果不是,则行为是不确定的。
马丁

嗯,只要未标记变量,volatile我就将其视为已定义的行为。a ^= a,完全等于a = 0
2014年

30
@马丁:这不是固定的。该值允许更改。这是一个非常实际的考虑。可以将一个变量分配给一个CPU寄存器,但是虽然尚未初始化(即其有效值有效期尚未开始),但相同的CPU寄存器可以被另一个变量占用。该其他变量中的更改将被视为此未初始化变量的“不稳定”值。在实践中通常会使用未初始化的变量观察到这一点。
AnT

@AndreyT这是一个很好的解释
马丁

1
没关系,发现它是我的错误:stackoverflow.com/questions/20300665/…,实际上是C的
Thomas

Answers:


73

在C11中:

  • 如果a从未获取其地址,则根据6.3.2.1/2明确地未定义它(在下面引用)
  • 它可能是陷阱表示(访问时会导致UB)。6.2.6.1/5:

某些对象表示形式不必表示对象类型的值。

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

6.2.4 / 6说初始值是不确定的,并且在3.19.2中的定义是未指定值或陷阱表示

此外:正如Pascal Cuoq所指出的,在C11 6.3.2.1/2中:

如果左值指定了可以使用寄存器存储类声明的自动存储持续时间的对象(从未使用其地址),并且该对象未初始化(未使用初始化器声明,并且在使用前未对其进行任何赋值) ),则行为未定义。

字符类型也不例外,因此该子句似乎取代了前面的讨论;x即使不存在陷阱表示,访问也是立即未定义的。此子句已添加到C11中,以支持Itanium CPU,这些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

15
@DavidHeffernan您最好将对不确定数据的访问与UB一样对待,因为即使没有陷阱值,编译器也可能会这样做。请参阅blog.frama-c.com/index.php?post/2013/03/13/…–
Pascal Cuoq

@Pascal我现在明白了。那是安德烈答案的最后一个段落。
David Heffernan

@DavidHeffernan例子很2 * j奇怪,甚至比Andrey的答案中的图片还要差,但您明白了。
Pascal Cuoq

编写C89标准时,可以预期实现会指定该标准未指定的许多内容,并且该标准的作者认为没有理由详细说明应考虑对指定某些内容的实现定义动作的所有情况(例如,“ unsigned int”没有陷阱表示)但在没有实现的情况下则未定义(例如,将不确定的位模式读取为“ unsigned int”可能会产生陷阱表示)。
超级猫

32

是的,这是未定义的行为。

首先,任何未初始化的变量都可以具有“中断”(也称为“陷阱”)表示。甚至单次尝试访问该表示形式都会触发未定义的行为。而且,即使非陷阱类型的对象(如unsigned char)仍可以获取特殊的平台相关状态(如Itanium上的NaT-Not-A-Thing-),这可能表示其“不确定的价值”。

其次,不能保证未初始化的变量具有稳定的值。对同一未初始化变量的两次顺序访问可以读取完全不同的值,这就是为什么即使两次访问a - a都“成功”(不捕获)的原因,也仍然不能保证其a - a评估结果为零。


1
您有最后一段的引文吗?如果是这样,那么我们甚至不需要考虑陷阱。
David Heffernan

2
@Matt McNabb:嗯,这可能是通过语言规范的不同解决方案以不同方式解决的问题。但是DR#260的解决方案(open-std.org/jtc1/sc22/wg14/www/docs/dr_260.htm)明确指出,具有不确定值的变量可以“自行”任意更改。
AnT

4
@Matt McNabb:DR#451在2013年10月和2014年4月open-std.org/Jtc1/sc22/WG14/www/docs/dr_451.htm中从DR#260再次重申了相同的决定。对DR#451的确认响应明确指出“此观点重申了C99 DR260的位置”
AnT 2014年

1
@hyde您可能最接近陷阱表示形式的信号是NaN。en.wikipedia.org/wiki/NaN#Signaling_NaN否则,您需要获得一台具有显式奇偶校验位的计算机,一台信号量级计算机(其中-0被视为陷阱值)或同等奇异的东西。
Pascal Cuoq 2014年

1
@chux:否。没有什么可以将未定义的行为限制为“执行您的想法,但如果不是,则陷阱”。从字面上看,任何行为都是允许的。
Ben Voigt

1

如果对象具有自动存储期限,并且未使用其地址,则尝试读取该对象将产生未定义行为。获取此类对象的地址并使用“ unsigned char”类型的指针读出其字节,标准保证了产生“ unsigned char”类型的值,但并非所有编译器都遵守该标准。例如,在给出ARM GCC 5.1的情况下:

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

有一种方法可以强制编译器将变量视为持有其类型的任意值,以防该变量值被同样接受,这会有所帮助,但是标准并未指定这样做的明确方法(保存用于存储某些特定的值,该值会起作用,但通常会不必要地变慢)。即使在逻辑上应强制变量保留一个可以表示的值(因为某些位组合)的操作也不能依赖于所有编译器。因此,不能保证任何有关此类变量的有用信息。


公平的说,上面有一个链接的缺陷报告,确切说明了您可以使用不确定值执行的操作,部分决定是指定将不确定值传递给任何库函数是UB。memmove是一个库函数,因此将在此处应用。
BeeOnRope

@BeeOnRope:如果标准的作者包括了将不确定的值解析为最差的未指定值的方法,则在将其他不确定的值传递给库函数之前,要求使用此类方法是合理的。鉴于缺乏这样的手段,我唯一能读懂他们的决定的是他们对使一种语言“易于优化”而不是使一种语言的效用更感兴趣。
超级猫

@BeeOnRope:他们的理由是,使行为未定义不应阻止编译器针对处理器和应用程序字段定义行为,而这样做实际上是有用的。不幸的是,委员会的此类决定是否应具有这种效果,很明显确实如此。
超级猫

我想,是的,他们可能已经引入了某种T std::freeze(T v)方法,可以将“不确定的”不确定值变成不确定但稳定的值。但是,它具有“三阶”的用途:使用不确定的值已经很模糊了,很少使用,因此添加一个特殊的结构来巩固这些值似乎就已经远远超过了已经模糊的角落。该标准,许多编译器的核心转换/优化阶段都必须支持该标准。
BeeOnRope17年

@BeeOnRope:冻结值的能力在那些必不可少的情况下,其成本基本上为零,并且在没有代码的情况下尝试调试优化的代码是通往精神错乱的必经之路。如果一个写操作foo=moo; if (foo < 100) bar(foo);,并moo得到一些其他线程意外更改,当试图诊断和那里的东西出了问题可以是基本不可能的。能够说出foo=moo; freeze(foo); if (foo < 100) bar(foo);并让编译器承诺使用某个值foo将使事情变得更加健壮。
超级猫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.