第94章 副作用:什么是副作用?i++ 和 ++i 的区别

说实话,做了十几年嵌入式开发,面试过不下两百个候选人,能把这个题目讲清楚的人,真不多。

很多人觉得 i++ 和 ++i 不就是个先后顺序吗?嗯,表面上看确实是这样。但如果你深入理解「副作用」这个概念,你会发现这两个操作符背后藏着 C 语言最核心的设计哲学。

一、什么是副作用?

先别急着看 i++,咱们先把「副作用」这个词搞明白。

在 C 语言里,表达式求值会产生两个东西:

  • 主要结果:表达式本身的值
  • 副作用:表达式求值过程中对程序状态造成的改变

举个例子:

int a = 5;
int b = a + 3;

这里 a + 3 的主要结果是 8。它有没有副作用?没有。因为 a 的值没变,程序状态没变。

再看这个:

int a = 5;
int b = a++;

这里 a++ 的主要结果是 5(自增前的值)。但副作用是什么?a 变成了 6。这个改变,就是副作用。

核心定义:副作用是指表达式求值过程中,对程序状态(如变量值、文件内容、硬件寄存器等)产生的持久性改变。

我当年刚入行时,在一个电机控制项目里写过这样的代码:

if (buffer[index++] > threshold) {
    // 处理数据
}

看起来没问题对吧?但后来调试时发现,有时候 index 多跳了一次。排查了半天,才发现是另一个线程也在用 index 做自增操作。这就是副作用带来的竞态问题。

二、i++ 和 ++i 的本质区别

好了,现在咱们来看这两个操作符。

操作符 主要结果 副作用 典型用途
i++(后置自增) 自增前的值 i 增加 1 循环体内部、数组索引
++i(前置自增) 自增后的值 i 增加 1 循环条件判断、链式赋值

说白了,两者的副作用是一样的——都是让 i 加 1。区别在于主要结果不同。

看代码:

int i = 3;
int x = i++;   // x = 3, i = 4

int j = 3;
int y = ++j;   // y = 4, j = 4

为什么会这样?因为后置自增的实现逻辑是:先保存当前值作为结果,再执行自增。前置自增则是:先执行自增,再返回新值。

我的个人习惯:在 for 循环里,我几乎只用 ++i。不是因为性能(现代编译器已经优化了),而是因为语义更清晰——我想表达的是「先自增,再使用」。

三、副作用带来的陷阱

这部分是我最想强调的。很多 bug 就出在副作用上。

陷阱1:同一个变量在同一个表达式里多次自增

int i = 1;
int arr[] = {10, 20, 30};
int result = arr[i++] + arr[i++];  // 未定义行为!

这段代码在有的编译器上结果是 30(10+20),在有的编译器上是 40(20+20)。为什么?因为 C 标准没有规定子表达式的求值顺序。i++ 的副作用什么时候生效?不知道。

我曾经踩过的坑:在一个通信协议解析模块里,我写了类似 buffer[head++] = data[tail++] 的代码。在 GCC 上跑得好好的,换到 IAR 编译器就出问题了。从那以后,我再也不在同一个表达式里对同一个变量做两次自增操作。

陷阱2:函数参数中的自增

int i = 0;
printf("%d %d", i++, i++);  // 未定义行为!

函数参数的求值顺序也是未定义的。你永远不知道哪个 i++ 先执行。

陷阱3:宏定义中的自增

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 5;
int y = MAX(x++, 10);  // x 可能被自增两次!

宏展开后变成 ((x++) > (10) ? (x++) : (10))。如果 x 原来等于 5,条件为假,x 只自增一次。但如果 x 原来等于 11,条件为真,x 会被自增两次。这种 bug 极难排查。

四、知识体系图

下面这张图帮你理清本章的核心逻辑:

副作用 表达式求值中的状态改变 主要结果 副作用 i++ → 返回原值 ++i → 返回新值 常见陷阱 同一表达式多次自增 | 函数参数 | 宏定义 副作用相同,主要结果不同 → 陷阱由此产生

五、最佳实践

说了这么多陷阱,那到底该怎么用?我总结了几条原则:

  1. 独立使用:如果只是单纯让 i 加 1,i++ 和 ++i 随便用,效果一样。
  2. 避免复合表达式:不要在一个表达式里对同一个变量多次自增。
  3. 函数参数里别用:调用函数时,参数里不要出现自增操作。
  4. 宏定义要小心:写宏时,如果参数可能被多次求值,就别用自增。
  5. 循环里用 ++i:虽然现代编译器没区别,但语义更清晰。

我建议:写代码时,把自增操作单独写一行。比如:

// 不推荐
arr[i++] = value;

// 推荐
arr[i] = value;
i++;

这样虽然多了一行,但可读性大大提高,也避免了副作用带来的问题。

六、总结

副作用这个概念,说白了就是「表达式干了额外的事」。i++ 和 ++i 的区别,本质上是「主要结果不同,副作用相同」。理解了这个,你就掌握了 C 语言中一个非常重要的设计思想。

我在项目中见过太多因为副作用导致的 bug,有的甚至让整个系统崩溃。记住一句话:副作用越少,代码越安全

嗯,这一章就到这里。希望你能把副作用这个概念刻在脑子里,写代码时多问自己一句:「这个表达式有没有副作用?」


公众号:蓝海资料掘金营,微信deep3321