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

Meh*_*dad 34 c undefined-behavior language-lawyer

我不确定我是否完全理解未定义行为可能危及程序的程度.

假设我有这段代码:

#include <stdio.h>

int main()
{
    int v = 0;
    scanf("%d", &v);
    if (v != 0)
    {
        int *p;
        *p = v;  // Oops
    }
    return v;
}
Run Code Online (Sandbox Code Playgroud)

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

Mat*_*lia 15

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

在3.4.3标准中可以找到这方面的提示:

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

这似乎意味着,如果这样的"错误数据"是正确的,那么行为将被完美地定义 - 这似乎非常适用于我们的情况.


附加示例:整数溢出.任何使用用户提供的数据进行添加而不对其进行大量检查的程序都会受到这种未定义的行为的影响 - 但只有当用户提供此类特定数据时才添加UB.


Kei*_*son 12

由于这有一个标签,我有一个非常挑剔的论点,即程序的行为是不确定的,无论用户输入,但不是出于你可能期望的原因 - 尽管它可以很好地定义(何时v==0)取决于实现.

该计划定义main

int main()
{
    /* ... */
}
Run Code Online (Sandbox Code Playgroud)

C99 5.1.2.2.1表示主要功能应定义为

int main(void) { /* ... */ }
Run Code Online (Sandbox Code Playgroud)

或者作为

int main(int argc, char *argv[]) { /* ... */ }
Run Code Online (Sandbox Code Playgroud)

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

int main()不等于int main(void).前者作为一种宣言,表示main采用固定但未指明的数量和类型的论点; 后者表示不需要任何论据.区别在于递归调用main

main(42);
Run Code Online (Sandbox Code Playgroud)

如果您使用int main(void),则违反约束,但如果您使用则不会int main().

例如,这两个程序:

int main() {
    if (0) main(42); /* not a constraint violation */
}
Run Code Online (Sandbox Code Playgroud)


int main(void) {
    if (0) main(42); /* constraint violation, requires a diagnostic */
}
Run Code Online (Sandbox Code Playgroud)

不等同.

如果它接受的实现文档int main()作为扩展,那么这不适用于该实现.

这是一个极其挑剔的观点(不是每个人都同意这一点),并且通过声明int main(void)(无论如何你应该做什么)很容易避免;所有函数都应该有原型,而不是旧式声明/定义.

在实践中,我见过的每个编译器都会int main()毫无怨言地接受.

要回答有意的问题:

一旦进行了这种改变,如果是v==0,并且未定义,则程序的行为是明确定义的v!=0.是的,程序行为的定义取决于用户输入.这没什么特别不寻常的.

  • @Mehrdad:`int main(...)`是一个语法错误; 可变函数必须至少有一个命名参数.更一般地,可变参数(如`printf`)可以合法地用可变数字和参数类型调用.必须使用定义指定的参数的确切数量和类型来调用具有旧式非原型声明的函数 - 但编译器不会诊断具有错误参数的调用.这就是原型被添加到语言中的原因. (3认同)

Nem*_*emo 9

让我举一个论据,为什么我认为这仍然是未定义的.

首先,响应者说这是"大部分定义"或某些,基于他们对某些编译器的经验,是错误的.您的示例的一个小修改将用于说明:

#include <stdio.h>

int
main()
{
    int v;
    scanf("%d", &v);
    if (v != 0)
    {
        printf("Hello\n");
        int *p;
        *p = v;  // Oops
    }
    return v;
}
Run Code Online (Sandbox Code Playgroud)

如果您提供"1"作为输入,该程序会执行什么操作?如果你的答案是"它打印你好,然后崩溃",你错了."未定义的行为"并不意味着某些特定语句的行为未定义; 这意味着整个程序的行为是不确定的.允许编译器假设您不参与未定义的行为,因此在这种情况下,它可能假设v非零并且根本不发出任何括号内的代码,包括printf.

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

int test(int x) { return x+1 > x; }
Run Code Online (Sandbox Code Playgroud)

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

实际上,GCC编译此函数以返回常量1.为什么?因为整数溢出是未定义的行为,因此编译器可能假设你没有这样做,因此x不能相等INT_MAX,因此x+1大于x,因此这个函数可以无条件地返回1.

未定义的行为可以而且确实会导致与自身不相等的变量,比较大于正数的负数(参见上面的示例)以及其他奇怪的行为.编译器越聪明,行为越离奇.

好的,我承认我不能引用标准的章节和诗句来回答你问的确切问题.但是那些说"是的,但在现实生活中取消引用NULL只会产生一个seg错误"的人比他们想象的更加错误,并且他们在每一代编译器中都会出错.

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

  • @Nemo:这是一个延伸.几乎每个用C编写的程序(例如,每个使用指针的程序)都有无法访问的代码路径来调用UB.但只要这些路径从未被采用,程序的行为就是明确定义的.在这个问题中唯一令人困惑的问题是路径是否取决于输入.这使得程序*不安全*,但UB仍然以接收"坏"输入为条件. (4认同)
  • 我不明白你的例子是如何支持你想要的.您的示例无条件执行.我的代码有条件地执行.这两者有什么关系? (2认同)
  • @Nemo:一旦程序收到输入组合,编译器就可以生成代码来执行它想要的任何操作,这些输入将不可避免地导致未定义的行为.就C标准而言,代码甚至可以构建一个时间机器并且在收到这样的输入之前擦除每个人对程序记忆的记忆.尽管如此,如果存在任何将产生定义行为的输入值组合,那么如果实际提供了那些输入值,则编译器生成的代码必须产生明确定义的行为. (2认同)