未指定,未定义和实现定义的行为WIKI for C

虽然SO上有很多关于这个主题的链接,但我认为有些东西缺失:用简单的语言明确解释未指定行为 (UsB), 未定义行为 (UB)和实现定义行为 (IDB)之间的区别)详细但容易解释任何用例和示例。

注意:为了在WIKI中实现紧凑性,我将UsB缩写为up,但是不要指望在其他地方使用它。

我知道这可能看起来与其他post(它更接近的是这个 ) 的重复 ,但在任何人将此标记为重复之前 ,请考虑我已经找到的所有材料的问题(我将要制作这个post中的社区WIKI):

  • 太多分散的例子。 当然,例子并不坏,但有时人们无法找到一个很好地适应他手头问题的例子,因此它们可能会令人困惑(特别是对于新手)。

  • 示例通常只是代码,但解释很少。 在这些微妙的问题上,特别是(相对)新手,更自上而下的方法可能更好:首先是一个清晰,简单的解释与抽象(但不是法律)的描述, 然后是一些简单的例子,解释为什么他们触发一些行为

  • 有些post经常混合使用C和C ++示例。 C和C ++有时与他们认为的UsB,UB和IDB不一致,所以一个例子可能会误导那些不熟悉这两种语言的人。

  • 当给出UsB,UB和IDB的定义时,通常它是标准的简单引用,有时可能不清楚或难以为新手消化。

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

由于我自己不是这个主题的超级专家,我将建立一个社区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很新,所以我希望通过这种方式开始问答我不会违反任何规则。 对不起,如果是这样的话。 我们欢迎各位模特让我知道。

C标准以可归纳如下的方式定义UsB,UB和IDB:

未指定的行为( UsB

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

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

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

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

更具体地说,考虑一个int变量:一个实现可以自由选择任何int值,并且这个选择可以是完全随机的,非确定性的,或者是由实现的想法所左右, 这不需要记录任何关于它 。 只要实施保持在标准规定的限制范围内,这是可以的,用户不能抱怨。

未定义的行为( UB

由于命名表明这是C标准没有强制或保证程序应该或应该做什么的情况。 所有赌注都已关闭。 这样的情况:

  • 使程序错误不可移植

  • 从实现中不需要任何东西

这是一个非常讨厌的情况:只要有一段代码具有未定义的行为, 整个程序就被认为是错误的,并且标准允许执行所有操作

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

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

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

我希望最后两件( 真的)项目可以让你对UB的肮脏感有正确的感觉。 即使大多数实现都不会插入必要的代码来格式化硬盘,但真正的编译器会进行优化!

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

未定义行为的有用性

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

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

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

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

实现定义的行为( IDB

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

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

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

未指定行为的示例

评估顺序

函数参数

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

例如,在c(a(), b()); 是否在b之前或之后调用函数a未指定。 唯一的保证是在c函数之前调用它们。

未定义行为的示例

指针

取消引用空指针

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

从技术上讲,这是未定义的行为。 但是,由于这是一个非常常见的错误来源,因此大多数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类

把标题放在这里!

把你的解释放在这里!

实现定义的行为的示例

第1类

把标题放在这里!

把你的解释放在这里!

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

N1256是早期的草案,其中包含C99标准以及三项技术勘误的更改。

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

  • J.1未指定的行为
  • J.2未定义的行为
  • J.3实现定义的行为
  • J.4特定于语言环境的行为
  • J.5常用扩展