未定义的行为

什么是未定义行为(UB)?

未定义的行为是 C 标准中使用的术语。C11 标准(ISO / IEC 9899:2011)将术语未定义行为定义为

行为,在使用不可移植或错误的程序结构或错误数据时,本国际标准不对此要求

如果我的代码中有 UB 会怎么样?

根据标准,这些结果可能由于未定义的行为而发生:

注意可能的未定义行为包括完全忽略具有不可预测结果的情况,在转换或程序执行期间以环境特征(有或没有发出诊断消息)的文档方式执行,终止转换或执行(使用发出诊断信息)。

以下引用通常用于描述(不太正式)从未定义的行为发生的结果:

“当编译器遇到[给定的未定义构造]时,让恶魔飞出你的鼻子是合法的”(这意味着编译器可以选择任何奇怪的方式来解释代码而不违反 ANSI C 标准)

为什么 UB 存在?

如果它是如此糟糕,为什么他们不只是定义它或使其实现定义?

未定义的行为允许更多优化机会; 编译器可以合理地假设任何代码都不包含未定义的行为,这可以允许它避免运行时检查并执行其有效性成本高昂或无法证明的优化。

为什么 UB 难以追踪?

至少有两个原因导致未定义的行为导致难以检测的错误:

  • 编译器不需要 - 通常不能可靠 - 警告你未定义的行为。实际上要求它这样做会直接反对存在未定义行为的原因。
  • 不可预测的结果可能无法在操作的确切位置开始展开,其中行为未定义的构造发生; 未定义的行为会影响整个执行,其影响可能随时发生:在未定义的构造期间,之后或甚至之前

考虑空指针解除引用:编译器不需要诊断空指针解除引用,甚至不能,因为在运行时传递给函数或全局变量的任何指针都可能为 null。*当空指针解引用发生时,标准不要求程序需要崩溃。*相反,程序可能会更早,更晚或者根本不崩溃; 它甚至可以表现为空指针指向有效对象,并且表现完全正常,仅在其他情况下崩溃。

在空指针解除引用的情况下,C 语言与 Java 或 C#等托管语言不同,其中定义了空指针解除引用的行为 :在确切的时间抛出异常(Java 中的 NullPointerException,C#中的 NullReferenceException)因此来自 Java 或 C#的那些人可能错误地认为在这种情况下,无论是否发出诊断消息,C 程序都必须崩溃

附加信息

有几种这样的情况应该明确区分:

  • 显式未定义的行为,即 C 标准明确告诉你不受限制的行为。
  • 隐式未定义的行为,标准中根本没有文本预见到你带来程序的情况的行为。

还要记住,在许多地方,C 标准故意未定义某些构造的行为,以便为编译器和库实现者提供自己定义的空间。一个很好的例子是信号和信号处理程序,其中 C 的扩展(例如 POSIX 操作系统标准)定义了更详细的规则。在这种情况下,你只需检查平台的文档; C 标准无法告诉你任何事情。

还要注意,如果在程序中发生未定义的行为,并不意味着仅发生未定义行为的点是有问题的,而是整个程序变得毫无意义。

由于存在这些问题,因此在 C 语言中编程人员至少熟悉触发未定义行为的事物时,这一点很重要(特别是因为编译器并不总是警告我们关于 UB)。

应该注意的是,有一些工具(例如静态分析工具,如 PC-Lint)有助于检测未定义的行为,但同样,它们无法检测到所有出现的未定义行为。