原始题目

考察函数执行过程:以下代码的输出是什么?

int a = 0;
int f(int x, int y) {
    return x + y;
}
int main() {
    cout << f(++a, ++a);
    return 0;
}
  • A. 1
  • B. 2
  • C. 3
  • D. 未定义行为

答案:D(未定义行为)


直觉为什么会出错

很多人会这样推:a=0,两次 ++a,a 变成 2,两个参数是 1 和 2,输出 3——选 C。

这个推理有一个隐藏前提:“先算第一个参数,再算第二个参数”。但 C++ 标准从来没有做过这个承诺。

关键规则:实参求值顺序是无序的

  1. 函数实参之间的求值顺序是不确定的(unsequenced)。C++ 标准不保证 f(++a, ++a) 里先算哪个参数。GCC 在 x86 上常从右往左算,其他编译器可能从左往右。

  2. 更严重的是:这不仅是“顺序不确定”,而是未定义行为(UB)。两次 ++a 是对同一个变量 a 的两个副作用,且这两个副作用之间无序——C++ 标准规定:同一标量对象上两个无序的副作用,程序行为未定义。编译器可以生成任何代码、产生任何结果,甚至崩溃。

  3. 实际运行结果千差万别

    • GCC/x86 从右往左:两个参数都算完才传值,参数是 (2, 2),输出 4
    • 某些编译器从左往右:参数是 (1, 2),输出 3
    • 开优化后还可能输出完全不同的值

注意选项里根本没有 4 —— 这本身就是出题人的提示:想选出“确定值”就已经掉坑了

// g++ 10 实测(典型结果,不可依赖)
cout << f(++a, ++a);   // 常见输出 4,而不是 3

易混淆对比

写法 是否合法 说明
f(++a, ++a) ❌ UB 同一变量两次无序修改
f(a+1, a+1) ✅ 合法 只读取 a,不修改,输出 2
int b = a; f(++b, ++b); ✅ 合法 修改的是不同对象 b,输出 3
cout << ++a << ++a; ❌ UB 同理,<< 之间对 a 的多个修改也无序

常见的相关 UB(同一考点家族)

int i = 0;
cout << i++ << " " << i++;  // UB:两个无序副作用
i = i++ + 1;                // UB(C++17 前;C++17 起右侧先完全求值,已合法)
int j = i++ * i++;          // UB

C++17 引入了部分求值顺序规则(如函数实参求值虽然彼此无序,但整体先于函数体执行),但实参之间依然无序,所以 f(++a, ++a) 在 C++17 及之后仍然是 UB。

考点总结(CSP-J 初赛选择题)

  1. 功能:考察“序列点 / 求值顺序”概念,不是考递归也不是考函数调用栈。
  2. 结论:在同一个表达式里对同一变量修改多次(且没有序列点隔开)→ 未定义行为,答案选 D。
  3. 秒杀技巧:选择题里看到 f(++a, ++a)i++ + ++icout << i++ << i++ 这类“一个表达式里反复自增同一个变量”,直接选未定义行为,不用模拟。
  4. 正确写法:把修改拆到不同语句里:
int a = 0;
++a;            // 先改
int x = a;      // 再用
++a;
cout << f(x, a);   // 输出 3,清晰无歧义

思考题

cout << ++a + ++a;(a=0)的输出是确定的吗?

答案 不是确定值——同样是未定义行为。两个 ++a 对同一变量无序修改,任何输出都可能。