在C语言中,while(1);是未定义行为吗?

36

1
我猜如果在C语言中for(;;)语句被定义良好,那么while(1)也不应该在C语言中是未定义的。请记住,无限循环的检测是一个不可判定的问题。 - Grijesh Chauhan
1
如果您愿意的话,我可以进一步解释6.8.5条款中的第六条,特别是为什么我所在的编译器公司很有可能不会使用这个条款。 - Bryan Olivier
@BryanOlivier 加油 :) - Tony The Lion
1
@Tony,谢谢,骑着爱好的马总是很愉快的。 - Bryan Olivier
可能是在C中,(空)无限循环的未定义行为吗?的重复问题。 - jinawee
4个回答

36

这是一种明确定义的行为。在C11中,新增了一个6.8.5条款6。

迭代语句的控制表达式不是常量表达式156),其在执行过程中不进行输入/输出操作,不访问易变对象,并且在其语句体、控制表达式或(在for语句的情况下)表达式3中不执行同步或原子操作,实现可以假定该循环会终止157)


157)这旨在允许编译器进行转换,例如即使无法证明终止,也可以删除空循环。

由于您循环的控制表达式是常量,编译器可能不会假定循环终止。这适用于应该永远运行的反应程序,例如操作系统。

但是对于以下循环,其行为是不清楚的。

a = 1; while(a);

实际上,编译器可能会或可能不会删除此循环,从而导致程序可能终止或可能不终止。这并不是真正的未定义,因为它不允许擦除您的硬盘,但这是一种避免的构造。

然而还有一个问题,考虑以下代码:

a = 1; while(a) while(1);

现在,由于编译器可能假设外部循环终止,内部循环也应该终止,否则外部循环怎么可能终止呢?因此,如果您有一个非常聪明的编译器,那么一个不应该终止的 while(1); 循环必须在所有方法中都包含这样的无限循环,一直到 main 方法。如果您真的想要无限循环,最好在其中读取或写入一些volatile变量。

为什么这个限制不实用

我们的编译器公司很少会利用这个限制,主要是因为它是一个非常句法化的属性。在中间表示(IR)中,通过常量传播,以上示例中常量和变量之间的差异很容易丢失。

该条款的目的是允许编译器编写者应用以下理想的转换。考虑一个不太常见的循环:

int f(unsigned int n, int *a)
{       unsigned int i;
        int s;
        
        s = 0;
        for (i = 10U; i <= n; i++)
        {
                s += a[i];
        }
        return s;
}

出于架构的原因(例如硬件循环),我们希望将此代码转换为:

int f(unsigned int n, int *a)
{       unsigned int i;
        int s;
        
        s = 0;
        for (i = 0; i < n-9; i++)
        {
                s += a[i+10];
        }
        return s;
}

如果没有6.8.5和6条款,这是不可能的,因为如果n等于UINT_MAX,则循环可能永远无法终止。但对于人类来说,很明显,这不是代码编写者的意图。现在,6.8.5和6条款允许进行此转换。然而,这样做的方式对于编译器编写者来说并不实用,因为在IR上维护无限循环的语法要求很难。

请注意,ni必须是unsigned,因为在signed int上溢出会导致未定义行为,因此可以基于这个原因来证明该转换是合理的。然而,使用unsigned除了拥有更大的正数范围之外,还有助于编写高效的代码。

另一种方法

我们的方法是,代码编写者必须通过插入像assert(n < UINT_MAX)这样的东西或类似Frama-C的保证来表达他的意图。这样,编译器可以“证明”终止,并且不必依赖于6.8.5和6条款。

P.S:我正在查看2011年4月12日的草案,因为Paxdiablo明显正在查看不同版本,也许他的版本更新了。在他的引用中,未提及常量表达式的要素。


@undefinedbehaviour 我刚刚下载了N1570,但它仍然包含我的引用版本的从句,在那里针对“其控制表达式不是常量表达式”的情况做出了例外。但正如我在上面所辩论的那样,这并没有真正有所帮助。 - Bryan Olivier
啊,我没有注意到那个补充。好的,你正在查看的是最新的C11标准草案。 - autistic
1
@R.. 这是一些晦涩的代码,我需要深入研究。但我相信这可以在前端解决,而且差异不会迁移到IR中。 - Bryan Olivier
C++版本的线程强烈表明,“可以假定终止”意味着如果它实际上不会终止,那么代码就具有未定义的行为。 - M.M
为了防止问题无解而导致未定义行为,可以添加一些副作用。对于程序员来说,这样做可能并不是太困难,但是会严重影响编译器在生成代码时允许执行的优化类型,以便实际查找解决方案。不幸的是,鉴于标准文本的内容,我不知道有任何严格符合要求的方法来编写循环,除非添加这种愚蠢的虚拟副作用。 - supercat
显示剩余7条评论

