C语言运算符优先级潜规则
前言
当运算符开始“论资排辈”,你的代码还好吗?
亲爱的程序员朋友,你是否曾经写下 a = b + c * d
,自信满满地按下编译键,结果程序跑出来的数字让你怀疑人生?
你是否在深夜调试时,盯着 if (x & y == z)
这样的代码,内心咆哮:“这TM到底先算谁?!”
别慌,你不是一个人!C语言的运算符优先级,就像一群性格迥异的大佬聚在一起开会——*
觉得自己的优先级天下第一,++
在旁边冷笑:“呵,你算老几?”,而 =
只能卑微地等所有人吵完再干活……
本文将带你走进这场“运算符权力的游戏”,用最接地气的方式,帮你理清谁先算、谁后算,从此告别“优先级玄学”,让代码不再“薛定谔的正确”!
(友情提示:阅读本文后,你可能再也不会写出 a+++++b
这种让编译器崩溃的代码了……)
目录
一、C语言中的运算符
运算符类别 | 运算符 |
---|---|
下标引用、函数调用和结构成员 | [] () -> . |
单目运算符 | ! ~ ++ -- + - sizeof (type) * & |
算术运算符 | + - * / % |
移位运算符 | << >> |
关系运算符 | > >= < <= == != |
位运算符 | & ^ | |
逻辑运算符 | && || |
赋值运算符 | = += -= *= /= %= &= ^= |= <<= >>= |
条件运算符(三目运算符) | expr1 ? expr2 : expr3 |
逗号运算符 | , |
二、C语言中运算符的优先级
C语言运算符优先级表(由上至下,优先级依次递减)
运算符 | 结合性 |
---|---|
() [] -> . |
L-R |
! ~ ++ -- + - (type) * & sizeof |
R-L |
* / % |
L-R |
+ - |
L-R |
<< >> |
L-R |
< <= > >= |
L-R |
== != |
L-R |
& |
L-R |
^ |
L-R |
| |
L-R |
&& |
L-R |
|| |
L-R |
= += -= *= /= %= &= ^= |= <<= >>= |
R-L |
expr1 ? expr2 : expr3 |
R-L |
, |
L-R |
L-R(Left-to-Right,从左到右结合)
R-L(Right-to-Left,从右到左结合)
三、帮助记忆优先级的方法
1、先粗分
优先级最高的其实并不是真正意义上的运算符,包括数组下标、函数调用操作符和各结构体成员选择操作符。
单目运算符的优先级仅次于前述运算符,在所有真正意义是的运算符中,单目运算符的优先级最高。
优先级比单目运算符要低的,接下来就是双目运算符。
双目运算符之后就是三目运算符(条件运算符)。
优先级最低的就是逗号运算符了。
2、再细分
需要进一步细分的就是双目运算符了,我们需要记住:在双目运算符中,算术运算符的优先级最高,移位运算符次之,关系运算符再次之,接着就是位运算符,最后是逻辑运算符。
你们可能以为我忘了双目运算符中还有一个赋值运算符,其实赋值运算符算是一个特例:赋值运算符的优先级低于三目运算符(条件运算符)。
需要注意的最重要的两点是:
- 任何一个逻辑运算符或位运算符的优先级低于任何一个关系运算符。
- 移位运算符的优先级比算术运算符要低,但是比关系运算符要高。
3、直接拿下
C语言运算符优先级与数学思维的完美对应
1.算术运算符:与数学规则高度一致
C语言中的算术运算符优先级设计完全符合我们的数学常识:
- 高级运算:乘法(
*
)、除法(/
)和取模(%
)具有相同的高优先级 - 低级运算:加法(
+
)和减法(-
)优先级较低
这种设计使得表达式a + b * c
会先计算乘法后计算加法,与数学中的运算顺序完全一致,大大降低了学习成本。
2. 关系运算符:特殊注意相等性判断
在关系运算符中有一个需要特别注意的优先级规则:
- 比较运算符:
>
、>=
、<
、<=
具有较高优先级 - 相等性判断:
==
和!=
的优先级相对较低
这意味着表达式a > b == c < d
实际上等价于(a > b) == (c < d)
,这种设计使得复合逻辑判断更加直观。
3.位运算与逻辑运算:严谨的优先级层次
位运算符和逻辑运算符的优先级设计体现了严谨的逻辑层次:
- "与"类运算优先级最高:
- 按位与
&
高于按位或|
- 逻辑与
&&
高于逻辑或||
- 按位与
- 异或运算居中:
- 按位异或
^
的优先级介于按位与&
和按位或|
之间
- 按位异或
这种层次分明的优先级设计使得位操作表达式能够准确表达开发者的意图,例如a & b ^ c | d
会按照(a & b) ^ c) | d
的顺序计算。
四、因不明确优先级而造成的常见问题
1、经典优先级陷阱案例
1. 指针解引用与自增的"暧昧关系"
int arr[] = {1, 2, 3};
int *p = arr;
int val = *p++; // 你以为是什么?
问题分析:
- 很多人误以为等价于
(*p)++
,实际是*(p++)
- 因为
++
优先级高于*
,且是右结合
正确写法:
(*p)++; // 这才是解引用后自增
2. 位运算与比较运算的"优先级之争"
int flags = 0x0F;
if (flags & 0x0F == 0x0F) { // 这个条件永远为假!
printf("All bits set\n");
}
问题分析:
==
优先级高于&
,所以实际是flags & (0x0F == 0x0F)
(0x0F == 0x0F)
结果为1,所以实际是flags & 1
正确写法:
if ((flags & 0x0F) == 0x0F)
2、那些年我们踩过的优先级坑
1. 移位运算与算术运算的"爱恨情仇"
int x = 1 << 2 + 3; // 你以为结果是32?其实是128!
解析:
+
优先级高于<<
,所以是1 << (2 + 3) = 1 << 5 = 32
- 但如果你想要
(1 << 2) + 3 = 4 + 3 = 7
,就需要加括号
2. 逻辑运算的"短路特性"引发的优先级误解
int a = 1, b = 0;
if (a == 1 || b == 1 && some_expensive_function()) {
// 你以为some_expensive_function()不会执行?
}
解析:
&&
优先级高于||
,所以等价于a == 1 || (b == 1 && some_expensive_function())
- 由于
a == 1
为真,||
短路,后面的确实不会执行 - 但这样的代码可读性很差,建议加括号明确意图
3、防御性编程建议
多用括号:即使你知道优先级规则,加括号也能提高代码可读性
// 不推荐 a = b + c * d; // 推荐 a = b + (c * d);
分解复杂表达式:将复杂表达式拆分成多个简单语句
// 不推荐 result = (*ptr++) + (x << 2) & mask; // 推荐 int val = *ptr; ptr++; int shifted = x << 2; result = (val + shifted) & mask;
编写单元测试:对涉及复杂运算符的代码编写测试用例
4、常见优先级陷阱速查表
危险组合 | 常见误解 | 实际含义 | 解决方案 |
---|---|---|---|
*p++ |
(*p)++ |
*(p++) |
明确加括号 |
a & b == c |
(a & b) == c |
a & (b == c) |
加括号 |
a << b + c |
(a << b) + c |
a << (b + c) |
加括号 |
a = b = c |
从左到右赋值 | 从右到左赋值 | 保持原样或拆分 |
结语
运算符优先级就像交通规则,了解它们可以避免代码中的"交通事故"。记住:当你对优先级有哪怕一丝怀疑时,就加上括号吧!这不会降低你的专业度,反而会让你的代码更健壮、更易维护。
最后的小测试:你能一眼看出下面代码的问题吗?
int x = 5, y = 10, z = 15; int result = x > y ? y : z + 10;
答案在评论区见!