所以你以为你了解 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)。