我不确定完全了解未定义行为可能危及程序的程度。
假设我有这段代码:
#include <stdio.h>
int main()
{
int v = 0;
scanf("%d", &v);
if (v != 0)
{
int *p;
*p = v; // Oops
}
return v;
}
这个程序的行为是否仅为 v
非零的情况未定义,或者即使v
为零也未定义?
答案 0 :(得分:15)
我会说只有当用户插入任何不同于0的数字时,行为才是未定义的。毕竟,如果实际上没有运行违规代码部分,则不满足UB的条件(即非初始化指针)不会被创建,也不会被解除引用。)
这一点可以在标准中找到,见3.4.3:
行为,在使用不可移植或错误的程序结构或错误数据时, 本国际标准没有要求
这似乎意味着,如果这样的“错误数据”是正确的,那么行为将被完美地定义 - 这似乎非常适用于我们的情况。
附加示例:整数溢出。任何使用用户提供的数据进行添加而不对其进行大量检查的程序都会受到这种未定义的行为的影响 - 但只有当用户提供此类特定数据时才添加UB。
答案 1 :(得分:12)
因为它有language-lawyer标签,所以我有一个极其挑剔的论点,即无论用户输入如何,程序的行为都是未定义的,但不是出于你可能期望的原因 - 虽然它可以很好地定义(当v==0
)取决于实施情况。
该计划将main
定义为
int main()
{
/* ... */
}
C99 5.1.2.2.1表示主要功能应定义为
int main(void) { /* ... */ }
或
int main(int argc, char *argv[]) { /* ... */ }
或同等的;或者以其他一些实现定义的方式。
int main()
不等同于int main(void)
。前者作为声明说,main
采用固定但未指明的数量和类型的论点;后者表示不需要任何论据。不同之处在于对main
的递归调用,例如
main(42);
如果您使用int main(void)
,则违反约束,但如果您使用int main()
则不会违反。
例如,这两个程序:
int main() {
if (0) main(42); /* not a constraint violation */
}
int main(void) {
if (0) main(42); /* constraint violation, requires a diagnostic */
}
不等同。
如果实施文档它接受int main()
作为扩展名,那么这不适用用于该实现。
这是极的挑剔点(不是每个人都同意的),并且通过声明int main(void)
(你应该做的事情,你应该做的事情,很容易避免);所有的功能都应该有原型,而不是旧的式样声明/定义)。
在实践中,我见过的每个编译器都会接受int main()
而没有投诉。
回答有意的问题:
一旦做出更改,如果v==0
,则程序的行为已明确定义,如果v!=0
未定义,则该行为未定义。是的,程序行为的定义取决于用户输入。这没什么特别不寻常的。
答案 2 :(得分:9)
让我说一下为什么我认为这仍然是未定义的。
首先,响应者说这是“大部分定义”或某些,基于他们对某些编译器的经验,是错误的。您的示例的一个小修改将用于说明:
#include <stdio.h>
int
main()
{
int v;
scanf("%d", &v);
if (v != 0)
{
printf("Hello\n");
int *p;
*p = v; // Oops
}
return v;
}
如果您提供“1”作为输入,该程序会执行什么操作?如果你的答案是“它打印你好,然后崩溃”,你错了。 “未定义的行为”并不意味着某些特定语句的行为未定义;这意味着整个程序的行为未定义。允许编译器假定您不参与未定义的行为,因此在这种情况下,它可能假设v
非零且根本不发出任何括号内的代码,包括{{1 }}
如果您认为这不太可能,请再想一想。 GCC可能不会完全执行此分析,但它确实执行非常类似的分析。我最喜欢的例子实际上说明了真实的要点:
printf
尝试编写一个小测试程序,打印出int test(int x) { return x+1 > x; }
,INT_MAX
和INT_MAX+1
。 (请务必启用优化。)典型的实现可能会将test(INT_MAX)
显示为2147483647,INT_MAX
为-2147483648,INT_MAX+1
为1。
实际上,GCC编译此函数以返回常量1.为什么?因为整数溢出是未定义的行为,因此编译器可能假设您没有这样做,因此x不能等于test(INT_MAX)
,因此INT_MAX
大于x+1
,因此此函数可以无条件返回1
未定义的行为可以而且确实会导致与自身不相等的变量,比较大于正数的负数(参见上面的示例)以及其他奇怪的行为。编译器越聪明,行为越奇怪。
好的,我承认我不能引用标准的章节和诗句来回答你提出的确切问题。但是那些说“是的,但在现实生活中取消引用NULL只会产生一个seg错误”的人比他们想象的更加错误,而且每一代编译器都会出错。
在现实生活中,如果代码已经死了,你应该删除它;如果它没有死,你一定不能调用未定义的行为。这就是我对你问题的回答。
答案 3 :(得分:2)
如果v为0,则永远不会执行随机指针赋值,并且函数将返回零,因此它不是未定义的行为
答案 4 :(得分:1)
当声明变量(特别是显式指针)时,会分配一块内存(通常是一个int)。这种内存的和平被标记为free
系统,但存储在那里的旧值没有被清除(这取决于编译器实现的内存分配,它可能用零填充该位置)所以你的{{ 1}}将有一个随机值(垃圾),它必须解释为int *p
。结果是integer
指向的内存中的位置(p的指针)。当你尝试p
(也就是访问这段内存)时,它(几乎每次都)被另一个进程/程序占用,所以试图改变/修改其他一些内存会导致{{1问题由dereference
。
所以在这个例子中,任何其他值然后0将导致未定义的行为,因为此时没有人知道access violation
将指向什么。
我希望这个解释有所帮助。
编辑:啊,对不起,我前面几乎没有答案:)
答案 5 :(得分:1)
很简单。 如果一段代码没有执行,它就没有行为!!!,无论是否定义。
如果输入为0,则if
内的代码不会运行,因此它取决于程序的其余部分来确定是否定义了行为(在这种情况下已定义)。
如果输入不为0,则执行我们都知道的未定义行为的代码。
答案 6 :(得分:0)
我会说这会使整个程序不明确。
未定义行为的关键是未定义。编译器在看到该语句时可以执行任何操作。现在,每个编译器都会按预期处理它,但是他们仍然有权利做任何他们想做的事情 - 包括改变与它无关的部分。
例如,如果编译器检测到未定义的行为,则可以选择向程序添加“此程序可能有危险”的消息。无论v
是否为0,这都会改变输出。
答案 7 :(得分:-1)
你的程序非常明确。如果v == 0则返回零。如果v!= 0则它会溅到内存中的某个随机点上。
p是一个指针,它的初始值可以是任何值,因为你没有初始化它。在运行程序之前,实际值取决于操作系统(在将其提供给进程之前为零内存,有些则没有),编译器,硬件以及内存中的内容。
指针赋值只是写入随机存储器位置。它可能会成功,它可能会破坏其他数据,也可能会出现段错误 - 这取决于所有上述因素。
就C而言,非定义变量没有已知值,并且您的程序(尽管可能编译)不正确。