运算符优先级与结合性:优先级表、结合性概念、如何避免优先级问题
说到运算符,很多初学者觉得不就是加减乘除嘛,有什么好讲的?
但我在项目中见过太多因为优先级搞错而引发的bug了。有一次排查一个通信协议栈的bug,整整花了两天,最后发现就是一行代码里 && 和 || 的优先级搞混了。嗯,从那以后,我每次写复杂表达式都会多看一眼优先级表。
一、运算符优先级表
C语言里运算符有几十个,优先级从高到低分了15个等级。你不需要全部背下来,但常用的那几个必须烂熟于心。
| 优先级 | 运算符 | 名称 | 结合性 |
|---|---|---|---|
| 1(最高) | () [] -> . | 括号、数组下标、成员访问 | 左→右 |
| 2 | ! ~ ++ -- + - * & (类型) sizeof | 单目运算符 | 右→左 |
| 3 | * / % | 乘除取模 | 左→右 |
| 4 | + - | 加减 | 左→右 |
| 5 | << >> | 移位 | 左→右 |
| 6 | < <= > >= | 关系比较 | 左→右 |
| 7 | == != | 相等判断 | 左→右 |
| 8 | & | 按位与 | 左→右 |
| 9 | ^ | 按位异或 | 左→右 |
| 10 | | | 按位或 | 左→右 |
| 11 | && | 逻辑与 | 左→右 |
| 12 | || | 逻辑或 | 左→右 |
| 13 | ?: | 三目运算符 | 右→左 |
| 14 | = += -= *= /= %= <<= >>= &= ^= |= | 赋值运算符 | 右→左 |
| 15(最低) | , | 逗号运算符 | 左→右 |
你想想看,优先级最高的其实是括号和成员访问,最低的是逗号运算符。这个顺序是有道理的——括号最优先,因为它要明确指定计算顺序;逗号最靠后,因为它只是把多个表达式串起来。
核心记忆口诀:
单目 > 算术 > 移位 > 关系 > 位运算 > 逻辑 > 三目 > 赋值 > 逗号
单目 > 算术 > 移位 > 关系 > 位运算 > 逻辑 > 三目 > 赋值 > 逗号
二、结合性的概念
优先级决定了谁先算,但遇到同一优先级的运算符时,谁先算?这就看结合性了。
说白了,结合性就是「当优先级相同时,计算方向是左到右,还是右到左」。
举个例子:
int a = 3, b = 4, c = 5;
int result = a - b - c;
// 左结合:先算 a - b = -1,再算 -1 - c = -6
加减法是左结合,所以从左往右算。但赋值运算符是右结合:
int x, y, z;
x = y = z = 10;
// 右结合:先算 z = 10,再算 y = z,最后 x = y
我记得刚学C语言时,看到 x = y = z = 10 这种写法觉得挺奇怪的。后来才明白,这就是右结合的实际应用——从右往左依次赋值。
个人经验: 我写嵌入式代码时,很少连续赋值。因为有些编译器优化会搞出奇怪的行为。建议一行只赋一个值,代码可读性更好。
三、如何避免优先级问题——用括号
你可能会问:优先级表这么长,我记不住怎么办?
我的回答很简单:记不住就别硬记,用括号。
括号的优先级是最高的,而且它能让代码意图一目了然。我个人的习惯是:只要表达式里用了两种以上不同的运算符,就加括号。
来看几个我踩过的坑:
坑1:移位和加减混用
// 错误写法
int val = 1 << 2 + 3; // 你以为的是 (1<<2)+3 = 7
// 实际是 1 << (2+3) = 32
// 正确写法
int val = (1 << 2) + 3; // 明确告诉编译器先移位再加
为什么会这样?因为 + 的优先级(4级)比 <<(5级)高。所以先算加法,再移位。
坑2:逻辑运算符和比较运算符混用
// 我曾经在状态机里写过这样的代码
if (status & 0x02 == 0x02) // 本意是判断第1位是否为1
// 实际:== 优先级高于 &,所以先算 0x02 == 0x02 = 1
// 再算 status & 1,结果完全不对!
// 正确写法
if ((status & 0x02) == 0x02) // 先按位与,再比较
曾经踩过的坑: 我在一个产品代码里看到过
if (a & b == c) 这种写法,结果调试了整整一个下午。从那以后,我要求团队所有成员:位运算必须加括号,逻辑运算必须加括号,比较运算也建议加括号。
坑3:三目运算符嵌套
// 三目运算符是右结合,但可读性极差
int max = a > b ? a > c ? a : c : b > c ? b : c;
// 我建议拆开写,或者加括号
int max = (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c);
// 但说实话,这种嵌套三目我一般不用,直接 if-else 更清晰
四、知识体系总览
下面这张图把优先级、结合性、避坑方法串起来了,你可以保存下来随时看:
五、总结几条实用建议
- 记优先级不用死记硬背:记住「单目 > 算术 > 关系 > 逻辑 > 赋值」这个顺序就够了,其他查表。
- 结合性只关注右结合的运算符:赋值、三目、单目是右结合,其他基本都是左结合。
- 括号是你的朋友:我写代码时,只要表达式里用了两种以上运算符,就加括号。代码是给人读的,不是给编译器读的。
- 代码审查时重点看表达式:我每次做code review,都会盯着那些没有括号的复杂表达式多看几眼。十有八九能发现问题。
我的习惯: 在嵌入式项目中,我甚至会在宏定义里加括号,哪怕看起来多余。比如
#define MUL(a,b) ((a)*(b)),外面再加一层括号,确保宏展开后不会出问题。这是血的教训换来的经验。
好了,优先级和结合性就讲到这里。记住一句话:括号不丢人,丢人的是bug查不出来。
公众号:蓝海资料掘金营,微信deep3321