对于一堂课,我想向学生展示goto
的未定义行为。我想出了以下程序:
#include <stdio.h>
int main()
{
goto x;
for(int i = 0; i < 10; i++)
x: printf("%d\n", i);
return 0;
}
我希望编译器(gcc
版本4.9.2)警告我i
访问未定义的行为,但没有警告,甚至没有:
gcc -std=c99 -Wall -Wextra -pedantic -O0 test.c
运行程序时,i
显然已初始化为零。为了理解发生了什么,我使用第二个变量j
扩展了代码:
#include <stdio.h>
int main()
{
goto x;
for(int i = 0, j = 1; i < 10; i++)
x: printf("%d %d\n", i, j);
return 0;
}
现在编译器警告我在没有初始化的情况下访问j
。我理解这一点,但为什么i
也没有未初始化?
答案 0 :(得分:2)
现在编译器警告我,我正在访问j而不是它 初始化。我理解这一点,但为什么我没有被初始化为 好?
有未定义行为的点,它有时可以工作,或不工作,或部分工作,或打印垃圾。问题在于你无法知道你的编译器究竟在做什么,而不是编译器产生不一致结果的错误,因为正如你承认的那样,行为是未定义的。
在那一点上,唯一能保证的是,没有任何保证可以证明这一点。不同的编译器甚至可能会给出不同的结果,或者可能会有不同的优化级别。
编译器也不需要检查它,并且它不需要处理它,因此编译器不需要。无论如何,您无法使用编译器可靠地检查未定义的行为。这是什么单元测试和许多测试用例或统计分析。
答案 1 :(得分:2)
未定义的行为是一种运行时现象。因此,编译器很少能够为您检测它。在超出编译器范围的事情时,会调用大多数未定义行为的情况。
为了使事情变得更复杂,编译器可能会优化代码。假设它决定将i
放在CPU寄存器中但j
放在堆栈上,反之亦然。然后假设在调试构建期间,它将所有堆栈内容设置为零。
如果需要可靠地检测未定义的行为,则需要一个静态分析工具,该工具可以检查编译器完成的工作。
答案 2 :(得分:2)
根据C标准,使用“goto”跳过变量初始化将允许编译器做任何它想要的事情,即使在通常会产生不确定值但不具有任何其他值的不确定值的平台上副作用。在这种情况下,gcc的行为似乎没有像在例如整数溢出,但它的优化可能有点有趣虽然良性。给出:
int test(int x)
{
int y;
if (x) goto SKIP;
y=x+1;
SKIP:
return y*2;
}
int test2(unsigned short y)
{
int q=0;int i;
for (i=0; i<=y; i++)
q+=test(i);
return q;
}
编译器将观察到在所有已定义的情况下,test将返回2,因此可以通过生成test2的代码来消除循环:
int test2(unsigned short y)
{
return (int)y << 1;
}
然而,这样的例子可能给人一种印象,即编译器以良性的方式对待UB。不幸的是,在gcc的情况下,这一般不再正确。过去,在没有硬件陷阱的机器上,编译器会将Indeterminate Value的使用视为简单地产生任意值,这些值可能会或可能不会以任何一致的方式运行,但没有任何其他副作用。我不确定使用goto
跳过变量初始化的任何情况还会导致副作用,而不是在变量中有无意义的值,但这并不意味着作者gcc将来不会决定利用这种自由。