
许多人第一次被 C 语言“整顿”的瞬间,往往来自一行看似无害的表达式:为什么 1 + 2 * 3 是 7 而不是 9?为什么 a/b*c 和 a/(b*c) 完全不同?再列如把 i++ 混进来,程序甚至可能直接掉进“未定义行为”的深坑。本文从工程实践出发,把“算术运算符的优先级与结合性”讲清楚,并配上容易踩坑的例子与可操作的改写准则。
一、算术运算符家族与“两个核心维度”
在 C 里,围绕数值计算的常见运算符主要有:
- 一元:+(正号)、-(取负)、++(自增)、--(自减)
- 二元:*、/、%、+、-
- 括号:( )(分组,改变默认结合顺序;不是运算符,但常与优先级一起讨论)
判断表达式值的两个核心维度:
- 优先级(precedence):谁先算。
- 结合性(associativity):同优先级从左往右还是从右往左。
牢记一句话:先看优先级,再按结合性。
二、只谈算术的“最小必记”表
下面这张表只保留算术相关条目,按从高到低排列,后缀/前缀也标出结合性:
层级 | 运算类别 | 代表 | 结合性 |
最高 | 括号分组 | (expr) | —(强制分组) |
后缀 | 后缀自增/自减 | i++、i-- | 左结合 |
前缀 | 前缀自增/自减/一元正负 | ++i、--i、+i、-i | 右结合 |
乘除模 | 乘、除、取余 | *、/、% | 左结合 |
加减 | 加、减 | +、- | 左结合 |
经验法则
乘除模优先于加减;同层级按左结合。
一元运算(如 -i、++i)整体早于乘除;后缀 i++ 优先级高于前缀 ++i。
括号能“越级”,是最稳的“保险丝”。
三、从规则到结果:逐步拆解 6 个表达式
例 1:基础混合
int v = 1 + 2 * 3 - 4 / 2; // 1 + 6 - 2 -> 5
先乘除(左结合),再加减,得到 5。
例 2:同级左结合的威力
int v = 24 / 5 % 3; // (24/5)=4,再 4%3=1 -> v=1
若“凭感觉”写成 24 / (5 % 3),结果会变成 12,完全不同。
例 3:一元负号与乘法
int a = -2 * 3 + 1; // (-2*3)=-6,再 +1 -> -5
一元 - 在乘除之前结合到常量上。
例 4:自增参与但不作死
int i = 3;
int x = (i++) + 2; // 使用后再加1:x=5,i=4(安全)
只有一处修改 i,且另一侧不依赖 i,可读性与行为都清晰。
例 5:反例:未定义行为(UB)
int i = 2;
int y = i++ * ++i; // UB:一个序列点内对 i 的多次修改/读取冲突
这不是“结果不确定”,而是程序行为未定义——编译器可以做任何事情。修正:
int i = 2;
int a = i++; // a=2, i=3
int b = ++i; // i=4, b=4
int y = a * b; // y=8
例 6:同级左结合不等同“先算大括号”
double t1 = 1.0/2*2; // 0.5*2 -> 1.0
double t2 = 1/2*2; // 0*2 -> 0 (整数除法先发生)
优先级不解决类型问题,见下一节。
四、类型提升与“常规算术转换”——看不见的裁判
运算之前,C 会做一系列隐式转换,决定以什么精度计算:
- 整数提升(integer promotions) char、short 等会先提升为 int(或 unsigned int),再参与运算。
- 常规算术转换(usual arithmetic conversions) 为了让两侧“说同一种语言”:
- 若任一操作数是 long double/double/float,另一侧向其提升;
- 否则在整数层面按“类型等级与有无符号”规则对齐到共同类型(例如 int 与 long long 结果为 long long)。
这些规则直接决定除法与取模的语义:
- 整数除法 /:截断趋零。例如 -7/3 == -2。
- 取模 %:余数与被除数同号,-7 % 3 == -1。
- % 只对整数有效,浮点取模请用 fmod(math.h)。
关键结论
1/2 按整数算是 0;1.0/2 才是 0.5。
摆放顺序不变的情况下,类型决定了过程:(double)a/b 与 a/(double)b 都触发浮点除法,而 a/b 后再转 double 只是把 0 变成 0.0。
有符号整数溢出是未定义行为;无符号整数按模 2^N 回绕(定义良好但常常不是你要的)。
五、五个高频坑位与可执行改写
- 把 ++/-- 混进复杂表达式
- ❌ a += a++、i = i++ + ++i(UB)
- ✅ 拆解成独立语句;或仅在独立行使用 i++/--i。
- 指望优先级“自动分组”
- ❌ a/b*c 以为是 a/(b*c)
- ✅ 用括号表达意图:a / (b*c) 或 (a/b) * c。
- 整数与浮点混算“被降级”
- ❌ avg = sum / n;(整数)
- ✅ avg = sum / (double)n; 或 avg = (double)sum / n;。
- 溢出的隐蔽性
- ❌ int area = 100000 * 100000;(32 位溢出)
- ✅ long long area = 100000LL * 100000; 或使用范围检查。
- 宏里重复求值 #define SQR(x) ((x)*(x))
int v = SQR(i++); // i++ 被求值两次 -> UB - ✅ 改为内联函数:static inline int sqr(int x){ return x*x; }
六、工程实践的三条“硬规范”
- 能加括号就加括号:用括号表达“设计意图”,而不是考读者记表。
- 自增自减只做“单兵行动”:把副作用隔离到独立语句。
- 在边界上写测试:零、负数、极大/极小值、不同平台 int 宽度、是否开启优化等。
七、十个“即刻辨析”练习(附解析)
自测时先写出你的第一反应,再对照解析找出“规则”在哪起作用。
- 3 + 4 * 2 - 8 / 3 答:3 + 8 - 2 = 9。先乘除,后加减。
- 24 / 5 % 3 答:(24/5)=4,4%3=1。同级左结合。
- -3*2 + 5 答:-6 + 5 = -1。一元 - 先结合到 3。
- 1/2*2 与 1.0/2*2 答:前者 0*2=0,后者 0.5*2=1.0。类型主导。
- 7 % -3 与 -7 % 3 答:7%-3 == 1,-7%3 == -1。余数与被除数同号。
- int i=2; int x = i++ + 2; 答:x=4?不,是 x=4?——错。应为 x=2+2=4?再加 1,i=3。 解析:后缀自增返回旧值,表达式求值后再加 1。
- int i=2; int y = i++ * ++i; 答:未定义行为。同一序列点对同一对象多次修改/读取。
- int a=5,b=2; double r = a/b; 答:2.0 而非 2.5。先整数除法,后转为 double。
- int x=100000, y=100000; int z = x*y; 答:可能溢出(32 位)。用 long long z = 1LL*x*y;。
- int n=3; int m = n+++n; 答:词法解析为 (n++) + n,结果 3+4=7(n 先参与加法后变 4)。可读性极差,避免。
八、把规则内化为“肌肉记忆”的写法
- 表达式=数据流 + 副作用:尽量别混。
- 括号是文档的一部分:给未来的你和同事看。
- 类型显式化:凡是涉及除法、边界、跨平台,先把类型对齐到你期望的精度。
- 单元测试要“脏”:专挑负数、零、极值去打;模 0、除 0、溢出、未定义都要在测试中暴露。
当你把“优先级—结合性—类型转换”这三板斧用顺手,C 的算术表达式就不再是谜语,而是可控、可读、可维护的工程资产。