5
在查阅C99标准草案后,我认为“不是”未定义的行为。在草案中,我找不到任何要求迭代结束的语言。
迭代语句的语义描述如下:
“迭代语句导致称为循环体的语句重复执行,直到控制表达式与0相等。”
如果适用的话,我期望出现类似于C++11规定的限制。还有一个名为“约束”的章节,也没有提到任何这样的约束。
当然,实际标准可能会有所不同,尽管我觉得这种情况很少见。

前进保证是在C11(N1570)中添加的。 - M.M

1
最简单的答案涉及到§5.1.2.3p6中的一句话,它规定了符合实现的最小要求:
引用如下:

符合实现的最少要求是:

— 对于易失性对象的访问必须严格按照抽象机器的规则进行评估。

— 在程序终止时,写入文件的所有数据必须与根据抽象语义执行程序产生的结果完全相同。

— 交互设备的输入和输出动态行为应按照7.21.3中指定的方式进行。这些要求的目的是确保未缓冲或行缓冲输出尽可能快地出现,以确保提示消息实际上在程序等待输入之前出现。

这是程序的可观察行为。

如果机器码由于执行的优化而无法产生可观察的行为,则编译器不是C编译器。一个只包含这样一个无限循环的程序在终止点的可观察行为是什么?这样的循环唯一能够结束的方式是通过信号使其过早地结束。在SIGTERM的情况下,程序终止。这将不会产生任何可观察的行为。因此,该程序的唯一有效优化是编译器抢先系统关闭程序并生成立即结束的程序。
/* unoptimised version */
int main() {
    for (;;);
    puts("The loop has ended");
}

/* optimised version */
int main() { }

一种可能是发出信号并调用longjmp使执行跳转到不同的位置。似乎唯一可以跳转到的地方是在循环之前执行期间到达的某个地方,因此只要编译器足够聪明以注意到引发了信号导致执行跳转到其他地方,它就有可能优化循环(和引发信号)以立即进行跳转。
当多个线程进入方程式时,一个有效的实现可能能够将程序所有权从主线程转移到另一个线程,并结束主线程。无论是否进行优化,程序的可观察行为仍必须可观察。

你的名字几乎像是这个问题的一个新奇账户。 - Tony The Lion

1
以下语句出现在《C11 6.8.5迭代语句/6》中:
一个迭代语句的控制表达式不是常量表达式,它不执行输入/输出操作,在其主体,控制表达式或(在for语句的情况下)expression-3中不访问易失对象,并且不执行同步或原子操作的迭代语句可以被实现假定会终止。
由于while(1)使用了常量表达式,因此实现不允许假定它将终止。
如果表达式是非常量的并且所有其他条件都满足,则编译器可以完全删除这种循环,即使无法确定循环是否会终止。

假设它将终止并不完全是免费的。需要进一步处理,以确保程序的可观察行为得到满足。如果没有任何方式可以到达循环后面的代码,则编译器也需要对其进行优化。 - autistic
@undefinedbehaviour 我不同意。我认为循环后的任何可观察行为,可能因为循环中的变量而看似无法到达,根据这个条款,可能会变得可达,并且不需要被优化掉(首先)。 - Bryan Olivier
@BryanOlivier 正如我所写的,“需要进一步处理以确保程序的可观察行为得到满足。” 你能告诉我为什么这个程序没有打印任何东西吗? - autistic
@R.I.P.Seb,标准规定它可能会终止,而不是一定会终止——实际是否终止取决于实现。无论如何,你的具体代码for(;;); printf ...之所以没有打印任何内容,几乎肯定是由于C11 6.8.5.3/2: 省略的表达式-2将被替换为非零常量。因此,它与for(;1;)完全相同,由于常量表达式的限制,无法被优化消除。 - paxdiablo
1
然而,感谢您让我重新关注这个问题。在某个时候,ISO添加了常量表达式子句,这使得我的答案完全错误。已经修复了它。 - paxdiablo
显示剩余3条评论

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