C 程序里的一切都是未定义行为
文章摘要
作者 Thomas Habets 写 C/C++ 30 年,但这篇博文的核心论点是悲观的——”没人能写出正确的 C 或 C++。“未定义行为(UB)不是边角案例,而是渗透在语言规范本身。任何非平凡的 C/C++ 程序几乎一定隐含 UB——只是你没发现而已。
他用一连串看似无害的代码片段证明这一点:
- 指针对齐:把
void*转为int*,如果地址没对齐就是 UB——即使在 x86 这种宽容架构上能跑,未来架构可能不会容忍。重点是 UB 不是”硬件层面会不会崩”,而是”C 语言规范层面你的程序已经无效了“,编译器有权按任意方式翻译它。 - 类型转换的隐藏陷阱:调用
isxdigit()时把char隐式转int可能产生负值——而isxdigit内部往一个 256 长数组里查表,于是发生越界访问。 - 浮点转整数:C 标准明文写”如果整数部分无法用目标类型表示,行为未定义”——很多人以为只是截断。
- 空指针假设:地址为 0 的对象在 C 里无法被安全实现——这把一些嵌入式架构挡在了门外。
- 变参函数:
printf里 format 和参数类型不匹配是 UB。 - 整数提升:subtle 到让 overflow 变得几乎不可预测。
作者的态度更极端——这种语言在 2026 年根本不适合再写。他甚至建议公司让 LLM 系统性扫描 UB,并把企业里继续写 C/C++ 类比为可能违反 Sarbanes-Oxley 法案(公司治理合规)。结论的金句很硬——”没人能写正确的 C 或 C++。”——连 OpenBSD 这种成熟项目也藏着未被检测的 UB;安全漏洞往往从最无害的操作里冒出来;初级程序员现实里不可能掌握所有 subtleties。
文章不直接推销替代语言,但暗示了——如果你真的想写正确的代码,去找一个 UB 模型更可控的语言。
HN 评论精华
-
muvlon(最被点赞的”加码”):作者举的例子还不够离谱,他来举一个——
volatile int x = 5; printf("%d in hex is 0x%x.\n", x, x);看起来人畜无害——但因为volatile让每次读都算 side effect(5.1.2.4.1),而函数参数求值顺序是 indeterminately sequenced 的(6.5.3.3.8),同一标量的 unsequenced side effects 是 UB(6.5.1.2)——结论:”我们在 C 里可以在一个线程内、没有任何写入的情况下,构造一个 data race。” -
beeforpork(深化对齐这个例子):”未对齐指针本身就是 UB——不是访问它才 UB。”即使你只是把
void*隐式转int*然后从不解引用,只要那个地址未对齐,你的 C 程序就已经形式上无效了,编译器有权对它做任何事。这是 C UB 模型最反直觉的地方——UB 是语言层面的,跟硬件、跟 crash 都无关。 - quelsolaar(HN 最受欢迎的段子):”C 里 UB 的认知五阶段”——
- 否认:”我知道在我机器上 signed overflow 怎么处理。”
- 愤怒:”这编译器是垃圾!为什么不照我说的做!”
- 讨价还价:”我要给 WG14 提案修复 C……”
- 抑郁:”C 写的东西还能信任吗?”
- 接受:”别写 UB 就行。”
-
parasti(业内 20 年的反对意见):抛出一个真实困惑——”我写了 20 年 C,过去 6 个月在 HN 上看到的 UB 讨论比之前一辈子加起来还多。“工作里 UB 这话题从没出现过——你写代码、不工作就 debug、修。”为什么 UB 总能上 HN 首页?”——一位评论员的疑问反而说明了:UB 在工业实践和学院 / 标准党讨论里是两个平行宇宙。
-
bestouff(真正讲清楚问题):UB 真正的问题不是”在某些架构上会崩”——而是 “编译器假定 UB 不会发生”——所以优化器有权把 UB 代码翻译成任何对 happy path 方便的东西。有时候”任何”会意外到——比如整段
if检查被优化掉。 -
greysphere(认为文章有点 sensationalism):作者举的例子并不真的”是 UB”——它们是”在某些输入下会变成 UB“的代码。按这种标准,每个函数调用都是 UB(因为可能爆栈)。”C 有足够多真正的边角问题值得讨论,这种 sensationalism 反而模糊了新手的注意力,可能弊大于利。”
-
debugnik:更尖锐——”开头我同意,但例子不好,整篇文章就是个推销 LLM 写代码的幌子。”——作者文末确实建议用 LLM 扫 UB,被一些读者视为本末倒置。
-
jb1991:吐槽文章把 C 和 C++ 混着喷——”这是两门已经差得很远的语言了”——而且文中那些 raw pointer 和直接指针操作的 C++ 代码风格早已十年不算 idiomatic,今天会被视为 code smell。
-
maple3142(教科书式提问):试图把 UB 形式化——”程序 P 的输入空间分成 A(不触发 UB)和 B(触发 UB)。正确的编译器把 P 编译成 P’;对 A 中的输入 P’ 应该和 P 行为一致;对 B 中的任意输入,P’ 没有任何行为要求。“——这是教 UB 时最准的一个 mental model。
-
pizlonator(哲学派):”问题在于错误地假定规范在某种严格意义上有意义——其实没有。真正算数的是编译器实际做什么、真实 C 程序实际期待什么——两者在中间相遇形成的文化才是真实的 C。”
-
JonChesterfield(务实派):”你没法在标准 C 里写出 malloc——这比记得’写位转换时要用 char 指针 memcpy’重要得多。但这不要紧——你写的不是标准 C,你写的是编译器支持的方言,而且实际比标准暗示的要正常得多。”
- rurban(直接打作者脸):”非常糟的建议——好的新 LLM 当然知道 UB,但你还是该用 ubsan(
-fsanitize=undefined),不是 LLM。”