第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 加 1,i++ 和 ++i 随便用,效果一样。
- 避免复合表达式:不要在一个表达式里对同一个变量多次自增。
- 函数参数里别用:调用函数时,参数里不要出现自增操作。
- 宏定义要小心:写宏时,如果参数可能被多次求值,就别用自增。
- 循环里用 ++i:虽然现代编译器没区别,但语义更清晰。
我建议:写代码时,把自增操作单独写一行。比如:
// 不推荐
arr[i++] = value;
// 推荐
arr[i] = value;
i++;
这样虽然多了一行,但可读性大大提高,也避免了副作用带来的问题。
六、总结
副作用这个概念,说白了就是「表达式干了额外的事」。i++ 和 ++i 的区别,本质上是「主要结果不同,副作用相同」。理解了这个,你就掌握了 C 语言中一个非常重要的设计思想。
我在项目中见过太多因为副作用导致的 bug,有的甚至让整个系统崩溃。记住一句话:副作用越少,代码越安全。
嗯,这一章就到这里。希望你能把副作用这个概念刻在脑子里,写代码时多问自己一句:「这个表达式有没有副作用?」
公众号:蓝海资料掘金营,微信deep3321