C 语言第 5 章:选择结构——分支语句与运算符优先级

写于 2026-11-01

程序从"能算"变成"会判断",就是从这一章开始的。选择结构的语法只有 if 和 switch 两个,加起来不到一页纸,但错误率极高——不是因为难,而是因为 C 语言在几个地方故意不报错:if (a = 0) 语法完全合法,0 < x < 10 也合法,case 漏写 break 更是静默地继续往下执行。编译器的沉默让这些错误拖到运行时才暴露。本章的目标是把这些"合法但错误"的写法一次性识别出来,并建立一条判断链:先看条件怎么写对,再看分支怎么选型,最后看边界怎么处理(尤其是浮点比较与除零)。

一、关系运算、逻辑运算与优先级

关系运算符有六个:< <= > >= == !=。结果是 int,真为 1、假为 0(C99 起可用 <stdbool.h> 的 bool,底层仍是整数)。

三条必须记牢的规则:

  1. == 与 = 完全不同。if (a = 0) 是赋值,表达式的值是 0,条件恒假,而且顺手把 a 改成了 0。防错写法是把常量写在左边:if (0 == a),一旦手滑写成 if (0 = a) 编译器会立刻报错。
  2. 浮点数不能用 == 比较。第 2 章讲过,0.1 无法精确表示,0.1 + 0.2 == 0.3 是假。判断相等要用 fabs(a - b) < 1e-6,判断"是否为 0"要用 fabs(a) < 1e-12。
  3. 数学里的连写不等式不能照搬。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 章温度换算里已经踩过一次。

易错点

  1. if (a = 0) 把判断写成赋值。习惯把常量写左边。
  2. 0 < x < 10 恒为真。必须拆成两个条件用 && 连接。
  3. 悬空 else。嵌套 if 一律加花括号。
  4. switch 漏 break 导致意外贯穿,或以为贯穿一定是错误。
  5. switch 的表达式用了 float 或字符串,编译报错。
  6. 用 == 比较浮点数,或用 == 判断浮点是否为 0。
  7. 除法不判断除数为 0,整数除零直接崩溃,浮点除零得到 inf。
  8. 忘记 && 的优先级低于关系运算,写出 a > 0 && b < 0 || c == 0 却按另一种方式理解。

这一章在整门课里的位置

本章把第 3 章流程图里的菱形判断框翻译成代码,并直接用上第 2 章的类型知识(char 属于整型,所以能做 switch 的表达式)和第 4 章的输入输出(判断的前提是数据已经正确读入)。它向下为第 6 章铺路:循环的终止条件本质上就是一个关系表达式,循环里嵌 if 更是常态;本章练熟的"条件怎么写对",在第 6 章会以"循环边界怎么写对"的形式再考一次。

本文是《C 语言程序设计》第 5 章的知识点摘要。完整板书与代码见 课程页;上一章 顺序结构,下一章 循环结构。

相关课程

这篇笔记涉及的主题,我录过对应的视频课程: