问题 在C中是否有任何保证代码在未定义的行为之前?


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

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

    printf("%d,%d\n",++c,++c);
}

更一般地说,如果一个程序有未定义的行为,整个程序是不确定的,还是仅从开始出现问题代码的序列点开始?

请注意:我是  询问编译器对第二个printf的作用。我问的是第一个printf是否有保证会发生。

我知道未定义的行为能够炸毁你的计算机,崩溃你的程序或其他什么。


10248
2017-10-23 02:35


起源

这里有什么不确定的? - Tom
@tom c在序列点之间被修改两次, - Good Person
也许,你需要阅读c.faq: c-faq.com/expr/evalorder2.html 和 c-faq.com/ansi/undef.html - qrtt1


答案:


甚至忽略了“任何事情都可能发生!程序可以及时返回并防止自己在第一时间运行!”这样的事情,编译器很可能检测到某些形式的未定义行为而不能在这种情况下编译如果你不能让它在第一时间运行。所以,是的,未定义的行为在原则上具有传染性,但在实践中大部分时间都不一定如此。


8
2017-10-23 02:51



你能链接到标准的一些支持声明吗? - Pacerier


无论程序在导致未定义的行为之前做了什么,当然已经完成了。

所以 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;
}

在这个程序中,甚至在转换时都不知道存在未定义的行为 - 只有当程序的输入指示应该处理相同的数组元素时才会出现(或者如果输入指定了可能发生不同类型的未定义行为,则会发生这种情况)无效的索引值)。

因此,让我们用这个程序提出同样的问题:标准对于第一个可能发生的事情说了些什么 printf() 结果或副作用?

如果输入提供有效的索引值,则只能发生未定义的行为  首先 printf()。假设输入是 argv[1] == "1" 和 argv[2] == "1":编译器实现没有在第一个之前确定的自由 printf() 因为未定义的行为将在程序中的某个点发生,所以允许跳过第一个 printf() 并直接进入格式化硬盘的未定义行为(或任何其他可能发生的恐怖事件)。

鉴于编译器同意转换程序,未来未定义行为的承诺并不能让编译器在实际发生未定义行为之前自由地执行任何操作。当然,正如我之前提到的,未定义行为造成的损害可能会破坏以前的结果 - 但这些结果必定已经发生。


5
2017-10-23 03:03



不,未定义的行为范围远大于此。看到接受的答案。 - R..
@R。:我不认为接受的答案是正确的 - 未来未定义行为的承诺并不意味着在实际发生未定义的行为之前所有的赌注都会被取消。 - Michael Burr
你提出了一个很好的论据。另一方面,最初的问题是“在以下代码中保证”0 \ n“被打印?”并且鉴于流可以被缓冲并且未定义的行为可以包括不刷新缓冲区的程序终止,我不知道保证打印“0 \ n”。我承认这是分裂的头发(他问是否打印或是否打印了printf?)。 - Logan Capaldo
@Logan - 我用一个具体的例子问了一个更普遍的问题。我想最好把它称为“将printf调用并按预期执行”或类似的东西。 - Good Person
给出的大多数答案的问题是他们关注的是具体的例子,其中提出的实际问题是“如果一个程序有未定义的行为,整个程序是不确定的还是仅从开始有问题的代码的序列点?”。编译器可以随意对代码执行任何操作 - 重新排序,抛出部分内容,添加额外代码。对此的唯一限制是定义的行为。如果您的程序未定义,编译器仍将尝试进行优化,因此您将在该行之前和之后获得非常奇怪的行为。 - Merlyn Morgan-Graham


未定义的行为取决于编译器供应商/随机机会。这意味着它可能会抛出异常,程序中的数据损坏,写入你的mp3集合,调用天使,或点燃你的祖母着火。一旦您有未定义的行为,您的整个程序将变得不确定。

一些编译器和一些编译器配置将提供一些方法,但是一旦打开优化,大多数程序的表现都会非常糟糕。

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

运行到未定义点的代码可能已经做了正确的事情。但这只会带来很多好处。一旦你点击未定义的行为,字面上 什么 可以发生。是否有事  发生的事情被覆盖 墨菲定律 :)

优化依赖于明确定义的行为,并在该框之外播放各种技巧以获得速度。这意味着您的代码可以完全不按顺序执行,只要副作用对于定义良好的程序是无法区分的。仅仅因为未定义的行为似乎从源代码中的某个点开始并不能保证以前的任何代码行都不会受到影响。启用优化后,您的代码可以更加轻松地更早地触及未定义的行为。

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


3
2017-10-23 02:46



我知道这件事。我的问题更具体 - 即问题行定义之前的代码是什么? - Good Person
这只会给我我特定的编译器,架构和一天中的时间的答案。 Logan Capaldo提出了一个很好的观点,即编译器可以拒绝编译程序和/或回过头来回复我的问题。感谢您的时间和精力:-) - Good Person


对于未定义的行为,可能应该区分在编译时可检测到的事物(如您的情况)和依赖于数据的事物,并且仅在运行时发生,例如意外写入 const 合格的对象。

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

更合理的选择是不产生任何东西,即抛出错误而根本不编译。一些编译器在被告知时会这样做,例如 gcc 你得到这个 -Wall -std=c99 --pedantic -Werror


0
2017-10-23 08:16