调用未定义行为(在此示例中为零)的代码将永远不会执行,程序是否仍是未定义行为?
int main(void)
{
int i;
if(0)
{
i = 1/0;
}
return 0;
}
我认为这仍然是不确定的行为,但是我在标准中找不到任何证据来支持或否认我。
那么,有什么想法吗?
调用未定义行为(在此示例中为零)的代码将永远不会执行,程序是否仍是未定义行为?
int main(void)
{
int i;
if(0)
{
i = 1/0;
}
return 0;
}
我认为这仍然是不确定的行为,但是我在标准中找不到任何证据来支持或否认我。
那么,有什么想法吗?
Answers:
让我们看看C标准如何定义术语“行为”和“未定义行为”。
参考的是ISO C 2011标准的N1570草案;我不知道三个已发布的ISO C标准(1990、1999和2011)中的任何相关差异。
第3.4节:
行为
外观或动作
好的,这有点含糊,但是我认为给定的语句没有“外观”,当然也没有“动作”,除非它是实际执行的。
第3.4.3节:
使用本国际标准不要求的非便携式或错误程序构造或错误数据时的不确定行为
它说这样的构造“在使用时”。该标准未定义“使用”一词,因此我们回到常见的英语含义。如果从未执行过构造,则不会“使用”。
在该定义下有一个注释:
注意可能的不确定行为包括从完全忽略情况以无法预测的结果到在翻译或程序执行过程中以环境特有的文件化方式表现的行为(有或没有发出诊断消息)到终止翻译或执行(有发出诊断消息)。
因此,如果未定义行为,则允许编译器在编译时拒绝程序。但是我对此的解释是,只有证明程序的每次执行都会遇到未定义的行为,它才能这样做。我认为这意味着:
if (rand() % 2 == 0) {
i = i / 0;
}
当然可以具有不确定的行为,不能在编译时被拒绝。
实际上,程序必须能够执行运行时测试,以防止调用未定义的行为,并且标准必须允许它们这样做。
您的示例是:
if (0) {
i = 1/0;
}
永远不会执行除以0的操作。一个非常常见的习惯用法是:
int x, y;
/* set values for x and y */
if (y != 0) {
x = x / y;
}
如果y == 0
,该部门当然具有未定义的行为,但如果,则永远不会执行y == 0
。行为定义得很好,并且出于与示例定义得很好相同的原因:因为潜在的未定义行为永远不会发生。
(除非INT_MIN < -INT_MAX && x == INT_MIN && y == -1
(是的,整数除法可能会溢出),但这是一个单独的问题。)
在注释中(因为删除),有人指出编译器可以在编译时评估常量表达式。正确,但在这种情况下不相关,因为在
i = 1/0;
1/0
不是常量表达式。
甲常数表达式是一个句法类别减少到条件表达式(其不包括分配和逗号表达式)。产生常量表达式仅在实际需要常量表达式(例如大小写标签)的上下文中出现在语法中。因此,如果您写:
switch (...) {
case 1/0:
...
}
然后1/0
是一个常量表达式-并且违反了6.6p4中的约束:“每个常量表达式的求值结果必须是其类型的可表示值范围内的常量。”,因此需要进行诊断。但是赋值的右边不需要常量表达式,只需要条件表达式,因此对常量表达式的约束不适用。编译器可以评估在编译时能够执行的任何表达式,但前提是该行为与在执行过程中进行了评估(或在上下文中if (0)
,在execute()期间未进行评估)相同。
(看上去完全像常量表达式的东西不一定是常量表达式,就像在中x + y * z
,由于序列的上下文不同,该序列x + y
也不是加性表达式。)
这意味着我要引用的N1570第6.6节中的脚注:
因此,在下面的初始化中,
static int i = 2 || 1 / 0;
该表达式是值为1的有效整数常量表达式。
实际上与这个问题无关。
最后,定义了一些导致未定义行为的东西,这些东西与执行期间发生的事情无关。C标准的附件J,第2节(再次参见N1570草案)列出了导致未定义行为的内容,这些内容是从该标准的其余部分收集来的。一些示例(我不认为这是详尽的列表)是:
- 非空源文件不会以换行符结尾,而该换行符不能紧跟反斜杠字符,也不能以部分预处理标记或注释结尾
- 令牌串联产生与通用字符名称的语法匹配的字符序列
- 源文件中会遇到基本源字符集中没有的字符,但标识符,字符常量,字符串文字,标头名称,注释或永远不会转换为令牌的预处理令牌除外
- 标识符,注释,字符串文字,字符常量或标题名称包含无效的多字节字符,或者不以初始移位状态开头和结尾
- 相同的标识符在同一翻译单元中具有内部和外部链接
这些特殊情况是编译器可以检测到的。我认为他们的行为是不确定的,因为委员会不想或不可能将相同的行为强加给所有实现,并且定义一系列允许的行为是不值得的。它们并不是真正属于“永远不会执行的代码”的类别,但是出于完整性的考虑,我在这里提到它们。
1/0
要成为常量表达式吗?
1/0
不是常量表达式,因为它没有被解析为常量表达式,而只是被解析为赋值表达式的一部分的条件表达式。case 1/0:
将违反约束条件并需要诊断。
该文章讨论了第2.6节这样一个问题:
int main(void){
guard();
5 / 0;
}
作者认为该程序是在guard()
不终止时定义的。他们还发现自己区分“静态未定义”和“动态未定义”的概念,例如:
标准11的意图似乎是,通常情况下,如果不容易为情境生成代码,就使情境成为静态未定义的。仅当可以生成代码时,情况才能动态取消定义。
11)与委员会成员的私人通信。
我建议看整篇文章。综上所述,它描绘了一幅一致的图画。
文章的作者必须与委员会成员讨论该问题的事实证实,该标准目前对您的问题的答案尚不明确。
guard()
未定义),只有且仅当该语句5 / 0;
实际执行时,行为才是未定义的。(请注意,编译器可以用对之5 / 0
的调用abort()
或类似的方法合法地代替对的求值;然后且仅当执行到达该点时,程序才会中止。)只有当编译器确定可以永远终止时,程序才可以拒绝该程序guard()
。
guard()
不终止的复杂编译器根本不必为5/0生成任何代码。相反,没有办法为生成代码(int)(void)5
,也不能仅为生成代码(int)(void)z
,因为这也不正确。所以作者认为…
if (0) (int)(void)5;
因为它给幼稚的编译器带来了难题,而无法访问的动态UBif (0) 5 / 0;
则是无害的。这是从他们与委员会成员的讨论中得到的结果,我在其他地方也看到过类似的论点(但也许来自同一来源,特别是因为我不记得它在哪里)。目前,我正在研究C99的基本原理,如果有任何提及,我会回来指出。
(int)(void)5
是违反约束的。N1570 6.5.4,描述了强制转换运算符:“约束:除非类型名称指定了void类型,否则类型名称应指定原子,合格或不合格的标量类型,并且操作数应具有标量类型。”。(void)5
没有标量类型,因此(int)(void)5
违反了该约束,无论包含它的代码是否曾经执行过。
在这种情况下,未定义的行为是执行代码的结果。因此,如果未执行代码,则不会有未定义的行为。
如果未执行的行为仅是代码声明的结果,则未执行的代码可能会调用未定义的行为(例如,如果变量影子的某些情况未定义)。
#include "//e"
哪个调用UB。
对于行为不确定的问题,通常很难将形式方面与实际方面分开。这是1989年标准中未定义行为的定义(我手头没有较新的版本,但我不希望这有很大的变化):
1个未定义的行为 使用不可移植或错误的程序构造或 错误数据,本国际标准对此不施加任何要求 2注意可能的不确定行为范围为完全忽略情况 在翻译或程序执行过程中表现出不可预测的结果 以书面形式记录环境特征(有或没有 发出诊断消息),以终止翻译或 执行(发出诊断消息)。
从正式的角度来看,我想说您的程序确实会调用未定义的行为,这意味着该标准对运行时将执行的操作没有任何要求,仅因为它包含零除。
另一方面,从实际的角度来看,我会惊讶地发现编译器的运行方式不符合您的直觉期望。
该标准说,正如我所记得的对,从一开始就违反规则就可以做任何事情。也许有一些具有全球风味的特殊事件(但我从未听说过或读过类似的东西)...所以我会说:不,这不可能是UB,因为只要行为已明确定义,始终为0 false,因此规则在运行时不会被破坏。