C 语言第 5 章:选择结构——分支语句与运算符优先级
程序从"能算"变成"会判断",就是从这一章开始的。选择结构的语法只有 if 和 switch 两个,加起来不到一页纸,但错误率极高——不是因为难,而是因为 C 语言在几个地方故意不报错:if (a = 0) 语法完全合法,0 < x < 10 也合法,case 漏写 break 更是静默地继续往下执行。编译器的沉默让这些错误拖到运行时才暴露。本章的目标是把这些"合法但错误"的写法一次性识别出来,并建立一条判断链:先看条件怎么写对,再看分支怎么选型,最后看边界怎么处理(尤其是浮点比较与除零)。
一、关系运算、逻辑运算与优先级
关系运算符有六个:< <= > >= == !=。结果是 int,真为 1、假为 0(C99 起可用 <stdbool.h> 的 bool,底层仍是整数)。
三条必须记牢的规则:
==与=完全不同。if (a = 0)是赋值,表达式的值是 0,条件恒假,而且顺手把 a 改成了 0。防错写法是把常量写在左边:if (0 == a),一旦手滑写成if (0 = a)编译器会立刻报错。- 浮点数不能用
==比较。第 2 章讲过,0.1 无法精确表示,0.1 + 0.2 == 0.3是假。判断相等要用fabs(a - b) < 1e-6,判断"是否为 0"要用fabs(a) < 1e-12。 - 数学里的连写不等式不能照搬。
0 < x < 10在 C 里等价于(0 < x) < 10,前半段算出 0 或 1,永远小于 10,条件恒真。正确写法是x > 0 && x < 10。
逻辑运算符三个:&&(与)、||(或)、!(非)。C 的 && 与 || 采用短路求值:a && b 中 a 为假就不再算 b,a || b 中 a 为真就不再算 b。这不是优化,而是可以依赖的语义,最常见的用法是防御性判断:
if (p != NULL && p->value > 0) /* p 为空时不会解引用 */
优先级从高到低可以记成一条链:
!→ 算术运算 → 关系运算(< <= > >=高于== !=)→&&→||→ 赋值=
所以 !a > b 是 (!a) > b,a > b && c > d 是 (a > b) && (c > d),a || b && c 是 a || (b && c)——&& 的优先级高于 ||,这一点最常被记反。记不住就加括号,括号永远不丢人。
二、单分支与双分支
if (x > 0) {
y = 1;
}
if 后面的表达式可以是任意类型,非 0 即真,0 即假。语句只有一条时可以省略花括号,但建议永远写上——省掉之后再加一行,就会掉进"只有第一句受 if 控制"的陷阱。
双分支用 else,三分支以上用 else if 链:
if (x > 0) y = 1;
else if (x == 0) y = 0;
else y = -1;
这里有两个语义细节。第一,else if 不是新语法,它是 else 后面接一条 if 语句,所以整条链本质上是嵌套。第二,else 与最近的未配对 if 结合(悬空 else 问题):
if (a > 0)
if (b > 0) printf("A");
else
printf("B"); /* 这个 else 属于内层 if,不是外层 */
缩进骗人,规则不骗人。判断归属只看"就近配对",要改变归属就必须加花括号。凡是有嵌套,一律加括号,这条纪律能消灭此类 bug。
顺带记一个紧凑写法:条件运算符 ? : 是 C 语言里唯一的三目运算符,max = (a > b) ? a : b; 等价于一个 if-else 赋值。它的优先级低于关系运算、高于赋值,也可以嵌套,但嵌套超过两层可读性就崩了,应改回 if-else。
三、多路分支:switch
switch (grade) {
case 'A': printf("优秀\n"); break;
case 'B': printf("良好\n"); break;
case 'C': printf("及格\n"); break;
default: printf("不及格\n");
}
四条语法约束:
- 表达式必须是整型或字符型(char 也算整型)。float、double、字符串都不行。
- case 后面必须是常量表达式,不能是变量、不能是运行时才确定的值,且同一 switch 内不能重复。
- break 用来跳出 switch。漏写会导致"贯穿"(fall through):执行完本 case 继续执行下一个 case 的语句,直到遇到 break 或 switch 结束。
- default 位置任意,只在所有 case 都不匹配时执行,通常写在最后。
贯穿有时是故意的,用来让多个值共享同一段处理:
case 'a': case 'e': case 'i': case 'o': case 'u':
printf("元音\n"); break;
所以不能简单地说"漏 break 一定是 bug"——要判断的是:这段代码是否打算贯穿。打算贯穿时建议加一行注释说明,否则读代码的人会以为是漏写。
if 与 switch 怎么选:
| 场景 | 选 if | 选 switch |
|---|---|---|
| 判断范围(x > 60) | 是 | 不行 |
| 判断浮点或字符串 | 是 | 不行 |
| 判断离散的整型/字符值 | 可以 | 更清晰 |
| 分支数超过 4~5 个的等值判断 | 可读性下降 | 更清晰 |
| 需要多值共享同一段代码 | 要写多个 || |
用贯穿,自然 |
| 条件是复合逻辑(与、或、非) | 是 | 不行 |
一句话判据:等值、离散、整型,用 switch;范围、浮点、复合条件,用 if。
四、一元二次方程求根
double a, b, c, d, x1, x2;
scanf("%lf%lf%lf", &a, &b, &c);
if (fabs(a) < 1e-6) {
printf("a 为 0,不是二次方程\n");
} else {
d = b * b - 4 * a * c;
if (d > 1e-6) {
x1 = (-b + sqrt(d)) / (2 * a);
x2 = (-b - sqrt(d)) / (2 * a);
printf("两个实根:%.4f, %.4f\n", x1, x2);
} else if (fabs(d) <= 1e-6) {
printf("两个相等实根:%.4f\n", -b / (2 * a));
} else {
double re = -b / (2 * a), im = sqrt(-d) / (2 * a);
printf("共轭复根:%.4f±%.4fi\n", re, im);
}
}
三个要害:a 是否为 0 用容差判断,不能写 a == 0(用户输入 0.0000001 时理论上有解,但数值上已经是病态);判别式同样用容差,d == 0 几乎永远不成立;复根时 sqrt 的参数取 −d,因为 d 已经是负数,直接 sqrt(d) 会得到 nan。
五、简单计算器与除零处理
double a, b, r; char op;
scanf("%lf %c %lf", &a, &op, &b);
switch (op) {
case '+': r = a + b; break;
case '-': r = a - b; break;
case '*': r = a * b; break;
case '/':
if (fabs(b) < 1e-12) { printf("除数不能为 0\n"); return 1; }
r = a / b; break;
default: printf("非法运算符\n"); return 1;
}
printf("%.4f\n", r);
除零必须显式判断,原因是两类除零的行为不同:整数除以 0 是未定义行为,多数平台直接触发异常终止程序;浮点除以 0 不报错,会得到 inf 或 nan 并继续算下去,错误被悄悄带到后面的结果里。后者更危险,因为它不崩。所以除法之前一律先判断除数。
另外,如果想让 5/2 得 2.5,参与运算的数至少有一个要是 double;全是 int 时 5/2 得 2,这一点在第 4 章温度换算里已经踩过一次。
易错点
if (a = 0)把判断写成赋值。习惯把常量写左边。0 < x < 10恒为真。必须拆成两个条件用&&连接。- 悬空 else。嵌套 if 一律加花括号。
- switch 漏 break 导致意外贯穿,或以为贯穿一定是错误。
- switch 的表达式用了 float 或字符串,编译报错。
- 用
==比较浮点数,或用==判断浮点是否为 0。 - 除法不判断除数为 0,整数除零直接崩溃,浮点除零得到 inf。
- 忘记
&&的优先级低于关系运算,写出a > 0 && b < 0 || c == 0却按另一种方式理解。
这一章在整门课里的位置
本章把第 3 章流程图里的菱形判断框翻译成代码,并直接用上第 2 章的类型知识(char 属于整型,所以能做 switch 的表达式)和第 4 章的输入输出(判断的前提是数据已经正确读入)。它向下为第 6 章铺路:循环的终止条件本质上就是一个关系表达式,循环里嵌 if 更是常态;本章练熟的"条件怎么写对",在第 6 章会以"循环边界怎么写对"的形式再考一次。
相关课程
这篇笔记涉及的主题,我录过对应的视频课程:
- C 语言程序设计 15 讲