在C语言中,代码是否有保证可以避免未定义行为?

11
在以下代码中,“0\n”是否被保证会被打印出来?
#include <stdio.h>
int main(void)
{
    int c = 0;
    printf("%d\n",c);

    printf("%d,%d\n",++c,++c);
}
更广义地说,如果程序存在未定义的行为,整个程序是否变得未定义,还是只有从引发问题的代码序列点开始的部分变得未定义?
请注意:我并不是在询问编译器对第二个printf执行的操作。我想知道的是第一个printf是否保证会发生。
我知道未定义的行为能够使你的计算机崩溃、程序崩溃或其他什么情况。

5
@tom c在序列点之间被修改了两次。 - Good Person
也许你需要阅读c.faq: http://c-faq.com/expr/evalorder2.html 和 http://c-faq.com/ansi/undef.html - qrtt1
C++11标准明确表示“甚至是在第一个未定义操作之前的操作也不保证”,但我在查找C11标准中的“第一个未定义操作”时没有得到结果。 - Medinoc
4个回答

8

即使忽略像“任何事情都可能发生!程序可以回到过去,并阻止自己首次运行!”这样的事情,编译器完全可以检测到某些形式的未定义行为,在这种情况下不进行编译,那么您将无法使其首次运行。因此,原则上未定义行为是具有传染性的,尽管在大多数情况下实践中并非如此。


你能否提供一些标准支持声明的链接? - Pacerier

5
在程序引起未定义行为之前所做的任何事情,当然已经完成了。
因此,printf() 已将 "0\n" 发送到 stdout 流。数据是否实际到达设备取决于该流是无缓冲、有缓冲还是行缓冲。
另一方面,我想未定义行为在完成的明确定义操作之后执行可能会造成损坏,以至于似乎明确定义的行为没有正确完成。我猜就像那些“如果树倒在树林里……”的事情一样。
更新以解决未来未定义行为意味着所有赌注都失效甚至在程序开始执行之前的信念……
以下是 C99 标准对在序列点之间多次修改对象值的情况的说明:

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

标准还对访问对象进行了如下说明:

access

 <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()并直接进入其格式化硬盘(或任何其他可能发生的恐怖事件)的未定义行为。
考虑到编译器同意翻译程序,未来的未定义行为承诺并不给编译器在实际发生未定义行为之前就可以做任何想做的事情的自由。当然,正如我之前提到的,未定义行为造成的损害可能会破坏先前的结果 - 但这些结果必须已经发生过。

不,未定义行为的范围比那要广泛得多。请参见被接受的答案。 - R.. GitHub STOP HELPING ICE
@R.:我不认为被接受的答案是正确的——未来未定义行为的承诺并不意味着在实际发生该未定义行为之前一切都无法预测。 - Michael Burr
你提出了一个很好的论点。另一方面,原始问题是“在以下代码中是否保证打印“0\n”?”,考虑到流可以被缓冲和未定义行为可能包括程序终止而不刷新缓冲区,我不知道是否保证打印“0\n”。我承认这有些吹毛求疵(他是在问它是否被打印还是printf是否被调用?)。 - Logan Capaldo
@Logan - 我是在使用一个具体的例子来问一个更普遍的问题。我想最好用“printf是否会被调用并执行如预期”的措辞。 - Good Person
1
大多数回答给出的问题是它们关注特定的示例,而实际上问的问题是“如果程序具有未定义的行为,整个程序是否变为未定义,还是仅从开始问题代码的序列点开始?”编译器可以自由地处理您的代码-重新排序、丢弃部分代码、添加额外的代码。此唯一受限于定义行为。如果您的程序未定义,则编译器仍将尝试进行优化,因此您会得到非常奇怪的行为,既在之前,也在该代码行之后。 - Merlyn Morgan-Graham
如果程序在执行过程中需要输入,并且如果程序的行为在未收到某些输入时是明确定义的,则必须产生规范要求在接收到第一个无效输入之前必须产生的任何输出。然而,如果编译器确定某些特定的输入最终会导致未定义的行为,它可以合法地为该输入添加特殊情况测试,并在输入后立即开始发射核导弹。我怀疑任何编译器都不会这样做,但规范允许这样做。 - supercat

3

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

某些编译器和某些编译器配置将提供抛出骨头的模式,但是一旦启用了优化,大多数程序的表现都很差。

如果程序具有未定义行为,整个程序是否变得未定义还是只从开始有问题的代码序列点开始?

运行到未定义点的代码可能会做正确的事情。 但是这只有那么多的好处。一旦出现未定义行为,实际上可以发生任何事情。是否会发生某件事由墨菲定律决定 :)

优化依赖于良好定义的行为,并采用各种技巧以获得速度。这意味着只要副作用对于良好定义的程序是无法区分的,那么您的代码可以完全无序执行。仅因为未定义的行为似乎从源代码中的某一点开始,并不保证任何先前的代码行会免疫。启用优化后,您的代码很容易更早地触发未定义的行为。
值得思考的是:各种恶意软件实现的缓冲区溢出利用非常依赖未定义的行为。

我知道这个。我的问题更具体——即在出现问题的那一行之前的代码是否已经定义? - Good Person
1
这只会给我特定编译器、架构和时间的答案。Logan Capaldo提出了一个很好的观点,即编译器可能会拒绝编译程序和/或返回到过去,这似乎回答了我的问题。感谢您的时间和努力 :-) - Good Person

0

对于未定义行为,应该区分在编译时可以检测到的事情(就像您的情况一样)和仅在运行时发生的数据相关的事情,例如意外写入一个const限定的对象。

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

更合理的选择是什么都不做,即抛出错误并根本不编译。某些编译器在被告知时会这样做,例如使用gcc,您可以使用-Wall -std=c99 --pedantic -Werror


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接