代码之家  ›  专栏  ›  技术社区  ›  Good Person

在C语言中,在未定义行为之前,代码是否有任何保证?

  •  11
  • Good Person  · 技术社区  · 14 年前

    在下面的代码中是否保证打印“0\n”?

    #include <stdio.h>
    int main(void)
    {
        int c = 0;
        printf("%d\n",c);
    
        printf("%d,%d\n",++c,++c);
    }
    

    更一般地说,如果一个程序有未定义的行为,那么整个程序是未定义的还是只从开始有问题代码的序列点开始?

    询问编译器如何处理第二个printf。我在问,第一次打印是否一定会发生。

    我知道未定义的行为可能会炸毁你的计算机,破坏你的程序,或者其他什么。

    4 回复  |  直到 14 年前
        1
  •  8
  •   Logan Capaldo    14 年前

    好吧,甚至忽略“任何事情都有可能发生!程序可以及时返回,并在第一时间阻止自己运行!”,编译器完全有可能检测到某些形式的未定义行为,并且在这种情况下不进行编译,在这种情况下,首先就不会运行它。所以,是的,未定义的行为在原则上是有传染性的,即使在实践中大多数时候不一定如此。

        2
  •  5
  •   Michael Burr    14 年前

    程序在导致未定义行为之前所做的一切当然已经完成了。

    printf() 会将“0\n”发送到 stdout 溪流。该数据是否真正进入设备取决于该流是无缓冲的、缓冲的还是行缓冲的。

    然后,我再次假设,在完成的、定义良好的操作之后执行的未定义行为可能会造成损害,以至于定义良好的行为似乎没有正确完成。我想有点像那种“如果一棵树掉在树林里……”的东西。


    以下是C99标准关于在序列点之间多次修改对象值的说明:

    在上一个序列点和下一个序列点之间,对象应具有其存储值 通过表达式的计算最多修改一次。

    标准中还提到了对对象的访问:

     <execution-time action> to read or modify the value of an object
     NOTE 1   Where only one of these two actions is meant, ``read'' or ``modify'' is used.
     NOTE 2   "Modify'' includes the case where the new value being stored is the same as the previous value.
     NOTE 3   Expressions that are not evaluated do not access objects.
    

    我不认为在序列点之间多次修改对象在转换时是“未定义的行为”,因为对象在转换时不被访问/修改。

    即使如此,我也同意编译器在编译时诊断这种未定义的行为是件好事,但我也认为,如果把这个问题仅应用于已成功编译的程序,它会更有趣。所以让我们稍微改变一下问题,给出一个编译器在翻译时无法诊断未定义行为的情况:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char* argv[])
    {
        int c[] = { 0, 1, 2, 3 };
        int *p1 = &c[0];
        int *p2 = &c[1];
    
        if (argc > 1) {
            p1 = &c[atoi(argv[1])];
        }
        if (argc > 2) {
            p2 = &c[atoi(argv[2])];
        }
    
        printf("before: %d, %d\n", *p1, *p2);
    
        printf("after:  %d, %d\n", ++(*p1),++(*p2)); /* possible undefined behavior */
    
        return 0;
    }
    

    所以让我们对这个程序提出同样的问题:对于第一个 打印()

    如果输入提供有效的索引值,则未定义的行为只能发生 之后 第一次 . 假设输入是 argv[1] == "1" argv[2] == "1" :编译器实现没有在第一个 打印() 因为未定义的行为会在程序中的某个时刻发生,所以允许跳过第一个 打印() 直接转到它未定义的格式化硬盘的行为(或者可能发生的任何其他恐怖事件)。

        3
  •  3
  •   Merlyn Morgan-Graham    14 年前

    未定义的行为取决于编译器供应商/随机机会。这意味着它可能会抛出一个异常,破坏你程序中的数据,重写你的mp3收藏,召唤天使,或者点燃你的祖母。一旦有了未定义的行为,整个程序就变得未定义。

    一些编译器和一些编译器配置会提供一些模式,这些模式会给您带来麻烦,但是一旦您启用优化,大多数程序的性能都会很差。

    如果一个程序有未定义的行为,那么整个程序是未定义的还是仅从开始问题代码的序列点开始?

    任何东西 可能发生。是否有什么 碰巧被 Murphy's Law :)

    值得思考的是:缓冲区溢出攻击,由各种类型的恶意软件实现,严重依赖于未定义的行为。

        4
  •  0
  •   Jens Gustedt    14 年前

    对于未定义的行为,您可能应该区分在编译时可检测的行为(如您的情况)和依赖于数据且仅在运行时发生的行为,如意外地向 const 限定对象。

    必须 一直运行到UB发生,因为它通常无法预先检测到它(对于非平凡的程序来说,模型检查是一项艰巨的任务),对于您的情况,它可能被允许生成任何类型的程序,例如向编译器供应商发送一些钱;-)

    一个更合理的选择是什么都不产生,也就是抛出一个错误而根本不编译。有些编译器在被告知时会这样做,例如 gcc 你拿着这个 -Wall -std=c99 --pedantic -Werror