11

尽管在 SO 上有很多关于这个主题的链接,但我认为缺少一些东西:用通俗易懂的语言清楚地解释未指定行为(UsB)、未定义行为(UB) 和实现定义行为(IDB ) 之间的区别) 对任何用例和示例进行详细而简单的解释。

注意:我做了UsB的首字母缩写词,但不要期望在其他地方看到它。

我知道这可能看起来与其他帖子重复(更接近的是这个),但在有人将其标记为重复之前,请考虑一下我已经找到的所有材料有什么问题(我将来自这篇文章的社区 WIKI):

  • 太多分散的例子。当然,例子还不错,但有时找不到一个能很好地解决他手头的问题的例子,所以它们可能会令人困惑(尤其是对于新手来说)。

  • 示例通常只是很少解释的代码。在这些微妙的问题上,尤其是对于(相对)新手来说,更自上而下的方法可能会更好:首先是带有抽象(但不是合法的)描述的清晰、简单的解释,然后是 一些简单的例子,解释为什么它们会触发某些行为.

  • 有些帖子经常混合使用 C 和 C++ 示例。C 和 C++ 有时在他们认为的 UsB、UB 和 IDB 上不一致,因此对于不精通这两种语言的人来说,一个示例可能会产生误导。

  • 当给出 UsB、UB 和 IDB 的定义时,通常是对标准的简单引用,对于新手来说有时可能不清楚或太难消化。

  • 有时对标准的引用是部分的。许多帖子仅引用对手头问题有用的部分的标准,这很好,但缺乏通用性。此外,标准的引用通常没有任何解释(对初学者不利)。

由于我自己不是这个主题的超级专家,所以我将创建一个社区 WIKI,以便任何有兴趣的人都可以贡献和改进答案。

为了不破坏我创建结构化的初学者友好 WIKI 的目的,我希望海报在编辑 WIKI 时遵循一些简单的准则:

  • 对您的用例进行分类。如果适用,请尝试将您的示例/代码放在现有类别下,否则创建一个新类别。

  • 首先是简单的文字描述。首先用简单的词语描述(当然不要过度简化——质量第一!)你试图提出的例子或观点。然后放代码示例或引用。

  • 通过引用引用标准。不要发布各种标准的片段,但要给出明确的参考(例如 C99 WG14/N... 第 1.4.7 节,第 ...),并尽可能发布相关资源的链接。

  • 更喜欢免费的在线资源。如果你想引用书籍或非免费资源,那没关系(并且可能会提高 WIKI 的质量),但也可以尝试添加一些免费资源的链接。这对于 ISO 标准尤其重要。欢迎您添加指向官方标准的链接,但也尝试添加指向免费草稿的等效链接。并且请不要将草稿链接替换为对官方标准的引用,添加到它们中。甚至一些大学的一些计算机科学系也没有 ISO 标准的副本,更不用说大多数程序员了!

  • 除非真的有必要,否则不要发布代码。仅当仅使用简单英语的解释会尴尬或不清楚时才发布代码。尝试将代码示例限制为单行。而是发布指向其他 SO Q&A 的链接。

  • 不要发布 C++ 示例。我希望这成为 C 的一种常见问题解答如果有人想为 C++ 启动一个双线程,那就太好了)。欢迎与 C++ 的相关差异,但仅作为旁注。那是在您彻底解释 C 案例之后,您可以添加一些关于 C++ 的语句,如果这对 C 程序员切换到 C++ 时有所帮助,但我不想看到超过 20% 的 C++ 内容的示例。通常像“(C++ 在这种情况下表现不同)”这样的简单注释加上相关链接就足够了。

由于我对 SO 还很陌生,因此我希望通过这种方式开始问答不会违反任何规则。对不起,如果是这种情况。欢迎模组让我知道。

4

2 回答 2

12

C 标准对 UsB、UB 和 IDB 的定义可以概括如下:

未指定行为 ( UsB )

这是一种行为,标准给出了一些实现必须选择的替代方案,但它没有规定如何以及何时做出选择。换句话说,实现必须接受用户代码触发该行为而不会出错,并且必须符合标准给出的替代方案之一。

请注意,实施不需要记录有关所做选择的任何内容。这些选择也可能是不确定的或依赖于(以未记录的方式)编译器选项。

总而言之:该标准提供了一些可供选择的可能性,实施选择何时以及如何选择和应用特定的替代方案。

请注意,该标准可能会提供大量的替代方案。典型的例子是没有显式初始化的局部变量的初始值。该标准规定,只要该值是变量数据类型的有效值,该值就是未指定的。

更具体地说,考虑一个int变量:实现可以自由选择任何int值,并且该选择可以是完全随机的、非确定性的或受实现的一时兴起的摆布,这不需要记录任何关于它的内容。只要实施保持在标准规定的范围内,就可以了,用户不能抱怨。

未定义行为 ( UB )

正如命名所表明的那样,这是一种 C 标准不强制或保证程序会或应该做什么的情况。所有的赌注都取消了。这样的情况:

  • 使程序错误不可移植

  • 完全不需要实施中的任何东西

这是一个非常糟糕的情况:只要有一段代码具有未定义的行为,整个程序就被认为是错误,标准允许实现做所有事情

换句话说,UB 原因的存在允许实现完全忽略标准,只要涉及触发 UB 的程序即可。

