1. 引言:为什么必须吃透运算符?
运算符(Operator)是 C 代码中最密集、最危险也最容易被轻视的"地雷区"。
一行看似人畜无害的表达式:
c
*p++ &= ~mask | 1<<bit;
背后同时涉及后缀自增 、指针解引用 、位运算 、复合赋值 、优先级 与序列点 六道关卡。
稍有疏忽就会引发未定义行为(UB)。本文尝试用"一张图 + 一张表 + 六条军规"把 C 语言 46 个运算符一次讲透,并给出工业级编码范式,助你安全通过所有关卡。
2. 运算符分类与优先级速查表
| 类别 | 运算符 | 优先级(1 最高) | 结合性 | 备注 |
|---|---|---|---|---|
| 后缀 | () [] -> . ++ -- |
1 | L→R | 函数调用、下标、成员访问 |
| 一元 | + - ! ~ ++ -- * & sizeof _Alignof |
2 | R→L | 类型转换也属此级 |
| 乘除 | * / % |
3 | L→R | % 要求整数 |
| 加减 | + - |
4 | L→R | 指针算术在此级 |
| 移位 | << >> |
5 | L→R | 负数移位 UB |
| 关系 | < <= > >= |
6 | L→R | 结果 0/1 |
| 相等 | == != |
7 | L→R | 容易误写 = |
| 位与 | & |
8 | L→R | 掩码常用 |
| 位异或 | ^ |
9 | L→R | 交换变量神器 |
| 位或 | ` | ` | 10 | L→R |
| 逻辑与 | && |
11 | L→R | 短路求值 |
| 逻辑或 | ` | ` | 12 | |
| 条件 | ?: |
13 | R→L | 三目运算符 |
| 赋值 | = += -= *= /= %= ... |
14 | R→L | 复合赋值同优先级 |
| 逗号 | , |
15 | L→R | 顺序求值,返回值最右 |
速记口诀:"后缀一元乘加减,移位关系等位逻,条件赋值逗号尾"
真记不住?加括号。这是 MISRA-C 推荐也是 Linux kernel 的硬性规范。
3. 位运算:被 90% 开发者浪费的"性能红利"
3.1 基本操作
| 运算符 | 功能 | 典型场景 |
|---|---|---|
& |
清除位 | flags & ~mask |
| ` | ` | 设置位 |
^ |
翻转位 | flags ^= toggle |
~ |
按位取反 | ~0u 生成全 1 |
<< >> |
移位 | 等价乘/除 2ⁿ,速度常数级 |
3.2 工业级技巧
-
无临时交换
ca ^= b; b ^= a; a ^= b;仅当
ab内存地址不同时可用,否则 UB。 -
判断奇偶
cif (x & 1) // 奇数 -
计算绝对值(无分支)
cint mask = x >> 31; int absx = (x ^ mask) - mask; -
位域 vs 掩码
位域语法糖可读性高,但对齐依赖实现;协议编码建议显式掩码 +
static inline读写函数,可移植且易审计。
4. 指针运算符:最锋利的"双刃剑"
| 运算符 | 读法 | 优先级 | 常见坑 |
|---|---|---|---|
& |
取地址 | 2(一元) | 不可对位域或寄存器变量使用 |
* |
解引用 | 2(一元) | 未初始化指针 UB |
-> |
间接成员 | 1 | 左侧必须是指向结构体的指针 |
[] |
下标 | 1 | a[i] 等价 *(a+i),因此 i[a] 也合法 |
优先级陷阱
c
int *p, arr[10];
*p++ = 0; // 后缀++ 优先级高于*,等价 *(p++)
(*p)++; // 先解引用,再把目标值 +1
安全范式
- 一次表达式只干一件事(Linux kernel CodingStyle 第 5 章)。
- 复杂偏移用临时变量,别把
*,&,++,--写进同一行。
5. 逻辑与条件:短路求值才是灵魂
c
if (p && p->ready) { ... } // p 为 NULL 时后半句不会执行
int x = (a > b) ? a : b; // 三目运算符可生成无分支汇编
注意:
-
&&,||,?:与,是唯四保证求值顺序的运算符,其余均不保证。 -
在
||后自增是常见笔误:cif (x || y++) // y 可能不被执行
6. 赋值与复合赋值:别忘了返回值是左操作数
c
a = b = c; // 右结合,等价 a = (b = c)
array[i++] = i; // UB,i 修改与求值无序列点
军规:
-
同一表达式中不要既用又改同一标量。
-
复合赋值自带隐式强制转换 :
cchar c = 1; c *= 255; // 先提升为 int,结果转回 char,可能溢出
7. 序列点与副作用:UB 的温床
C11 5.1.2.3 定义了序列点 (sequence point)。
关键规则:
- 前一条语句结束是序列点;
&&,||,?:,,的左操作数求值后是序列点;- 函数调用前对实参求值有一个序列点。
反面教材
c
printf("%d %d\n", i, i++); // UB
a[i] = i++; // UB
正面做法
拆分语句,让副作用落在不同语句:
c
int tmp = i++;
a[tmp] = i;
8. 性能视角:运算符背后的 CPU 故事
-
除法/取模
无硬件除法器的 MCU 上,
/,%会被编译器换成库函数调用 ,耗时数百周期。对 2ⁿ 取模用位与:
cx % 8 → x & 7 -
移位 vs 乘除
编译器已对
x * 16自动优化为x << 4,手写移位反而可能阻断其他优化 (如矢量化)。信任编译器,先写清晰再测性能。
-
位运算与分支
三目、异或等常可生成无分支汇编,在加密、图形像素处理中可显著降低分支预测失败惩罚。
9. 工业级编码军规(可直接写进团队手册)
-
优先级记不住就加括号,禁止秀技巧。
-
同一表达式只出现一次副作用。
-
位运算前显式无符号化 ,避免算术移位陷阱:
cunsigned int u = (unsigned int)x >> 24; -
对寄存器或协议字段写"读-改-写"函数,禁止裸运算符散布业务代码。
-
在
if/while条件里禁止自增/自减,除非团队一致同意。 -
启用
-Wsequence-point、-Werror,让编译器帮你排雷。
10. 结语:把运算符关进笼子里
C 语言给了程序员"距离硬件仅隔一层汇编"的特权,而运算符正是这层接口的核心。
理解优先级、结合性与序列点,不是为了炫技,而是为了把可能的未定义行为关进笼子里 。
当你能把一行复杂表达式拆成"一眼看懂、静态分析工具零告警"的三行代码时,才算真正驯服了 C 的运算符这只猛兽。
Happy hacking, and remember:
"括号是第一生产力,拆分是最佳设计模式。"