让我们看看 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 不是常量表达式。
constant-expression 是一个简化为 conditional-expression 的句法类别(不包括赋值和逗号表达式)。产生式constant-expression 出现在语法only 中实际需要常量表达式的上下文中,例如case 标签。所以如果你写:
switch (...) {
case 1/0:
...
}
然后1/0 是一个常量表达式——并且违反了 6.6p4 中的约束:“每个常量表达式都应计算为可表示范围内的常量
其类型的值。”,因此需要诊断。但赋值的右侧不需要 constant-expression,只需要 conditional-expression,所以常量表达式的约束不适用。编译器可以评估它在编译时能够评估的任何表达式,但前提是行为与在执行期间评估的行为相同(或者,在if (0) 的上下文中, 不在执行期间评估()。
(看起来完全像 constant-expression 的东西不一定是 constant-expression,就像在 x + y * z 中,序列 x + y 不是additive-expression 因为它出现的上下文。)
这意味着我要引用的 N1570 第 6.6 节中的脚注:
因此,在接下来的初始化中,
static int i = 2 || 1 / 0;
该表达式是一个有效的整数常量表达式,值为 1。
实际上与这个问题无关。
最后,有一些事情被定义为导致未定义的行为,这些行为与执行期间发生的事情无关。附录 J,C 标准的第 2 节(再次参见 N1570 draft)列出了从标准的其余部分收集的导致未定义行为的内容。一些示例(我并不认为这是一个详尽的列表)是:
- 非空源文件不以换行符结尾,该换行符之前不紧跟反斜杠字符或以部分结尾
预处理令牌或评论
- 标记连接产生与通用字符名称语法匹配的字符序列
- 在源文件中遇到不在基本源字符集中的字符,标识符、字符常量、字符串除外
字面量、标题名称、注释或预处理标记
从未转换为令牌
- 标识符、注释、字符串文字、字符常量或标头名称包含无效的多字节字符或没有开始
并以初始移位状态结束
- 同一个标识符在同一个翻译单元中既有内部链接又有外部链接
这些特殊情况是编译器可以检测到的。我认为他们的行为是未定义的,因为委员会不想或不能对所有实现强加相同的行为,并且定义一系列允许的行为是不值得的。它们并不真正属于“永远不会执行的代码”的类别,但为了完整起见,我在这里提及它们。