运算符优先级:*p++ 到底先做哪个运算?
这个问题,我几乎每次面试新人都会问。说实话,十个里面有八个会答错。不是大家不努力,而是这个运算符优先级组合确实有点反直觉。
先看一段代码,你猜输出是什么?
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40};
int *p = arr;
int result = *p++;
printf("result = %d\n", result);
printf("*p = %d\n", *p);
return 0;
}
result 是 10 还是 20?p 最终指向哪里?
很多人会脱口而出:先取指针指向的值,然后指针自增。嗯,这个结论是对的。但问题是——为什么?
优先级与结合性的博弈
在 C 语言中,*(解引用)和 ++(自增)都是单目运算符。它们优先级相同,但结合性是从右向左。
这就意味着:*p++ 等价于 *(p++)。
等等,别急着背结论。我们拆开来看:
p++是后置自增,它先返回 p 的旧值,然后 p 自增*对 p 的旧值进行解引用
所以整个表达式的执行顺序是:
- 取出 p 当前的值(指向 arr[0])
- p 自增,指向 arr[1]
- 对取出的旧值解引用,得到 arr[0] 的值 10
result 是 10,p 指向 arr[1]。这就是答案。
核心结论:*p++ 等价于 *(p++),先取旧值,再自增,最后解引用。
我踩过的坑
我曾经在写一个环形缓冲区时,用了类似这样的代码:
// 从缓冲区读取一个字节
uint8_t data = *buffer_ptr++;
当时我想当然地以为:先解引用拿到数据,然后指针自增。结果调试了一下午,发现数据总是错位。后来才意识到,如果缓冲区指针刚好指向末尾,自增后越界了,但解引用拿到的却是旧位置的数据——逻辑上没问题,但配合边界检查时,顺序就变得极其重要。
从那以后,我给自己定了个规矩:但凡涉及指针操作和自增,一律加括号明确意图。
我的建议:不要依赖优先级记忆。写 (*p)++ 还是 *(p++),括号一加,谁都看得懂。代码是写给人看的,不是写给编译器看的。
几种常见变体
光一个 *p++ 还不够,我们看看它的亲戚们:
| 表达式 | 含义 | 示例结果(假设 p 指向 arr[0]) |
|---|---|---|
| *p++ | 取 *p,然后 p++ | 得到 arr[0],p 指向 arr[1] |
| (*p)++ | 取 *p,然后 (*p)++ | 得到 arr[0],arr[0] 变为 11 |
| *++p | 先 p++,再取 *p | p 指向 arr[1],得到 arr[1] |
| ++*p | 先取 *p,再 ++(*p) | 得到 arr[0]+1,arr[0] 变为 11 |
你看,括号位置一变,语义天差地别。我见过不少生产环境的 bug,根源就是这里少写了一对括号。
知识体系图
下面这张图帮你理清 *p++ 相关的运算符优先级关系:
避坑指南
我曾经在代码审查时看到过这样的写法:
while (*p++ != '\0') {
// 处理字符串
}
这段代码看起来没问题,但如果你在循环体里同时使用 p 和 *p,就要格外小心。因为 p 已经自增了,*p 拿到的已经是下一个字符。很多人在这里翻车。
另外,我建议你在以下场景中强制加括号:
- 表达式中有多个运算符混合时
- 代码需要跨平台移植时(不同编译器可能有细微差异)
- 团队中有新手成员时
- 你自己熬夜写代码时(相信我,凌晨三点的判断力会下降 50%)
总结
*p++ 这个表达式,说白了就是 C 语言运算符优先级的一个缩影。它不复杂,但容易混淆。记住三点:
- 后置 ++ 优先级高于 *,但结合性是从右向左
- 实际执行时,先取旧值,再自增,最后解引用
- 拿不准就加括号,没人会笑话你
嗯,优先级这东西,理解原理比死记硬背重要得多。你想想看,C 语言设计了这么多运算符,就是为了让我们灵活表达。但灵活不等于晦涩,写出别人能一眼看懂的代码,才是真本事。