加载失败
这篇讨论围绕一个老牌 C/C++ 面试题:`int a = 5; a = a++ + ++a;`。在 C/C++ 里,这类表达式会对同一变量造成未排序的多次修改,因此落入 UB;不同编译器如 gcc、clang、MSVC 可能给出不同值,甚至同一题在不同平台和优化级别下也会变。评论还提到,这类题在早年的校园招聘和教材里很常见,尤其常被当成“考输出”的标准题。之所以会存在,是因为早期 C 需要适应多种硬件和编译器,让实现自由选择求值顺序、做寄存器分配和指令调度;但现代编译器与 UBSan 这类工具已经更容易把问题暴露出来了。
很多人把这题当作典型的烂 interview puzzle:真正该说的是这里有 Undefined Behavior,而不是去背某个编译器碰巧给出的数字。有人直接表示,唯一合理的回答是“我不会写这种必须知道答案的代码”,因为这类写法本身就不该进入真实项目。也有人强调,看到这种代码就应该拆成分步赋值,别把可读性和可维护性拿去赌。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9]
另一组评论认为,这题真正考的不是算术,而是能不能在不失礼的情况下指出对方的技术错误。能让处在权威位置的人接受你对 UB 的纠正,反而被看作是很强的团队协作和沟通能力;如果面试官自己错了还不愿意被说服,那往往是很差的工作环境信号。甚至有人说,事后查证并认可候选人,才说明这个面试过程有价值。
评论里给出的主线解释是:C 早期要适配非常多样的硬件和很弱的编译器,所以标准刻意给实现者留出调度空间。有人举了 gcc 的实参求值顺序会受 calling convention 影响、Sethi-Ullman register allocation 会重排子表达式,以及 PDP-11(早期小型机)那类地址模式支持后自增,来说明这种自由度确实曾经有性能意义。也有人补充说,C89 才逐步引入 sequencing 规则,而像 Java 这样的语言后来直接规定了左到右求值,说明这更多是历史包袱而不是技术必然。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7]
不少人主张,这类在同一表达式里多次修改同一变量的写法应当直接变成 compile-time error,因为它本质上就是 footgun。有人指出,虽然别名和 pointer aliasing 让静态检测不可能覆盖所有情况,但这不妨碍语言把最危险的形式先拦住。还有人认为,与其把问题推给 UB,不如把语义定义得更明确,或者要求开发者写成 `a += 1` 这种显式形式。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7]
有人拿 onlinegdb 和 Godbolt 实测后发现,gcc/clang 常给出 12,而 MSVC 给出 13,clang 还会提示 `multiple unsequenced modifications`。评论里因此反复强调:一旦触发 UB,标准层面就没有唯一正确答案,讨论 11、12、13 只是把问题从“程序正确性”变成“碰运气”。也有人借机区分 `unspecified behavior` 和 `implementation-defined behavior`,但结论仍然是:这种代码没有可移植性。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9] [来源10]
一些人回忆,印度的学校、大学和校园招聘材料里长期把这类题当成标准考题,教材甚至暗示存在一个“固定输出”。这让很多学习者误以为掌握 `++` 的细节就等于掌握 C,直到在不同编译器上跑出不同结果、真正理解 UB 和 sequence points 才发现被误导了。也有人承认自己以前很爱这类脑筋急转弯,但后来更倾向于用更长、更清楚的写法来换取可读性。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7]
Undefined Behavior(UB): 语言标准不规定结果的行为;一旦触发,编译器和运行时都不保证任何可移植结果。
sequence points / sequencing(序列点/求值顺序约束): 用来约束表达式里值计算和副作用发生顺序的规则,是这类 `++` 题目的核心概念。
implementation-defined behavior(实现定义行为): 标准允许编译器选择一种行为,但必须文档化并保持一致;它和 UB 不同,但也未必可移植。