未定义、未指定和实现定义的行为

新手上路,请多包涵

什么是 C 和 C++ 中的 未定义行为(UB)? 未指定的行为实现定义的 行为呢?它们之间有什么区别?

原文由 Zolomon 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.2k
2 个回答

未定义的行为 是 C 和 C++ 语言的那些方面之一,可能会让来自其他语言的程序员感到惊讶(其他语言试图更好地隐藏它)。基本上,即使许多 C++ 编译器不会报告程序中的任何错误,也可以编写行为无法预测的 C++ 程序!

我们来看一个经典的例子:

 #include <iostream>

int main()
{
    char* p = "hello!\n";   // yes I know, deprecated conversion
    p[0] = 'y';
    p[5] = 'w';
    std::cout << p;
}

变量 p 指向字符串文字 "hello!\n" ,下面的两个赋值尝试修改该字符串文字。这个程序有什么作用?根据 C++ 标准的第 2.14.5 节第 11 段,它调用 _未定义的行为_:

尝试修改字符串文字的效果是未定义的。

我可以听到人们尖叫“但是等等,我可以编译这个没问题并得到输出 yellow ”或“你是什么意思未定义,字符串文字存储在只读内存中,所以第一次赋值尝试导致核心转储”。这正是未定义行为的问题。基本上,一旦您调用未定义的行为(甚至是鼻恶魔),该标准就允许任何事情发生。如果根据您的语言心理模型存在“正确”行为,那么该模型就是错误的; C++ 标准拥有唯一的投票权,句号。

未定义行为的其他示例包括访问超出其边界的数组、 取消引用空指针在其生命周期结束后访问对象 或编写 据称聪明的表达式,如 i++ + ++i

C++ 标准的 1.9 节还提到了未定义行为的两个不太危险的兄弟, 未指定行为实现定义的行为

本国际标准中的语义描述定义了一个参数化的非确定性抽象机。

抽象机的某些方面和操作在本国际标准中描述为 实现定义(例如, sizeof(int) )。这些构成了抽象机的参数。每个实现都应包括描述其在这些方面的特征和行为的文档。

抽象机的某些其他方面和操作在本国际标准中描述为 未指定(例如,函数参数的评估顺序)。在可能的情况下,本国际标准定义了一组允许的行为。这些定义了抽象机器的不确定性方面。

本国际标准中将某些其他操作描述为 未定义(例如,取消引用空指针的效果)。 [ _注意_: 本国际标准对包含未定义行为的程序的行为没有要求。 —— 尾注]

具体来说,第 1.3.24 节规定:

允许的未定义行为范围从 完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(发出的诊断消息)。

您可以做些什么来避免遇到未定义的行为?基本上,你必须阅读那些知道他们在说什么的作者写的 好的 C++ 书籍。避免使用互联网教程。避免公牛。

原文由 fredoverflow 发布,翻译遵循 CC BY-SA 4.0 许可协议

从历史上看,实现定义的行为和未定义的行为都代表了这样一种情况,标准的作者期望编写高质量实现的人会使用判断来决定哪些行为保证(如果有的话)对在预期应用程序领域中运行的程序有用。预期目标。高端数字运算代码的需求与低级系统代码的需求大不相同,UB 和 IDB 都为编译器编写者提供了满足这些不同需求的灵活性。这两个类别都没有要求实现的行为方式对任何特定目的有用,甚至对任何目的都有用。然而,声称适用于特定目的的质量实现, _无论标准是否要求_,都应该以符合该目的的方式行事。

实现定义的行为和未定义的行为之间的唯一区别在于,前者要求实现定义和记录一致的行为 _,即使在实现可能没有任何用处的情况下也是如此_。它们之间的分界线不是定义行为的实现通常是否有用(无论标准是否要求,编译器编写者都应该在实际情况下定义有用的行为),而是 _是否可能存在定义行为同时代价高昂的实现而且没用_。对此类实现可能存在的判断并不以任何方式、形式或形式暗示对支持在其他平台上定义的行为的有用性的任何判断。

不幸的是,自 1990 年代中期以来,编译器编写者已经开始将缺乏行为要求解释为一种判断,即即使在至关重要的应用领域,甚至在几乎没有成本的系统上,行为保证也不值得付出代价。编译器作者没有将 UB 视为进行合理判断的邀请,而是开始将其视为 这样做的借口。

例如,给定以下代码:

 int scaled_velocity(int v, unsigned char pow)
{
  if (v > 250)
    v = 250;
  if (v < -250)
    v = -250;
  return v << pow;
}

二进制补码实现不必花费任何努力来将表达式 v << pow 视为二进制补码移位,而不考虑 v 是正数还是负数。

然而,当今一些编译器编写者的首选理念是,因为 v 只有在程序将要进行未定义行为时才能为负数,因此没有理由让程序剪切负数范围 v 。尽管过去每个有意义的编译器都支持负值的左移,并且大量现有代码都依赖于这种行为,但现代哲学会将标准说左移负值是 UB 解释为暗示编译器编写者应该随意忽略这一点。

原文由 supercat 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题