运算符优先级:*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 的旧值进行解引用

所以整个表达式的执行顺序是:

  1. 取出 p 当前的值(指向 arr[0])
  2. p 自增,指向 arr[1]
  3. 对取出的旧值解引用,得到 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++ 相关的运算符优先级关系:

*p++ 运算符优先级解析 *p++ 等价于 *(p++) 后置 ++ 优先级高 先返回旧值,再自增 解引用 * 优先级低 对旧值解引用 执行顺序: ① p++ 返回旧地址 ② p 自增 ③ * 解引用

避坑指南

我曾经在代码审查时看到过这样的写法:

while (*p++ != '\0') {
    // 处理字符串
}

这段代码看起来没问题,但如果你在循环体里同时使用 p 和 *p,就要格外小心。因为 p 已经自增了,*p 拿到的已经是下一个字符。很多人在这里翻车。

另外,我建议你在以下场景中强制加括号

  • 表达式中有多个运算符混合时
  • 代码需要跨平台移植时(不同编译器可能有细微差异)
  • 团队中有新手成员时
  • 你自己熬夜写代码时(相信我,凌晨三点的判断力会下降 50%)

总结

*p++ 这个表达式,说白了就是 C 语言运算符优先级的一个缩影。它不复杂,但容易混淆。记住三点:

  1. 后置 ++ 优先级高于 *,但结合性是从右向左
  2. 实际执行时,先取旧值,再自增,最后解引用
  3. 拿不准就加括号,没人会笑话你

嗯,优先级这东西,理解原理比死记硬背重要得多。你想想看,C 语言设计了这么多运算符,就是为了让我们灵活表达。但灵活不等于晦涩,写出别人能一眼看懂的代码,才是真本事。

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