未定义的行为如何未定义?

我不确定我是否理解未定义的行为会危害程序的程度。

比方说,我有这个代码:

#include <stdio.h> int main() { int v = 0; scanf("%d", &v); if (v != 0) { int *p; *p = v; // Oops } return v; } 

这个程序的行为是不是针对v为非零的情况定义,或者即使v是零也不定义?

我只是说,只有当用户插入任何不同于0的数字时,行为才是未定义的。毕竟,如果有问题的代码段没有被实际运行,UB的条件就不会被满足(即,未初始化的指针不被创build既没有解除引用)。

有一点可以在标准3.4.3find:

行为,在使用不可移植或错误的程序结构或错误的数据时,本国际标准对此没有要求

这似乎意味着,如果这种“错误的数据”是正确的,那么这种行为就会被完全定义 – 这似乎很适用于我们的情况。


另外的例子:整数溢出。 任何对用户提供的数据进行补充而没有对其进行广泛检查的程序都会受到这种未定义的行为的影响,但是只有当用户提供这些特定的数据时才会有UB的附加。

既然有了这个语言律师的标签,我有一个非常挑剔的说法,即程序的行为是不定的,不pipe用户input,但不是由于你可能期望的原因 – 虽然它可以很好的定义(当v==0 )取决于实施。

该程序定义main

 int main() { /* ... */ } 

C99 5.1.2.2.1说主要function应定义为

 int main(void) { /* ... */ } 

或如

 int main(int argc, char *argv[]) { /* ... */ } 

或同等学历; 或者以某种其他实现定义的方式。

int main()不等于int main(void) 。 前者作为一个声明,说main是固定的,但没有说明的数量和types的论点; 后者表示不需要任何争论。 不同之处在于recursion调用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 ,那么它是不确定的。 是的,程序行为的定义取决于用户的input。 这没什么特别的。

让我来辩论一下为什么我觉得这个问题还没有定义。

首先,响应者说这是“大部分定义的”,或者根据他们对一些编译器的经验,有些是错误的。 您的示例的一个小修改将用来说明:

 #include <stdio.h> int main() { int v; scanf("%d", &v); if (v != 0) { printf("Hello\n"); int *p; *p = v; // Oops } return v; } 

如果你提供“1”作为input,这个程序是做什么的? 如果你回答是“它打印你好,然后崩溃”,你错了。 “未定义的行为”并不意味着某些特定语句的行为是未定义的; 这意味着整个程序的行为是不确定的。 编译器可以假定你没有参与未定义的行为,所以在这种情况下,它可能会假设v是非零的,并且根本不会发出任何括号内的代码,包括printf

如果你认为这是不可能的,再想一想。 GCC可能不会完全执行此分析,但确实执行了非常类似的分析。 我最喜欢的例子,实际上说明了真实点:

 int test(int x) { return x+1 > x; } 

尝试编写一个小testing程序来打印出INT_MAXINT_MAX+1test(INT_MAX) 。 (确保启用优化。)一个典型的实现可能会显示INT_MAX为2147483647, INT_MAX+1为-2147483648,并且test(INT_MAX)为1。

事实上,GCC编译这个函数返回一个常量1.为什么? 由于整数溢出是未定义的行为,因此编译器可能会认为你没有这样做,所以x不能等于INT_MAX ,因此x+1大于x ,因此该函数可以无条件返回1。

未定义的行为可以并且确实导致不等于自身的variables,比正数大的负数(参见上面的例子)和其他奇怪的行为。 编译器越聪明,行为越离奇。

好吧,我承认我不能引用标准的章节和诗句来回答你问的确切问题。 但是那些说“是啊,但是在现实生活中,解引用NULL只是给出seg故障”的人比他们想象中的更加错误,而且每一代编译器都会犯下更多的错误。

而在现实生活中,如果代码死了,你应该删除它; 如果它没有死,你不能调用未定义的行为。 所以这是我的回答你的问题。

如果v是0,你的随机指针分配永远不会被执行,并且函数将返回零,所以它不是未定义的行为

当你声明variables(特别是显式指针)时,分配一块内存(通常是一个int)。 内存的这种安宁被标记为对系统是free ,但是那里存储的旧值不会被清除(这取决于编译器实现的内存分配,它可能用零填充)所以你的int *p将会有一个随机值(垃圾),它必须解释为integer 。 结果是p指向(p的指针)的地方。 当你试图dereference (也就是访问这段内存)时,它会被其他进程/程序占用(几乎每一次),所以试图修改/修改其他一些内存将导致memory manager access violation问题。

所以在这个例子中,任何其他的值都是0会导致不确定的行为,因为没有人知道*p会指向什么。

我希望这个解释是有帮助的。

编辑:啊,对不起,我再次提供了几个答案:)

很简单。 如果一段代码没有执行,它不具有行为!!!,无论是否定义

如果input是0,那么if里面的代码不运行,所以它依赖于程序的其余部分来确定行为是否被定义(在这个例子中是定义的)。

如果input不是0,则执行代码,我们都知道这是一个未定义行为的情况。

我会说这使整个程序不确定。

未定义的行为的关键是它是未定义的 。 编译器可以在看到该语句时做任何事情。 现在,每个编译器都会按预期处理它,但是他们仍然有权做任何他们想做的事情 – 包括更改与其无关的部分。

例如,如果编译器检测到未定义的行为,则可以select向程序添加“此程序可能是危险的”消息。 无论v是否为0,这都会改变输出。

你的程序非常好定义。 如果v == 0,那么它返回零。 如果v!= 0,那么它会飞溅到内存中的某个随机点上。

p是一个指针,它的初始值可能是任何东西,因为你不初始化它。 实际的值取决于操作系统(在给你的进程前一些零内存,有些则不),你的编译器,你的硬件以及运行程序之前的内存。

指针分配只是写入随机存储器位置。 它可能会成功,它可能会破坏其他数据或者可能会出现段错误 – 这取决于所有上述因素。

就C而言,已经很好地定义了未经初始化的variables没有已知的值,并且你的程序(虽然它可能会被编译)是不正确的。