Int a = 5; a = a++ + ++a; a =? (2011)
摘要
分析了C/C++表达式 'a = a++ + ++a;' 在 int a=5 时的未定义行为,展示了因编译器相关的求值顺序和后置递增处理而可能出现的三种结果(11、12、13),并进行了理论和实验分析。
暂无内容
查看缓存全文
缓存时间: 2026/05/14 18:25
# int a = 5; a = a++ + ++a; a = ? 来源: https://gynvael.coldwind.pl/?id=372
我从 furio 收到了这个标题谜题,觉得它很有趣,于是在接下来的几天里把它传给了每一个可能对 C/C++ 问题感兴趣的人。有趣的地方在于未定义行为(UB),嗯……实际上是两个 UB,因此有三个可能的正确答案:11、12 和 13。我们先从理论角度分析可能的答案,然后再看实验结果(最初由 nism0 收集,后来由波兰镜像站(https://gynvael.coldwind.pl/?id=369)的读者扩展)。
好的……那么,哪些地方是 UB/未知/编译器相关的呢?
**第一个 UB**
首先,我们不知道哪个 **a** 会先被读取(我说的*读取*是指将值从内存拷贝到某个内部寄存器)。这里有两种可能:
可能性 1. 先读取第一个 **a**,然后执行前置递增,再读取第二个 **a**(暂时忽略后置递增的问题):
```
a = a + ++a;
步骤 1. 读取第一个 a。a = 5 + ++a; (a == 5)
步骤 2. 前置递增 a。a = 5 + a; (a == 6)
步骤 3. 读取第二个 a。a = 5 + 6; (a == 6)
步骤 4. 计算加法。a = 11;
```
可能性 2. 先执行前置递增并将结果存入内存,然后再读取 **a**。
```
a = a + ++a;
步骤 1. 前置递增 a。a = a + a; (a == 6)
步骤 2 和 3. 同时读取两个 a。a = 6 + 6; (a == 6)
步骤 4. 计算加法。a = 12;
```
因此,即使忽略后置递增,我们也能得到两种不同的可能性(11 和 12)。
**第二个 UB**
第二个 UB 与后置递增以及看似简单的那行代码 **a = a++** 有关。实际上,这里也有两种可能性(我用 **int a = 5; a = a++;** 作为例子来说明)。
术语说明:
**a_mem** - 仍在内存中的 **a**(例如栈上的局部变量)
**a_copy** - 之前读取的 **a** 的一个副本,保存在某个内部寄存器中
可能性 1. 后置递增丢失(MIA):
```
初始状态: (a_mem == 5, a_copy == n/a)
步骤 1. 读取 a。a = 5++; (a_mem == 5, a_copy == 5)
步骤 2. 对变量(内存中)执行后置递增。a = 5; (a_mem == 6, a_copy == 5)
步骤 3. 执行"赋值"——将 a_copy 移回 a_mem。(a_mem == 5, a_copy == n/a)
```
在上面的情况中,后置递增的结果丢失了——它被写入了内存,但很快又被另一个(未递增的)值覆盖了。(实际上我一直认为后置递增总是在所有操作完成后才执行,所以我个人会把这种行为视为编译器 bug。)
可能性 2. 后置递增推迟到最后执行:
```
初始状态: (a_mem == 5, a_copy == n/a)
步骤 1. 读取 a。a = 5++; (a_mem == 5, a_copy == 5)
步骤 2. 执行"赋值"——将 a_copy 移回 a_mem。(a_mem == 5, a_copy == n/a)
步骤 3. 对变量(内存中)执行后置递增。a++ (a_mem == 6, a_copy == n/a)
```
因此,后置递增在计算的最末尾执行,并且不会被覆盖。
**UB 总结**
将前面描述的两个 UB 应用于 **a = a++ + ++a** 表达式,得到:
可能性 1 和 1: 5+6,后置递增丢失,**结果为: 11**
可能性 2 和 1: 6+6,后置递增丢失,**结果为: 12**
可能性 1 和 2: 5+6,后置递增推迟,**结果为: 12**
可能性 2 和 2: 6+6,后置递增推迟,**结果为: 13**
**实验结果**
以下是实验结果(感谢 nism0 进行初始测试和制作初始表格,感谢 nonek 和 qyon 发现一些笔误):
| 编译器/语言版本 | 结果1 | 结果2 | 结果3 | 结果4 | 结果5 | 结果6 |
|---|---|---|---|---|---|---|
| `代码1` `int a = 5; a = a++ + a++;` | | | | | | |
| `代码2` `int a = 5; a = a++ + ++a` | | | | | | |
| `代码3` `int a = 5; a = ++a + a++;` | | | | | | |
| `代码4` `int a = 5; a = ++a + ++a;` | | | | | | |
| `代码5` `int a = 5; a = a++;` | | | | | | |
| `代码6` `int a = 5; a = a + ++a;` | | | | | | |
| gcc 2.95 | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.1 | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.2 | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.2.1 Apple | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.3 | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.3.3 | 12 | 13 | 13 | 14 | 6 | 12 |
| gcc 4.4.4 | 12 | 13 | 13 | 14 | 6 | 12 |
| gcc 4.6.0 (exp.) | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.5.1 MinGW64 | 12 | 13 | 13 | 14 | 6 | 12 |
| tcc 0.9.25 | ? | ? | ? | ? | 5 | 12 |
| bcc 0.16.17 | ? | ? | ? | ? | 5 | 12 |
| Microsoft C/C++ 16.00.30319.01 (80x86) | 12 | 13 | 13 | 14 | 6 | 12 |
| Embarcadero C++ 6.31 for Win32 | 12 | 13 | 13 | 14 | 6 | 12 |
| Intel C++ 12.0.1.127 | 12 | 13 | 13 | 13 | 6 | 12 |
| Keil C 9.02 | 11 | 12 | 12 | 13 | 6 | 12 |
| SDCC 3.0.1 #6092 | 11 | 12 | 13 | 14 | 5 | 12 |
| clang 2.8 | 11 | 12 | 12 | 13 | 5 | 11 |
| clang 1.6 Apple | 11 | 12 | 12 | 13 | 5 | 11 |
| PHP 5.2.10 | 11 | 12 | 12 | 13 | 5 | 12 |
| Java 1.6.0_06 | 11 | 12 | 12 | 13 | 5 | 11 |
| javac 1.4.2_12 | 11 | 12 | 12 | 13 | 5 | 11 |
| Java 1.6.0_21 | 11 | 12 | 12 | 13 | 5 | 11 |
| javac 1.6.0_22 | 11 | 12 | 12 | 13 | 5 | 11 |
| C# 2.0 | 11 | 12 | 12 | 13 | 5 | 11 |
| C# 4.0 | 11 | 12 | 12 | 13 | 5 | 11 |
| C# Mono 2.6.4 | 11 | 12 | 12 | 13 | 5 | 11 |
| Borland Turbo C++ for DOS 2.0 | 12 | 13 | 13 | 14 | 6 | 12 |
| HiSoft C for ZX Spectrum 1.3 | 11 | 12 | 12 | 13 | 5 | 12 |
感谢以下用户提供的额外结果:
- Icewall (http://icewall.pl/)
- Krzysztof Kotowicz (http://blog.kotowicz.net/) (PHP 5.2.10)
- mlen (2x clang, 2x gcc)
- none'a (2x Java)
- Keraj (http://keraj.net/) (2x Java)
- MDobak (SDCC & Keil C)
- garbaty lamer (3x C#, Turbo C++, HiSoft C)
- Xgrzyb90 (gcc 4.4.4)
- no_name (gcc 4.3.3)
- dikamilo (mingw64 4.5.1)
Garbaty lamer 还在波兰镜像站的评论中发布了一张来自 **HiSoft C for ZX Spectrum** 的超酷截图(点击放大):
HiSoft C for ZX Spectrum, 截图由 garbaty lamer 提供 (https://gynvael.coldwind.pl/img/hisoftgynvael_garbaty_lamer.png)
如果你也想测试你的编译器(非常欢迎在评论中反馈结果,尤其是来自奇怪/不常见的编译器和支持前置/后置递增的其他语言;>),你可以使用文章末尾的代码(见附录3)。
就是这样了;>
**附录1:**
在波兰镜像站(https://gynvael.coldwind.pl/?id=369)的评论中,Rolek 建议对重载运算符应用同样的测试(代码见波兰侧评论)。结果如下(Rolek 使用 MSVC++,我使用 g++):
| 编译器/语言版本 | 结果1 | 结果2 | 结果3 | 结果4 | 结果5 | 结果6 |
|---|---|---|---|---|---|---|
| `代码1` `int a = 5; a = a++ + a++;` | | | | | | |
| `代码2` `int a = 5; a = a++ + ++a` | | | | | | |
| `代码3` `int a = 5; a = ++a + a++;` | | | | | | |
| `代码4` `int a = 5; a = ++a + ++a;` | | | | | | |
| `代码5` `int a = 5; a = a++;` | | | | | | |
| `代码6` `int a = 5; a = a + ++a;` | | | | | | |
| Microsoft C/C++ (无重载) 16.00.30319.01 | **12** | **13** | **13** | **14** | **6** | **12** |
| Microsoft C/C++ (有重载) 16.00.30319.01 | **11** | **13** | **12** | **14** | **5** | **12** |
| g++ (无重载) 4.5.0 MinGW | **12** | **13** | **13** | **14** | **6** | **12** |
| g++ (有重载) 4.5.0 MinGW | **11** | **13** | **12** | **14** | **5** | **12** |
此外,krlm 提供了一个关于序列点(sequence points)的好文章链接:http://en.wikipedia.org/wiki/Sequence_point
**顺便说一句……** 如果你想提升二进制文件和协议方面的技能,可以看看我将在 4 月至 6 月期间举办的 workshop → Mastering Binary Files and Protocols: The Complete Journey (https://hackarcana.com/workshop-session/2025-Q1-Q1-mastering-binary/buy?utm=gyn-blog-inad)
**附录2:**
Garbaty lamer(再次,在波兰镜像站 https://gynvael.coldwind.pl/?id=369 的评论中)提到,在 C# 中这个谜题其实不是谜题——它是定义良好的行为,参见 C# 规范 §7.3:*表达式中的操作数从左到右求值。例如,在 F(i) + G(i++) * H(i) 中,方法 F 使用 i 的旧值调用,然后方法 G 使用 i 的旧值调用,最后方法 H 使用 i 的新值调用。这与运算符优先级是分开且无关的。*
**附录3:测试代码**
```c
#include <stdio.h>
int main(void){
int a = 5, b = 5, c = 5, d = 5, e = 5, f = 5;
// test 1
a = a++ + a++; printf("%i \n",a);
// test 2
b = b++ + ++b; printf("%i \n",b);
// test 3
c = ++c + c++; printf("%i \n",c);
// test 4
d = ++d + ++d; printf("%i \n",d);
// test 5
e = e++; printf("%i \n",e);
// test 6
f = f + ++f; printf("%i \n",f);
// done
return 0;
}
```
**附录4:来自 Cem Paya 的评论**
--开始--
与 C# 类似,这在 Java 中也不是谜题,因为 Java 规定求值是严格从左到右的。请参见 §15.7,那里有一些类似于你这种情况的副作用示例:http://java.sun.com/docs/books/jls/second_edition/html/expressions.doc.html
在 C++ 中这是未定义的,结果还可能取决于编译时使用的优化级别,这可以改变一个值被引用的次数。例如,编译器可能认为“a”在求值期间不会改变其值,并将其他出现的地方优化为同一个已读取的值。
==结束==
相似文章
当编译器让你惊喜
Matt Godbolt 探讨了编译器优化如何将 O(n) 求和循环转换为 O(1) 的闭式解,突出了 Clang 和 GCC 如何采用循环展开和数学简化等复杂技术来大幅提升代码性能。
@learnwithella:Claude Code 的自进化技能简直离谱 一次循环 → 10 次测试,按评估打分,重写提示词,再测试,保留胜者……
Claude Code 可通过运行评估、自动重写提示词并保留最优结果,实现提示词自迭代,让“钩子文案”技能一夜之间从 32/50 飙到 47/50。
C++26:标准库强化
C++26 引入了标准化的库强化机制,用于在运行时捕获常见的未定义行为(如越界访问)。基于 Google 的生产经验,此举仅带来 0.30% 的性能开销,同时将段错误减少了 30%。
7行代码,3分钟:实现一种编程语言(2010)
本文介绍了一种基于 Lambda 演算的图灵完备函数式语言的极简 7 行解释器,展示了 eval/apply 设计模式。
@gabriel1:如果 Claude 5.5 速度提升 20 倍,你就能边说边写代码,界面会随着你说话实时更新
有人推测,如果 Claude 5.5 速度提升 20 倍,用户将能够实时对话并编写代码,界面会随着语音输入同步更新。