考虑以下程序:
#include <stdio.h>
int main(void)
{
unsigned int a;
printf("%u %u\n", a^a, a-a);
return 0;
}
它是不确定的行为吗?
表面上a
是一个未初始化的变量。因此,这指向未定义的行为。但是a^a
和a-a
等于0
的所有值a
,至少我认为是这种情况。是否有某种方法可以证明行为已被明确定义?
考虑以下程序:
#include <stdio.h>
int main(void)
{
unsigned int a;
printf("%u %u\n", a^a, a-a);
return 0;
}
它是不确定的行为吗?
表面上a
是一个未初始化的变量。因此,这指向未定义的行为。但是a^a
和a-a
等于0
的所有值a
,至少我认为是这种情况。是否有某种方法可以证明行为已被明确定义?
volatile
我就将其视为已定义的行为。a ^= a
,完全等于a = 0
Answers:
在C11中:
a
从未获取其地址,则根据6.3.2.1/2明确地未定义它(在下面引用)某些对象表示形式不必表示对象类型的值。
无符号整数可以具有陷阱表示形式(例如,如果它具有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,但是恶魔必须紧贴着鼻腔。
2 * j
奇怪,甚至比Andrey的答案中的图片还要差,但您明白了。
是的,这是未定义的行为。
首先,任何未初始化的变量都可以具有“中断”(也称为“陷阱”)表示。甚至单次尝试访问该表示形式都会触发未定义的行为。而且,即使非陷阱类型的对象(如unsigned char
)仍可以获取特殊的平台相关状态(如Itanium上的NaT-Not-A-Thing-),这可能表示其“不确定的价值”。
其次,不能保证未初始化的变量具有稳定的值。对同一未初始化变量的两次顺序访问可以读取完全不同的值,这就是为什么即使两次访问a - a
都“成功”(不捕获)的原因,也仍然不能保证其a - a
评估结果为零。
如果对象具有自动存储期限,并且未使用其地址,则尝试读取该对象将产生未定义行为。获取此类对象的地址并使用“ 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在0到65535范围之外,如果y为零,它将返回x。该标准明确规定,不确定值的无符号字符读取保证会产生范围内的值unsigned char
,并且的行为memmove
被定义为等效于一系列字符读取和写入。因此,temp2应该具有一个可以通过字符写入序列存储到其中的值,但是gcc决定用一个赋值替换memmove,而忽略了代码使用了temp1和temp2地址的事实。
有一种方法可以强制编译器将变量视为持有其类型的任意值,以防该变量值被同样接受,这会有所帮助,但是标准并未指定这样做的明确方法(保存用于存储某些特定的值,该值会起作用,但通常会不必要地变慢)。即使在逻辑上应强制变量保留一个可以表示的值(因为某些位组合)的操作也不能依赖于所有编译器。因此,不能保证任何有关此类变量的有用信息。
memmove
是一个库函数,因此将在此处应用。
T std::freeze(T v)
方法,可以将“不确定的”不确定值变成不确定但稳定的值。但是,它具有“三阶”的用途:使用不确定的值已经很模糊了,很少使用,因此添加一个特殊的结构来巩固这些值似乎就已经远远超过了已经模糊的角落。该标准,许多编译器的核心转换/优化阶段都必须支持该标准。
foo=moo; if (foo < 100) bar(foo);
,并moo
得到一些其他线程意外更改,当试图诊断和那里的东西出了问题可以是基本不可能的。能够说出foo=moo; freeze(foo); if (foo < 100) bar(foo);
并让编译器承诺使用某个值foo
将使事情变得更加健壮。
a
坐在那里的垃圾分配空间并随后从中读取。如果不是,则行为是不确定的。