请注意,这种情况下的实际行为可能涵盖无限的可能性,以下绝不是详尽的列表:

  • 可能会发出编译时错误。
  • 可能会发出运行时错误。
  • 该问题被完全忽略(这可能导致程序错误)。
  • 编译器默默地丢弃 UB 代码作为优化。
  • 您的硬盘可能已格式化。
  • 你的电脑可能会清空你的银行账户,并要求你的女朋友约会。

我希望最后两个(严肃的)项目能让你对 UB 的肮脏有正确的直觉。即使大多数实现不会插入必要的代码来格式化你的硬盘驱动器,真正的编译器会优化!

术语说明:有时人们争辩说,标准认为在其实现/系统/环境中是 UB 来源的某些代码以文档化的方式工作,因此它不可能是真正的 UB。这种推理是错误的,但它是一个常见的(并且在某种程度上可以理解的)误解:当在 C 上下文中使用术语 UB(以及 UsB 和 IDB)它意味着一个技术术语,其精确含义由标准定义。特别是“未定义”这个词失去了它的日常意义。因此,将错误或不可移植程序产生“明确定义”行为的示例作为反例展示是没有意义的。如果你尝试,你真的错过了重点。UB 意味着您失去了标准的所有保证。如果您的实现提供了扩展,那么您的保证只是您的实现的保证。如果您使用该扩展,您的程序就不再是符合标准的 C 程序(从某种意义上说,它不再是 C 程序,因为它不再遵循标准!)。

未定义行为的有用性

关于 UB 的一个常见问题是这样的:“如果 UB 如此讨厌,为什么标准不要求实现在面对 UB 时发出错误?”

首先,优化。允许实现不检查 UB 的可能原因允许进行许多优化,从而使 C 程序非常高效。这是 C 的特性之一,尽管它使 C 成为初学者许多陷阱的来源。

其次,标准中 UB 的存在允许符合标准的实现提供对 C 的扩展,而不会被视为整体不符合标准。

只要实现的行为符合符合程序的要求,它本身就是符合的,尽管它可能提供可能在特定平台上有用的非标准设施。当然,使用这些工具的程序将是不可移植的,并且将依赖于记录的 UB,即根据标准是 UB 的行为,但实现文档作为扩展。

实现定义的行为 ( IDB )

这是一种可以用类似于 UsB 的方式来描述的行为:标准提供了一些替代方案,实现选择了一个,但实现需要准确记录选择的方式

这意味着必须为阅读其编译器文档的用户提供足够的信息来准确预测特定情况下会发生什么

请注意,没有完整记录 IDB 的实现不能被视为符合要求。符合标准的实现必须准确记录在标准声明 IDB 的任何情况下会发生什么。



未指定行为的示例

评估顺序

函数参数

函数参数的评估顺序未指定EXP30-C

例如,c(a(), b());未指定函数a是在 before 还是 after 调用bc唯一的保证是在函数之前调用两者。



未定义行为的示例

指针

取消引用空指针

空指针用于指示指针未指向有效内存。因此,尝试通过空指针读取或写入内存没有多大意义。

从技术上讲,这是未定义的行为。然而,由于这是一个非常常见的错误来源,大多数 C 环境确保大多数取消引用空指针的尝试都会立即使程序崩溃(通常会因分段错误而终止它)。由于引用数组和/或结构所涉及的指针算法,这种保护并不完美,因此即使使用现代工具,取消引用空指针也可能会格式化您的硬盘驱动器。

取消引用未初始化的指针

就像空指针一样,在明确设置其值之前取消引用指针是 UB。与空指针不同,大多数环境不提供针对此类错误的任何安全网,除了编译器可以警告它。如果你还是编译你的代码,你很可能会体验到 UB 的全部糟糕之处。

取消引用无效指针

无效指针是包含不在任何已分配内存区域内的地址的指针。创建无效指针的常用方法是调用free()(在调用之后,指针将无效,这几乎就是调用的重点free()),或者使用指针算法来获取超出已分配内存块限制的地址。

这是指针解引用 UB 最邪恶的变体:没有安全网,没有编译器警告,只是代码可以做任何事情。通常情况下,它确实如此:大多数恶意软件攻击在程序中使用这种 UB 行为,以使程序按照它们希望的方式运行(如安装木马、键盘记录器、加密硬盘驱动器等)。有了这种 UB,格式化硬盘的可能性就变得非常真实!

抛弃不变

如果我们将一个对象声明为const我们向编译器承诺我们永远不会更改该对象的值。在许多情况下,编译器会发现这种无效的修改并向我们大喊大叫。但是,如果我们像这个片段一样抛弃 constness:

int const a = 42;
...
int* ap0 = &a;      //< error, compiler will tell us
int* ap1 = (int*)&a; //< silences the compiler
...
*ap1 = 43;          //< UB ==> program crash?

编译器可能无法跟踪这种无效访问,将代码编译为可执行文件,并且只有在运行时才会检测到无效访问并导致程序崩溃。

第 2 类

在这里放个标题!

把你的解释放在这里!



实现定义的行为示例

第一类

在这里放个标题!

把你的解释放在这里!

于 2013-08-24T16:38:33.797 回答
1

N1570是 ISO C 标准的草案,非常接近 ISO 官方文件。

N1256是较早的草案,包含 C99 标准以及三个技术勘误的更改。

附录 J 有 5 个部分,每个部分收集分散在标准其余部分的信息:

  • J.1 未指明的行为
  • J.2 未定义的行为
  • J.3 实现定义的行为
  • J.4 特定于地区的行为
  • J.5 通用扩展
于 2013-08-31T21:14:20.103 回答