前言
*p++、a = b = c、x & 0xFF == 0、i = i++ + 1——这些表达式有一个共同点:它们的含义完全取决于优先级(precedence)与结合性(associativity),而其中好几个的真实含义和你的直觉相反。
C 语言有 40 多个运算符,优先级表长到没人能背全。但经验丰富的 C 程序员并不靠背表,而是掌握三条策略:
- 记住影响最大的几组优先级关系;
- 遇到不确定就加括号;
- 能拆成多条语句就拆开,尤其要避免依赖求值顺序。
本文系统梳理优先级与结合性,重点讲解那些"写得出来但含义反直觉"的表达式,并给出可运行的验证代码。
一、优先级与结合性是什么
- 优先级(precedence):决定哪个运算符先与操作数结合。优先级高的先算。
- 结合性(associativity):当两个同优先级运算符相邻时,决定从哪边开始结合。分左结合(left-to-right)和右结合(right-to-left)。
以a - b - c为例,-是左结合,所以等价于(a - b) - c,而不是a - (b - c)。
以a = b = c为例,=是右结合,所以等价于a = (b = c)——这正是连续赋值能工作的原因。
二、优先级总表(常用部分)
从高到低排列:
| 优先级 | 运算符 | 说明 | 结合性 |
|---|
| 1 | ()[]->. | 函数调用、下标、成员 | 左 |
| 2 | !~++--+-*&(type)sizeof | 一元运算符(含强制类型转换) | 右 |
| 3 | */% | 乘除取模 | 左 |
| 4 | +- | 加减 | 左 |
| 5 | <<>> | 移位 | 左 |
| 6 | <<=>>= | 关系 | 左 |
| 7 | ==!= | 相等性 | 左 |
| 8 | & | 按位与 | 左 |
| 9 | ^ | 按位异或 | 左 |
| 10 | `\ | ` | 按位或 |
| 11 | && | 逻辑与 | 左 |
| 12 | `\ | \ | ` |
| 13 | ?: | 条件运算符 | 右 |
| 14 | =+=-=等 | 赋值 | 右 |
| 15 | , | 逗号 | 左 |
2.1 记忆口诀(旧版)
老教材有一个口诀:「括号成员第一;全体单目第二;乘除余三,加减四;移位五,关系六;等于不等排第七;位与异或和位或,三五之后八九十;逻辑与或十二起;条件十三,赋值十四;逗号最低。」
口诀只用于快速定位,真正写代码时请加括号。
三、八个最容易出错的表达式
3.1*p++
前缀/后缀++优先级(2)高于一元*(2,但后缀更紧),实际解析为*(p++)。
#include <stdio.h>
int main(void)
{
int arr[3] = {10, 20, 30};
int *p = arr;
int v = *p++; // 等价于 *(p++),v = 10,p 指向 arr[1]
printf("v = %d, *p = %d\n", v, *p); // v = 10, *p = 20
return 0;
}| 写法 | 解析 | 效果 |
|---|
*p++ | *(p++) | 取当前值,指针后移 |
(*p)++ | 明确 | 值加一,指针不动 |
*++p | *(++p) | 指针先移,再取值 |
++*p | ++(*p) | 值加一,指针不动 |
3.2x & 0xFF == 0
这是最经典的优先级陷阱。==(优先级 7)高于&(优先级 8),所以解析为x & (0xFF == 0),即x & 0,结果恒为 0。
#include <stdio.h>
int main(void)
{
unsigned int x = 0x100;
// ❌ 错误:等价于 x & (0xFF == 0) = x & 0 = 0
if (x & 0xFF == 0)
printf("low byte is zero (wrong logic)\n");
else
printf("else branch\n"); // 这里会执行
// ✅ 正确:加括号
if ((x & 0xFF) == 0)
printf("low byte is zero\n");
return 0;
}3.3a = b = c
=是右结合,解析为a = (b = c)。这是合法且常用的写法。
int a, b, c;
a = b = c = 5; // 全部为 5但要注意类型截断:
int i;
char ch;
i = ch = 300; // ❌ ch 先被截断为 300 & 0xFF = 44,i 得到 44 而非 3003.4i = i++ + 1
这是未定义行为,不是优先级问题。同一表达式中i既被修改又被读取,中间没有序列点。
int i = 0;
i = i++ + 1; // ❌ UB正确写法:
i = i + 1; // ✅3.5printf("%d", a ? b : c)与嵌套?:
?:优先级(13)很低,右结合。常见错误是把赋值写进条件分支:
int a = 1, b = 2, c = 3;
// 解析为 (a ? b : c) = 5?不,?: 不是左值
// 实际:a ? b : (c = 5) —— 若 a 为真,c 不会被赋值
a ? b : c = 5;
// ✅ 用括号明确
a ? b : (c = 5);3.6sizeof与表达式
sizeof是运算符(优先级 2,右结合),不是函数。sizeof(int)中的括号是类型语法的一部分。
int x = 5;
size_t s1 = sizeof x; // 4,不需要括号(但对变量可省)
size_t s2 = sizeof(int); // 4
size_t s3 = sizeof x + 1; // (sizeof x) + 1 = 5,不是 sizeof(x + 1)!
// ❌ 常见错误:以为会算 sizeof(x + 1)
// ✅ 明确写成 sizeof(x + 1)3.7 移位与加法的组合
+(优先级 4)高于<<(优先级 5)。
int a = 1, b = 2;
// 解析为 (1 + 2) << 3 = 24,不是 1 + (2 << 3) = 17
int r = a + b << 3;
// ✅ 明确意图
int r2 = (a + b) << 3;
int r3 = a + (b << 3);3.8 逗号运算符
逗号(优先级最低)会丢弃左侧的值,只返回右侧。
int a = (1, 2, 3); // a = 3
// ⚠️ 函数实参中的逗号是分隔符,不是逗号运算符
f(1, 2, 3); // 传三个参数
// 如果想传一个逗号表达式的值,要加括号
f((1, 2, 3)); // 传一个参数,值为 3注意:
int arr[2][3]里的逗号也是分隔符,不要混淆。
四、结合性的实际影响
4.1 左结合 vs 右结合的差异
#include <stdio.h>
int main(void)
{
int a = 10, b = 3, c = 2;
// 减法左结合:((10 - 3) - 2) = 5
printf("%d\n", a - b - c); // 5
// 赋值右结合:a = (b = (c = 7))
a = b = c = 7;
printf("%d %d %d\n", a, b, c); // 7 7 7
return 0;
}4.2 指针声明的"结合性"错觉
int *a, b;中*只作用于a,b是int。这不是优先级问题,而是声明符(declarator)语法。
int *a, b; // a 是 int*,b 是 int
int *a, *b; // 两个都是 int*五、实战:用编译器帮你抓优先级错误
GCC/Clang 提供-Wparentheses,能捕获大部分可疑的优先级写法。
gcc -Wall -Wextra -Wparentheses -O2 -o test test.c-Wall已经包含-Wparentheses,但显式写出更清晰。
#include <stdio.h>
int main(void)
{
unsigned int x = 0x100;
// 编译时会警告:suggest parentheses around comparison in operand of '&'
if (x & 0xFF == 0) { }
return 0;
}编译输出类似:
warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]5.1 用单元测试固定语义
#include <stdio.h>
#include <assert.h>
int main(void)
{
int arr[3] = {1, 2, 3};
// 固定 *p++ 的语义,防止误改
int *p = arr;
assert(*p++ == 1);
assert(*p == 2);
// 固定 sizeof 的语义
int x = 5;
assert(sizeof x + 1 == 5);
assert(sizeof(x + 1) == sizeof(int));
// 固定移位优先级
assert((1 + 2) << 3 == 24);
printf("all assertions passed\n");
return 0;
}把语义写成断言,比注释可靠得多。
常见坑点
坑点 1:&、|、^与==、!=的优先级关系反直觉。位运算符优先级低于相等性运算符,这是 C 语言设计上被广泛批评的一点。
// ❌ 全部需要括号
if (x & MASK == 0) { }
if (x | FLAG == FLAG) { }
// ✅
if ((x & MASK) == 0) { }
if ((x | FLAG) == FLAG) { }坑点 2:&&比||优先级高,导致条件分组错误。
// 解析为 a || (b && c)
if (a || b && c) { }
// ✅ 显式表达意图
if (a || (b && c)) { }
if ((a || b) && c) { }坑点 3:移动/取值与自增混用。
int arr[5] = {0};
int *p = arr;
// ⚠️ 需要加括号表达真实意图
int v = *p++; // 取 arr[0],p 后移
int w = (*p)++; // arr[1] 自增坑点 4:-的二元与一元歧义。
int a = 5, b = 3;
int r1 = a - -b; // 6,二元减 + 一元负
int r2 = a--b; // ❌ 词法解析为 a-- b,语法错误坑点 5:多重赋值中的截断。
int i;
char c;
i = c = 1000; // c 被截断,i 得到的是截断后的值坑点 6:条件运算符中遗漏括号导致赋值位置错误。
int a = 1, b = 2, c = 3;
// ⚠️ 实际是 a ? b : (c = 5)
a ? b : c = 5;
// ✅ 明确
a ? b : (c = 5);坑点 7:把->、.与*混用。
struct Node { int val; };
struct Node n = {1};
struct Node *p = &n;
// ✅ 两种等价写法,但优先级不同
int x1 = (*p).val; // 必须加括号
int x2 = p->val; // -> 优先级高,无需括号总结
| 表达式 | 实际解析 | 建议 |
|---|
*p++ | *(p++) | 加括号或拆开 |
x & 0xFF == 0 | x & (0xFF == 0) | (x & 0xFF) == 0 |
a = b = c | a = (b = c) | 可用,注意类型截断 |
a + b << 3 | (a + b) << 3 | 加括号 |
a ? b : c = 5 | a ? b : (c = 5) | 加括号 |
sizeof x + 1 | (sizeof x) + 1 | 加括号 |
i = i++ + 1 | UB | 拆成两条语句 |
三条黄金实践:
- 拿不准就加括号。括号不花钱,bug 很贵。
- 不要在一个表达式里堆超过两个运算符。拆成多条语句更易读。
- 开启
-Wall -Wextra并认真对待-Wparentheses警告。
优先级表值得看,但更值得养成的是"用括号显式表达意图"的习惯——因为代码是给人读的,编译器只是顺便执行它。