调用未定义行为的代码(在本例中,除以零)将永远不会被执行,程序仍然是未定义行为吗?
int main(void)
{
int i;
if(0)
{
i = 1/0;
}
return 0;
}
我认为这仍然是未定义的行为,但我在标准中找不到任何证据来支持或否认我。
那么,有什么想法吗?
调用未定义行为的代码(在本例中,除以零)将永远不会被执行,程序仍然是未定义行为吗?
int main(void)
{
int i;
if(0)
{
i = 1/0;
}
return 0;
}
我认为这仍然是未定义的行为,但我在标准中找不到任何证据来支持或否认我。
那么,有什么想法吗?
让我们看看 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
的行为 if ,但它永远不会执行 if y == 0
。行为定义明确,与您的示例定义明确的原因相同:因为潜在的未定义行为永远不会真正发生。
(除非INT_MIN < -INT_MAX && x == INT_MIN && y == -1
(是的,整数除法会溢出),但这是一个单独的问题。)
在评论中(已删除),有人指出编译器可能会在编译时评估常量表达式。这是真的,但在这种情况下不相关,因为在上下文中
i = 1/0;
1/0
不是常量表达式。
常量表达式是一种简化为条件表达式(不包括赋值和逗号表达式)的句法类别。产生式常量表达式仅出现在语法中实际需要常量表达式的上下文中,例如案例标签。所以如果你写:
switch (...) {
case 1/0:
...
}
then1/0
是一个常量表达式——它违反了 6.6p4 中的约束:“每个常量表达式都应计算为一个常量,该常量在其类型的可表示值范围内。”,因此需要进行诊断。但是赋值的右侧不需要常量表达式,只需要条件表达式,因此常量表达式的约束不适用。编译器可以评估它在编译时能够评估的任何表达式,但前提是行为与在执行期间评估的行为相同(或者,在 的上下文中if (0)
,在 execution() 期间未评估)。
(看起来完全像常量表达式的东西不一定是常量表达式,就像在 中,由于它出现的上下文,x + y * z
序列x + y
不是加法表达式。)
这意味着我要引用的 N1570 第 6.6 节中的脚注:
因此,在下面的初始化中,
static int i = 2 || 1 / 0;
表达式是一个值为 1 的有效整数常量表达式。
实际上与这个问题无关。
最后,有一些事情被定义为导致未定义的行为,这些行为与执行期间发生的事情无关。附录 J,C 标准的第 2 节(再次参见N1570 草案)列出了从标准的其余部分收集的导致未定义行为的事物。一些例子(我不认为这是一个详尽的列表)是:
- 非空源文件不以换行符结尾,该换行符之前不是反斜杠字符,也不是以部分预处理标记或注释结尾
- 标记连接产生与通用字符名称的语法匹配的字符序列
- 在源文件中遇到不在基本源字符集中的字符,但标识符、字符常量、字符串文字、标题名称、注释或从未转换为标记的预处理标记除外
- 标识符、注释、字符串文字、字符常量或标题名称包含无效的多字节字符或不在初始移位状态开始和结束
- 同一标识符在同一翻译单元中具有内部和外部链接
这些特殊情况是编译器可以检测到的。我认为他们的行为是未定义的,因为委员会不想或不能对所有实现强加相同的行为,并且定义一系列允许的行为是不值得的。它们并不真正属于“永远不会执行的代码”的类别,但为了完整起见,我在这里提到它们。
本文在第 2.6 节中讨论了这个问题:
int main(void){
guard();
5 / 0;
}
作者认为程序定义时guard()
不终止。他们还发现自己区分“静态未定义”和“动态未定义”的概念,例如:
标准11背后的意图似乎是,通常情况下,如果不容易为它们生成代码,它们就会静态地未定义。只有当代码可以生成时,情况才能动态地定义。
11) 与委员会成员的私人通信。
我建议查看整篇文章。总而言之,它描绘了一幅一致的画面。
文章作者必须与委员会成员讨论该问题这一事实证实,该标准目前对您问题的答案是模糊的。
在这种情况下,未定义的行为是执行代码的结果。所以如果代码没有被执行,就没有未定义的行为。
如果未定义的行为仅仅是代码声明的结果(例如,如果某些变量隐藏的情况未定义),则未执行的代码可能会调用未定义的行为。
标准说,我没记错,从那一刻起就可以做任何事情,规则被打破。也许有一些具有全球风味的特殊事件(但我从未听说或读过类似的东西)......所以我会说:不,这不可能是 UB,因为只要行为定义明确,0 总是false,因此规则在运行时不会被破坏。
只有当标准做出重大更改并且您的代码突然不再“永远不会被执行”时。但我看不出有任何合乎逻辑的方式会导致“未定义的行为”。它没有造成任何事情。
在未定义行为的主题上,通常很难将形式方面与实际方面分开。这是 1989 年标准中未定义行为的定义(我手头没有更新的版本,但我预计这不会发生重大变化):
1 未定义的行为 行为,在使用不可移植或错误的程序构造或 错误数据,本国际标准对此没有要求 2 注意可能的未定义行为范围从完全忽略这种情况 具有不可预知的结果,在翻译或程序执行期间表现 以书面方式记录环境特征(有或没有 发出诊断消息),终止翻译或 执行(发出诊断消息)。
从正式的角度来看,我会说您的程序确实调用了未定义的行为,这意味着标准对运行时的操作没有任何要求,只是因为它包含被零除。
另一方面,从实际的角度来看,我会惊讶地发现编译器的行为与您的直觉预期不同。
我认为这仍然是未定义的行为,但我在标准中找不到任何证据来支持或否认我。
我认为该程序不会调用未定义的行为。
缺陷报告 #109解决了一个类似的问题并说:
此外,如果给定程序的每一次可能执行都会导致未定义的行为,那么给定程序就不是严格符合的。一个符合的实现不能仅仅因为该程序的某些可能的执行会导致未定义的行为而不能翻译一个严格符合的程序。因为 foo 可能永远不会被调用,所以给出的示例必须由符合要求的实现成功翻译。
这取决于表达式“未定义的行为”是如何定义的,以及语句的“未定义行为”是否与程序的“未定义行为”相同。
该程序看起来像 C,因此对编译器使用的 C 标准(如某些答案所做的那样)进行更深入的分析是合适的。
在没有指定标准的情况下,正确答案是“视情况而定”。根据编译器的猜测,在某些语言中,编译器在出现第一个错误后会尝试猜测程序员的意思,但仍会生成一些代码。在其他更纯粹的语言中,一旦某些东西未定义,未定义就会传播到整个程序。
其他语言有“有界错误”的概念。对于某些有限类型的错误,这些语言定义了错误可能产生的破坏程度。特别是具有隐含垃圾收集的特定语言,无论错误是否使键入系统无效,经常会产生影响。