在C语言里,表达式求值并不总按我们读书时的从左到右顺序进行,编译器严格遵循一套由语言标准定义的运算符优先级与结合性规则。很多看似简单的语句,因为忽略了优先级,会产生完全不符合直觉的结果。理解这些规则,是写出正确且可维护C代码的基础。

一、C语言运算符优先级的基本框架
C语言标准将运算符分为约十五个优先级层次。位于最顶端的是括号 ()、数组下标 [] 以及结构体成员访问 . 和 ->,它们拥有最强绑定能力。其次是单目运算符,例如取地址 &、解引用 *、自增 ++ 等。再往下是乘除取模,接着是加减,然后是移位、关系运算、相等判断、位运算、逻辑运算,最后才是赋值类运算符。
结合性决定了同等优先级时运算的方向。大部分运算符为左结合,比如加减乘除从左往右算;但赋值运算符和单目运算符是右结合,例如 a = b = 0 实际等价于 a = (b = 0)。下面这段代码展示了优先级差异带来的不同结果:
#include <stdio.h>
int main() {
int a = 2, b = 3, c = 4;
int x = a + b * c; /* 等价于 a + (b * c),结果为14 */
int y = (a + b) * c; /* 强制先加,结果为20 */
printf("x=%d y=%dn", x, y);
return 0;
}
从上面例子可以看出,如果不加括号,乘法会先于加法执行。在实际工程中,过度依赖记忆优先级容易出错,稳妥的做法是对任何可能引起歧义的子表达式显式加括号,这并不会带来运行时开销,却显著提升可读性。
二、容易混淆的单目与后缀运算符
初学者经常分不清 *p++ 和 (*p)++ 的含义。根据优先级,后缀自增 ++ 比解引用 * 优先级更高,所以 *p++ 实际是 *(p++),即先取 p 指向的值,然后指针 p 自增。而 (*p)++ 才是将 p 指向的对象值加一。
我们通过一个遍历数组的示例来观察差异:
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30};
int *p = arr;
int val1 = *p++; /* val1得到10,p指向20 */
int val2 = (*p)++; /* val2得到20,arr[1]变为21 */
printf("val1=%d val2=%d arr1=%dn", val1, val2, arr[1]);
return 0;
}
这个例子说明,单目运算符虽然优先级高,但后缀形式又高于前缀和解引用,细微差别就会导致逻辑错误。在代码评审中,这类写法常被要求加上括号以避免误读。此外,sizeof 也是单目运算符,它内部的表达式不会真正求值,这也是容易忽略的特性。
三、位运算与逻辑运算的优先级陷阱
位运算符(&、|、^、<<、>>)的优先级低于关系运算符,而逻辑运算符(&&、||)又低于位运算符。很多人写掩码判断时会写出 if (flags & MASK == 0),本意是判断某位是否为零,但因为 == 优先级高于 &,它实际变成 flags & (MASK == 0),结果完全错误。
正确写法必须用括号改变顺序:
#include <stdio.h>
#define MASK 0x04
int main() {
int flags = 0x06;
/* 错误写法:if (flags & MASK == 0) 会被解析为 flags & (MASK == 0) */
if ((flags & MASK) == 0) {
printf("bit not setn");
} else {
printf("bit setn"); /* 此处会执行 */
}
return 0;
}
逻辑运算符还存在短路特性:&& 左侧为假时右侧不执行,|| 左侧为真时右侧不执行。由于优先级较低,它常和赋值混用时出错,比如 if (p = get() && q) 会被解析为 p = (get() && q),把逻辑结果赋给了 p,而不是先赋值再判断。这类问题可通过开启编译器警告来发现。
四、赋值与条件运算符的底层行为
赋值运算符优先级仅高于逗号运算符,因此它在表达式中通常最后执行。条件运算符 ? : 优先级高于赋值但低于逻辑运算,写 a = cond ? b : c 是安全的,但 a = b > c ? d : e 会先比较再选择再赋值。下面的代码演示了多重赋值与逗号运算的混合:
#include <stdio.h>
int main() {
int a, b, c;
a = b = c = 5; /* 右结合,全部为5 */
int m = (a += 2, b += 3, c += 4); /* 逗号表达式,m取最后一个值 */
printf("a=%d b=%d c=%d m=%dn", a, b, c, m);
return 0;
}
逗号运算符优先级最低,适合在 for 循环或宏中组合多个操作,但在普通赋值中加括号才明确意图。理解这些规则后,不必背诵整张表,只需记住括号最优先、赋值几乎最末、单目高于双目,遇到怀疑就加括号,并借助编译器警告与静态检查工具,就能稳定避开运算符优先级引发的bug。