所以你以为你了解 C?(2016)

一场流行的在线 C 测验依赖未定义和由实现定义的行为,再次引发了“了解 C”到底意味着什么的争论。评论者拆解了整数大小、结构体填充、字符编码、移位和多次自增等边角情况,争论在未指定平台、编译器和标准版本时,答案何时真正不可知。许多人认为这类谜题过于吹毛求疵或具有误导性,不如真实世界中的可移植性、在目标系统上测试以及避免难读代码更重要;也有人为其辩护,认为它们有助于提醒人们低层假设是多么脆弱。

关于测验的总体反应

  • 许多人认为这道测验更像是关于 C 的边角情况和未定义行为的“陷阱题”,而不是实用技能测试。
  • 有些人把它当作对 C 细微之处的提醒而乐在其中;另一些人则觉得它过于吹毛求疵、令人恼火,或在意识到“元游戏”的答案其实是“正确答案是:你不可能知道”之后,觉得这是“浪费时间”。
  • 一个反复出现的抱怨是:答案选项“我不知道”含糊不清;人们希望有更明确的选项,比如“未定义”“未指明”或“由实现定义”。

未定义 / 由实现定义 / 未指明的行为

  • 多条评论剖析了具体题目到底涉及未定义行为、由实现定义行为,还是未指明行为。
  • 对第一题存在直接分歧:有人称其为未定义,有人称其为由实现定义,有人说只是未指明;还有人指出,文章本身的解释假定了 sizeof(int) == 4,而这并不保证成立。
  • 结构体布局、整数宽度、char 的有符号性、字符编码(ASCII 与 EBCDIC),以及大于等于位宽的移位,都被拿来作为标准刻意保留非可移植行为的例子。

具体技术讨论

  • 整数提升:小于 int 的类型会在运算前提升为 int,从而导致令人意外的 UB 位置(例如 16 位与 32 位乘法的例子)。
  • 移位:在 C 和 C++ 中,移位位数大于等于类型位宽属于 UB;不同 CPU 的处理方式不同(掩码、置零、触发异常)。
  • 多次自增表达式(i++ + ++i、链式 XOR 交换)被强调为与序列点有关的经典未定义行为。
  • 无限循环:C 和 C++ 在编译器何时可以优化掉没有可观察副作用的循环方面有所不同;这对嵌入式代码很重要。
  • 基于 union 的类型惩罚(type punning)在 C 中有效,但在 C++ 中并不保证成立。

实用性 vs 纯粹性

  • 一派认为,在真实项目中你会针对已知平台开发,依赖事实上的规范(例如 32 位 int、ASCII、IEEE-754),并通过测试来验证;对每个边角情况都严格遵循标准未免过头。
  • 另一派强调,依赖没有保证的行为可能会在新编译器、编译选项、架构或嵌入式目标上出问题;这类测验有助于暴露危险的假设。

语言、工具与替代方案

  • 讨论延伸到了 C++ 的怪异之处、D 的类型转换语法和整数大小、Rust 更安全的移位方法、通过 Gambit-C 使用 Scheme,以及在 C 中使用 GC。
  • 还有几个人认为有更好的教育资源:最佳实践、内存管理、更安全的惯用法,以及书籍(包括同一网站上链接的 PDF)。