控制流:if/else/switch
站在一个十字路口,红绿灯控制着车流方向。绿灯直行,红灯停下,左转箭头亮起时走左转道。信号灯不关心车里坐的是谁,只看当前状态决定放行。
if (rain) {
bring_umbrella();
} else {
wear_sunglasses();
}
我意识到,原来我可以让程序自己做决定。控制流就是程序的"大脑"——它负责选择走哪条路、什么时候重复、什么时候停下来。
开篇故事
站在一个十字路口,红绿灯控制着车流方向。绿灯时直行,红灯时停下,左转箭头亮起时走左转道。交通信号灯不关心你车里坐的是谁,它只根据当前状态决定放行路线。每一辆车都在做同样的事情:看灯,选路。
程序的执行也是如此。默认情况下,代码从上到下依次执行,像直行通过一个十字路口。但现实世界充满了分支——下雨就带伞,天晴就戴太阳镜。C 语言的 if、else、switch 就是程序的红绿灯。它们根据条件的真假,让执行路径走向不同的分支。没有控制流,程序只是机械地念台词;有了控制流,程序才开始做选择。
控制流就是程序的"大脑"——它负责选择走哪条路、什么时候重复、什么时候停下来。
人生最重要的不是站在原地,而是知道在岔路口往哪走。——改编自 罗杰·贝肯
本章适合谁
- 已经掌握变量、数组和基本输入输出
- 想让程序根据条件做出不同响应
- 想理解 "missing break" 和 "dangling else" 为什么会导致难以追踪的 bug
你会学到什么
if / else if / else分支结构switch / case / default+break的重要性- 三元运算符
?:的简洁写法 - 嵌套条件和悬挂 else 问题
- 常见陷阱:忘记 break、条件赋值与比较混淆
前置要求
第一个例子
#include <stdio.h>
#include <stdint.h>
int main(void) {
int32_t score = 85;
if (score >= 90) {
printf("优秀 (A)\n");
} else if (score >= 80) {
printf("良好 (B)\n");
} else if (score >= 60) {
printf("及格 (C)\n");
} else {
printf("不及格 (F)\n");
}
return 0;
}
输出:
良好 (B)
分步解析
if (score >= 90)—— 先检查最高档else if (score >= 80)—— 90 不满足,继续往下score >= 80为真,打印 "良好 (B)",然后跳过剩余分支
原理解析
if / else / else if 结构
if (条件1) {
// 条件1 为真 → 执行这里
} else if (条件2) {
// 条件1 为假, 条件2 为真 → 执行这里
} else {
// 所有条件都不为真 → 执行这里
}
执行流程像瀑布——从上往下逐层判断,一旦命中就跳过其余。
switch / case
当需要根据一个整数的多个可能值选择分支时,switch 更清晰:
int32_t day = 3;
switch (day) {
case 1: printf("星期一\n"); break;
case 2: printf("星期二\n"); break;
case 3: printf("星期三\n"); break;
default: printf("无效编号!\n"); break;
}
三元运算符 ?:
int32_t age = 20;
const char *status = (age >= 18) ? "成年人" : "未成年人";
等价于:
const char *status;
if (age >= 18) {
status = "成年人";
} else {
status = "未成年人";
}
三元运算符适合简单的二选一赋值,但别嵌套——一旦嵌套三层以上,代码就变成天书了。
常见错误
❌ 错误 1: switch 中忘记 break
int32_t choice = 1;
// ❌ 没有 break → fall-through (穿透)
switch (choice) {
case 1:
printf("选项 A\n");
case 2:
printf("选项 B\n");
default:
printf("默认选项\n");
}
/*
输出:
选项 A
选项 B
默认选项
*/
修复:
// ✅ 每个 case 末尾加 break
switch (choice) {
case 1:
printf("选项 A\n");
break;
case 2:
printf("选项 B\n");
break;
default:
printf("默认选项\n");
break;
}
例外:有时故意穿透是合理的(如多月共享同一天数的逻辑),但要加注释。
❌ 错误 2: 条件中使用 = 而非 ==
int32_t x = 5;
// ❌ 这是赋值!不是比较。x 被赋值为 0,条件为假
if (x = 0) {
printf("x 是零。\n");
}
编译器(带 -Wall)会警告,但如果忽略警告就会发现 bug。我的习惯:
// ✅ Yoda condition —— 如果写成 = 会编译报错
if (0 == x) {
printf("x 是零。\n");
}
或者养成用 == 的习惯即可。
❌ 错误 3: 悬挂 else (Dangling Else)
int32_t a = 5, b = 10;
// ❌ else 属于哪个 if?C 的规则是"就近匹配"
if (a > 3)
if (b > 20)
printf("b > 20\n");
else
printf("这个 else 属于内层 if (b > 20)!\n");
修复——永远用花括号:
// ✅ 明确匹配关系
if (a > 3) {
if (b > 20) {
printf("b > 20\n");
}
} else {
printf("这个 else 属于外层 if (a > 3)。\n");
}
动手练习
🟢 入门: 判断奇偶数
写一个程序,根据输入的整数判断是奇数还是偶数。
#include <stdio.h>
#include <stdint.h>
int main(void) {
int32_t n = 7;
if (n % 2 == 0) {
printf("%d 是偶数。\n", n);
} else {
printf("%d 是奇数。\n", n);
}
return 0;
}
输出: 7 是奇数。
🟡 中级: 简易计算器
用 switch 实现一个简易计算器,支持 + - * / 四则运算。
#include <stdio.h>
#include <stdint.h>
int main(void) {
double a = 10.0, b = 3.0;
char op = '/';
switch (op) {
case '+':
printf("%.2f + %.2f = %.2f\n", a, b, a + b);
break;
case '-':
printf("%.2f - %.2f = %.2f\n", a, b, a - b);
break;
case '*':
printf("%.2f * %.2f = %.2f\n", a, b, a * b);
break;
case '/':
if (b != 0.0) {
printf("%.2f / %.2f = %.2f\n", a, b, a / b);
} else {
printf("错误: 除以零!\n");
}
break;
default:
printf("未知运算符: %c\n", op);
break;
}
return 0;
}
输出: 10.00 / 3.00 = 3.33
🔴 挑战: 闰年判断
判断给定年份是否为闰年。规则:能被 4 整除但不能被 100 整除;或者能被 400 整除。
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
bool is_leap_year(int32_t year) {
if (year % 400 == 0) {
return true;
}
if (year % 100 == 0) {
return false;
}
if (year % 4 == 0) {
return true;
}
return false;
}
int main(void) {
int32_t years[] = {2024, 1900, 2000, 2023};
int32_t n = (int32_t)(sizeof(years) / sizeof(years[0]));
for (int32_t i = 0; i < n; i++) {
const char *label = is_leap_year(years[i]) ? "闰年" : "平年";
printf("%d: %s\n", years[i], label);
}
return 0;
}
输出:
2024: 闰年
1900: 平年
2000: 闰年
2023: 平年
故障排查 (FAQ)
Q: switch 的 case 后面能用变量吗?
A: 不能。case 后面必须是编译时常量 (compile-time constant)。
int32_t x = 5;
switch (val) {
case x: // ❌ 编译错误
case 5: // ✅ 常量,可以
case 2 + 3: // ✅ 编译器能算出的常量,也可以
}
如果需要运行时比较,用 if / else if。
Q: switch 能用在字符串比较上吗?
A: 不能。C 语言的 switch 只支持整数类型 (int、char、enum)。字符串比较需要用 <string.h> 的 strcmp 配合 if / else if:
if (strcmp(name, "Alice") == 0) {
// ...
} else if (strcmp(name, "Bob") == 0) {
// ...
}
Q: if 条件里写 `if (flag = true)` 和 `if (flag == true)` 一样吗?
A: 不一样!这恰恰是"错误 2"的变体。
flag == true:比较,返回 boolflag = true:赋值,整个表达式的值就是true——条件永远成立
所以这行代码等价于"无条件执行",通常不是你想要的。
知识扩展 (选学)
Duff's Device —— switch 的极限用法
void send(int *to, int *from, int count) {
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while (--n > 0);
}
}
这是合法的 C 代码,利用了 switch 的 fall-through 特性做循环展开 (loop unrolling) 优化。日常几乎不需要,但知道后你会重新认识 switch 的灵活性。
守卫子句 (Guard Clause)
在函数开头尽早 return,避免深层嵌套:
// ❌ 深层嵌套
void process(int *data, int len) {
if (data != NULL) {
if (len > 0) {
// 真正逻辑缩进很深
}
}
}
// ✅ 守卫子句
void process(int *data, int len) {
if (data == NULL) return;
if (len <= 0) return;
// 真正逻辑在这里,少了一层缩进
}
小结
通过这一章我发现:
if / else if / else是条件判断的基础结构——命中就执行,其余跳过switch / case适合整数多分支场景,但别忘了break- 三元运算符
?:适合简单的二选一赋值,别嵌套使用 - 悬挂 else:C 的 else 就近匹配内层 if——用花括号消除歧义
=和==是常见的混淆源,编译器警告务必重视
术语表
| 术语 | 英文 | 解释 |
|---|---|---|
| 控制流 | Control Flow | 程序执行的路径和顺序 |
| 分支 | Branch | 根据条件选择不同执行路径 |
| 穿透 | Fall-through | switch 中缺少 break 导致执行进入下一个 case |
| 悬挂 else | Dangling else | else 与哪个 if 匹配的歧义 |
| 三元运算符 | Ternary operator | ?:,唯一的三元运算符 |
| 守卫子句 | Guard clause | 在函数开头提前 return 减少嵌套 |
| 编译时常量 | Compile-time constant | 编译时就能确定值的表达式 |
| 循环展开 | Loop unrolling | 将循环体展开多次以减少循环开销的优化技术 |