为什么在 C 中允许未定义的行为
Why is undefined behaviour allowed in C
最近为了学习C一直在胡思乱想。来自 Java,令我惊讶的是您可以执行声明为 "undefined" 的某些操作。
这对我来说似乎非常不安全。我知道程序员有责任不执行未定义的操作,但为什么允许它开始呢?例如,为什么编译器不捕捉数组索引越界,甚至不捕捉悬空指针?你只是最终访问了你永远不应该访问的内存块,没有(明显的)充分理由。
作为比较,Java 使 更加确定 你不会做 任何事情 愚蠢的事情,像热一样抛出异常蛋糕。
肯定有允许这样做的原因吗?这是什么?
回答:据我了解,主要原因是性能。此外,Java 确实有未定义的行为,尽管没有这样标记。
编辑:C 的限制问题
Java有运行时间环境照顾你。这就是为什么越界时会抛出异常的原因——这是编译时无法弄清楚的。
当对向量使用 at() 方法时,C++ 中有 运行 时间界限检查。这就是 at() 与 []operator
的区别
不允许未定义的行为,只是没有被编译器捕获。
这里的权衡是在速度和安全性之间。可以通过增加一些 CPU 周期来防止多种未定义的行为。
例如,当您从已分配但未初始化的内存中读取时,您可以通过让编译代码将零写入其中来防止发生 UB。但是,这会花费您额外写入内存,这是完全没有必要的。
类似地,可以通过检查 []
运算符内的边界来防止 reading/writing 越过数组的末尾。但是,这会在每次数组访问时额外花费一些 CPU 个周期。
C++ 设计者决定拥有速度并允许潜在的 UB 比强迫每个人为他们不需要的东西付费更好。然而,这种方法与 Java 的 "write once, run anywhere" 要求不兼容,因此 Java 语言的设计者几乎在所有情况下都坚持完全定义的行为。
最初,大多数形式的未定义行为表示一些实现可能会捕获的东西,但其他实现可能不会。因为标准的作者无法预测平台在出现陷阱时可能做的所有事情(从字面上看,包括系统发出警报并锁定直到操作员手动清除故障的可能性) , 陷阱的后果不在 C 标准的管辖范围内,因此从标准的角度来看,几乎所有平台可能会导致陷阱的行为都被认为是 "Undefined Behavior".
这不应被理解为暗示标准的作者不认为实现应该在实际情况下尝试对此类事情做出明智的行为。例如,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 之间的情况下生成伪造代码,即使 运行在历史上可以进行此类计算的平台上。据推测,这是以 "optimization" 的名义完成的;知道最终执行的 "optimizations" 此类技术中有多少实际上是有用的,并且无法通过更安全的方式实现,这将是一件很有趣的事情。
从历史上看,未定义行为是 C 编译器公开底层平台行为的许可证;在底层平台的行为符合程序员需求的情况下,这使得程序员的需求可以用机器代码更有效地表达,而不是一切都必须按照标准定义的方式来完成。然而,最近,它被解释为编译器实现行为的许可,这些行为不仅与底层平台中的任何内容无关,也与任何合理的程序员期望无关,甚至不受时间和因果关系的约束。
最近为了学习C一直在胡思乱想。来自 Java,令我惊讶的是您可以执行声明为 "undefined" 的某些操作。
这对我来说似乎非常不安全。我知道程序员有责任不执行未定义的操作,但为什么允许它开始呢?例如,为什么编译器不捕捉数组索引越界,甚至不捕捉悬空指针?你只是最终访问了你永远不应该访问的内存块,没有(明显的)充分理由。
作为比较,Java 使 更加确定 你不会做 任何事情 愚蠢的事情,像热一样抛出异常蛋糕。
肯定有允许这样做的原因吗?这是什么?
回答:据我了解,主要原因是性能。此外,Java 确实有未定义的行为,尽管没有这样标记。
编辑:C 的限制问题
Java有运行时间环境照顾你。这就是为什么越界时会抛出异常的原因——这是编译时无法弄清楚的。
当对向量使用 at() 方法时,C++ 中有 运行 时间界限检查。这就是 at() 与 []operator
的区别不允许未定义的行为,只是没有被编译器捕获。
这里的权衡是在速度和安全性之间。可以通过增加一些 CPU 周期来防止多种未定义的行为。
例如,当您从已分配但未初始化的内存中读取时,您可以通过让编译代码将零写入其中来防止发生 UB。但是,这会花费您额外写入内存,这是完全没有必要的。
类似地,可以通过检查 []
运算符内的边界来防止 reading/writing 越过数组的末尾。但是,这会在每次数组访问时额外花费一些 CPU 个周期。
C++ 设计者决定拥有速度并允许潜在的 UB 比强迫每个人为他们不需要的东西付费更好。然而,这种方法与 Java 的 "write once, run anywhere" 要求不兼容,因此 Java 语言的设计者几乎在所有情况下都坚持完全定义的行为。
最初,大多数形式的未定义行为表示一些实现可能会捕获的东西,但其他实现可能不会。因为标准的作者无法预测平台在出现陷阱时可能做的所有事情(从字面上看,包括系统发出警报并锁定直到操作员手动清除故障的可能性) , 陷阱的后果不在 C 标准的管辖范围内,因此从标准的角度来看,几乎所有平台可能会导致陷阱的行为都被认为是 "Undefined Behavior".
这不应被理解为暗示标准的作者不认为实现应该在实际情况下尝试对此类事情做出明智的行为。例如,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 之间的情况下生成伪造代码,即使 运行在历史上可以进行此类计算的平台上。据推测,这是以 "optimization" 的名义完成的;知道最终执行的 "optimizations" 此类技术中有多少实际上是有用的,并且无法通过更安全的方式实现,这将是一件很有趣的事情。
从历史上看,未定义行为是 C 编译器公开底层平台行为的许可证;在底层平台的行为符合程序员需求的情况下,这使得程序员的需求可以用机器代码更有效地表达,而不是一切都必须按照标准定义的方式来完成。然而,最近,它被解释为编译器实现行为的许可,这些行为不仅与底层平台中的任何内容无关,也与任何合理的程序员期望无关,甚至不受时间和因果关系的约束。