考虑这个程序:
#include <stdio.h>
int main(void)
{
unsigned int a;
printf("%u %u\n", a^a, a-a);
return 0;
}
这是未定义行为吗?
乍一看,a
是一个未初始化的变量。因此,这指向了未定义行为。但是,对于所有的 a
值,a^a
和 a-a
都等于 0
,至少我认为是这样的。是否可能有某种方式来证明这个行为是良好定义的?
考虑这个程序:
#include <stdio.h>
int main(void)
{
unsigned int a;
printf("%u %u\n", a^a, a-a);
return 0;
}
这是未定义行为吗?
乍一看,a
是一个未初始化的变量。因此,这指向了未定义行为。但是,对于所有的 a
值,a^a
和 a-a
都等于 0
,至少我认为是这样的。是否可能有某种方式来证明这个行为是良好定义的?
a
没有被取地址,它将根据6.3.2.1/2明确未定义(如下所引用)某些对象表示不需要表示对象类型的值。
无符号整数可以具有陷阱表示(例如,如果它有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的异议不再适用,并且我们处于已知没有陷阱表示的系统上,则该值为“未指定的值”。
根据此解决方案,int a; &a; int b = a - a;
使得b
仍然具有不确定值。
请注意,如果不确定值未传递给库函数,则仍处于未指定行为的范围内(而非未定义行为)。结果可能很奇怪,例如if ( j != j ) foo();
可能会调用foo,但恶魔必须保持在鼻腔中。
2 * j
是奇数的情况,这比 Andrey 回答中的图片稍微差一些,但你可以理解其中的意思。 - Pascal Cuoq是的,这是未定义行为。
首先,任何未初始化的变量都可能具有“损坏”的(也称为“陷阱”)表示。即使只有一次尝试访问该表示,也会触发未定义行为。此外,即使是非陷阱类型(如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为零,将生成返回x的代码,即使x超出0-65535的范围。 标准明确指出,对于不确定值的无符号字符读取保证产生一个在unsigned char范围内的值,并且memmove的行为被定义为等同于一系列字符读取和写入。 因此,temp2应该有一个值,可以通过一系列字符写入存储到其中,但是gcc决定用一个赋值替换memmove,并忽略代码取得temp1和temp2地址的事实。
在任何可接受任意值的情况下,强制编译器将变量视为持有其类型的任意值的方法将会很有帮助,但标准没有指定清洁的方法来实现这一点(除了存储某些特定值,这可能是不必要的缓慢)。 即使是应该逻辑上强制变量保存可以表示为某些位组合的值的操作也不能保证在所有编译器上都有效。 因此,这种变量的任何有用信息都无法保证。
T std::freeze(T v)
方法,将“摇摆不定”的不确定值转换为未指定但稳定的值。它将具有“三阶”实用性:使用不确定值已经很模糊且很少使用,因此添加一个特殊结构来巩固这些值似乎只是进一步深入标准中已经是一个模糊角落的兔子洞,而且它必须在许多编译器的核心转换/优化阶段得到支持。 - BeeOnRope
a
的空间,并随后从那里读取垃圾。如果没有,那么行为就是未定义的。 - martin^
的含义,UB 只是意外和附带的问题。这里问题的重点都在于 UB。 - David Heffernan