C 语言运算符全景:从入门到进阶

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 工业级技巧

  1. 无临时交换

    c 复制代码
    a ^= b; b ^= a; a ^= b;

    仅当 a b 内存地址不同时可用,否则 UB。

  2. 判断奇偶

    c 复制代码
    if (x & 1)   // 奇数
  3. 计算绝对值(无分支)

    c 复制代码
    int mask = x >> 31;
    int absx = (x ^ mask) - mask;
  4. 位域 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

安全范式

  1. 一次表达式只干一件事(Linux kernel CodingStyle 第 5 章)。
  2. 复杂偏移用临时变量,别把 *, &, ++, -- 写进同一行。

5. 逻辑与条件:短路求值才是灵魂

c 复制代码
if (p && p->ready) { ... }     // p 为 NULL 时后半句不会执行
int x = (a > b) ? a : b;       // 三目运算符可生成无分支汇编

注意

  • &&, ||, ?:,唯四保证求值顺序的运算符,其余均不保证。

  • || 后自增是常见笔误:

    c 复制代码
    if (x || y++)   // y 可能不被执行

6. 赋值与复合赋值:别忘了返回值是左操作数

c 复制代码
a = b = c;        // 右结合,等价 a = (b = c)
array[i++] = i;   // UB,i 修改与求值无序列点

军规

  1. 同一表达式中不要既用又改同一标量。

  2. 复合赋值自带隐式强制转换

    c 复制代码
    char 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 故事

  1. 除法/取模

    无硬件除法器的 MCU 上,/, % 会被编译器换成库函数调用 ,耗时数百周期。

    对 2ⁿ 取模用位与:

    c 复制代码
    x % 8  →  x & 7
  2. 移位 vs 乘除

    编译器已对 x * 16 自动优化为 x << 4手写移位反而可能阻断其他优化 (如矢量化)。

    信任编译器,先写清晰再测性能

  3. 位运算与分支

    三目、异或等常可生成无分支汇编,在加密、图形像素处理中可显著降低分支预测失败惩罚。


9. 工业级编码军规(可直接写进团队手册)

  1. 优先级记不住就加括号,禁止秀技巧

  2. 同一表达式只出现一次副作用

  3. 位运算前显式无符号化 ,避免算术移位陷阱:

    c 复制代码
    unsigned int u = (unsigned int)x >> 24;
  4. 对寄存器或协议字段写"读-改-写"函数,禁止裸运算符散布业务代码

  5. if/while 条件里禁止自增/自减,除非团队一致同意。

  6. 启用 -Wsequence-point-Werror,让编译器帮你排雷。


10. 结语:把运算符关进笼子里

C 语言给了程序员"距离硬件仅隔一层汇编"的特权,而运算符正是这层接口的核心。

理解优先级、结合性与序列点,不是为了炫技,而是为了把可能的未定义行为关进笼子里

当你能把一行复杂表达式拆成"一眼看懂、静态分析工具零告警"的三行代码时,才算真正驯服了 C 的运算符这只猛兽。

Happy hacking, and remember:
"括号是第一生产力,拆分是最佳设计模式。"

相关推荐
csbysj20205 小时前
Perl 格式化输出
开发语言
运维李哥不背锅6 小时前
Ansible 模块详解:高效管理你的 IT 基础设施
服务器·网络·ansible
tao3556676 小时前
【Python刷力扣hot100】42. Trapping Rain Water
开发语言·python·leetcode
消失的旧时光-19436 小时前
Kotlin 协程最佳实践:用 CoroutineScope + SupervisorJob 替代 Timer,实现优雅周期任务调度
android·开发语言·kotlin
错把套路当深情6 小时前
Kotlin保留小数位的三种方法
开发语言·python·kotlin
せいしゅん青春之我7 小时前
【JavaEE初阶】网络原理——TCP报文结构、确认应答机制
网络·笔记·网络协议·tcp/ip·java-ee
赵谨言8 小时前
基于Python Web的大数据系统监控平台的设计与实现
大数据·开发语言·经验分享·python
cellurw8 小时前
Day72 传感器分类、关键参数、工作原理与Linux驱动开发(GPIO/I²C/Platform/Misc框架)
linux·c语言·驱动开发
专注前端30年8 小时前
Vue2 中 v-if 与 v-show 深度对比及实战指南
开发语言·前端·vue