【问题标题】:Why is undefined behaviour allowed in C为什么在 C 中允许未定义的行为
【发布时间】:2016-04-10 00:20:17
【问题描述】:

我最近一直在努力学习 C。来自 Java,令我惊讶的是您可以执行声明为“未定义”的某些操作。

这对我来说似乎非常不安全。我知道程序员有责任不执行未定义的操作,但为什么甚至允许它开始呢?例如,为什么编译器不能捕获超出范围的数组索引,甚至是悬空指针?你最终只是访问了你永远不应该访问的内存块,没有(明显的)充分的理由。

作为比较,Java 使 更加确定你不会做 任何事情愚蠢,像热蛋糕一样抛出异常。

允许这样做肯定是有原因的吗?这是什么?

回答:据我了解,主要原因是性能。此外,Java 确实有未定义的行为,尽管没有这样标记。

编辑:对 C 的限制性问题

【问题讨论】:

  • 检查每个数组访问的边界会降低性能。
  • 这个问题的答案是:因为这是C++。
  • 编译器无法检测到所有未定义的行为 (it's the equivalent to the halting problem)。编译器会在可能的情况下尝试检测它并警告您,这会有所帮助,但 C 和 C++ 可以让您在需要时自取其辱。
  • 从技术上讲,Java 也有一些(在 C 和 C++ 中)被描述为未定义、未指定等行为的部分。主要区别在于它们没有被记录下来。这包括 (a) 与线程有关的任何事情,(b) 垃圾收集器的行为,(c) 对象初始化、生命周期和终结的某些方面,(d) 用户界面库(Swing、AWT)的一些行为, (e) 一大堆与性能、延迟有关的事情……不胜枚举。
  • @Peter 请继续,因为你正在创造我的一天 :-) 但是,从你已经说过的开始:GC 的不确定性使得 Java 不适合需要可预测实时响应时间的关键任务软件(例如控制航天器或心肺机)。 (例如)必须以 100 次/秒的精确时间间隔完成某事。但是,这可能会延迟 [有时会延迟几秒钟],因为 GC“刚刚决定”在错误的时刻启动。

标签: c undefined-behavior


【解决方案1】:

不允许未定义的行为,它只是没有被编译器捕获。

这里的权衡是在速度和安全性之间。可以通过增加几个 CPU 周期来防止许多类型的未定义行为。

例如,当您从已分配但未初始化的内存中读取时,您可以通过让已编译的代码向其中写入零来防止发生 UB。但是,这会花费您对内存的额外写入,这是完全没有必要的。

同样,可以通过检查[] 运算符内的边界来防止读取/写入数组末尾。但是,这会在每次访问阵列时花费您一些额外的 CPU 周期。

C++ 设计者认为,拥有速度并允许潜在的 UB 比强迫每个人为他们不需要的东西付费要好。然而,这种方法与 Java 的“一次编写,随处运行”的要求不兼容,因此 Java 语言的设计者坚持在几乎所有情况下都完全定义行为。

【讨论】:

  • 在所有情况下完全定义的行为不是有点强吗?
  • @PaulBoddington 除了 JNI,Java 标准是一个“控制狂”:要么编译器产生错误,要么你得到标准中描述的行为。至少这是标准作家所希望的。
  • 是的,因为它,我真的很喜欢 java,但是有一些未定义的行为 - 例如。您无法预测 WeakHashMap 的大小何时会发生变化。
  • 你说的是不是真的。考虑区分在编译和运行时执行的代码。现代编译器比你想象的要聪明得多。
  • @FISOCPP 您能否指出我的回答中导致您在评论中得出结论的语言,例如现代编译器不够聪明?我的回答是关于编译器不愿意来阻止UB,而不是他们不能这样做。
【解决方案2】:

最初,大多数形式的未定义行为表示某些实现可能会捕获的东西,但其他实现可能不会。因为标准的作者无法预测平台在遇到陷阱时可能会做的所有事情(包括,从字面上看,系统会发出警报并锁定直到操作员手动清除故障的可能性) ,陷阱的后果不在 C 标准的管辖范围之内,因此,从标准的角度来看,某些平台可能会导致陷阱的几乎所有操作都被视为“未定义行为”。

这不应被视为暗示该标准的作者不相信实现应该在实际情况下尝试对此类事情采取明智的行为。例如,C89 标准的作者指出,那个时代的大多数当前系统都会定义以下行为:

/* Assume USmall is half the size of "int" */
unsigned mult(USmall x, USmall y) { return x*y; }

在所有情况下,包括 x 和 y 的数学乘积在 INT_MAX+1 和 UINT_MAX 之间的情况,都等同于 (unsigned)x*y;。我认为没有理由相信他们不会预料到这种趋势会持续下去。

不幸的是,一种新的哲学已经流行起来,它基于修正主义观点,即编译器编写者只在标准未强制要求的情况下支持有用的行为,因为它们太简单而无法做任何其他事情。例如,在 gcc 中,使用优化级别 2 但没有其他非默认选项,上述“mult”例程有时会在产品介于 0x80000000u 和 0xFFFFFFFFu 之间的情况下生成虚假代码,即使在此类计算会运行的平台上运行时也是如此。历史上曾奏效。据说这是以“优化”的名义进行的;知道这些技术最终执行的“优化”中有多少实际上是有用的,并且无法通过更安全的方式实现,这将是一件有趣的事情。

从历史上看,未定义行为是 C 编译器公开底层平台行为的许可;在底层平台的行为符合程序员需求的情况下,这允许程序员的需求以机器代码更有效地表达,而不是一切都必须以标准定义的方式完成。然而,最近,它被解释为编译器实现行为的许可,这些行为不仅与底层平台中的任何事物或任何合理的程序员期望无关,而且甚至不受时间和因果律的约束。

【讨论】:

    【解决方案3】:

    Java 有一个运行时环境来照顾您。这就是为什么越界时会抛出异常的原因——这是在编译时无法计算出来的。

    在对向量使用 at() 方法时,在 C++ 中存在运行时边界检查。这就是 at() 与 [] 运算符的区别

    【讨论】:

      猜你喜欢
      • 2020-02-15
      • 2021-11-25
      • 1970-01-01
      • 2013-09-16
      • 1970-01-01
      • 2011-09-04
      • 2018-08-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多