Lesson 13: 车辆限行 CNB
练习任务
本课包含三个递进练习:
练习 1(get_last_char):实现函数 char get_last_char(char str[]),遍历字符串直到 '\0' 终止符,返回最后一个有效字符。输入 "A23456" → 输出 6,输入 "hello" → 输出 o。
练习 2(is_restricted):实现函数 int is_restricted(int tail_num, enum day today),用 switch-case 判断车牌尾号是否在指定星期限行。尾号 0/5 周一限、1/6 周二限、...、4/9 周五限。输入 2 6 → restricted,输入 3 9 → free。
练习 3(完整程序):实现 get_week_day() 从年月日推算星期,组合 get_last_char + is_restricted + get_week_day 完成完整限行判断。输入 A23456 + 2013 1 1 → restricted!。
提示:
switch-case的case分支如果不写break会「穿透」到下一个case——这看似是陷阱,实则是特性(允许多个case共享同一段代码)。日期推算星期的核心是:计算从已知参考日到目标日期的总天数差,对 7 取模。
核心知识点
enum枚举类型 — 为整数常量命名,第一个值默认为 0,后续自动递增switch-case多路分支 — 基于整数表达式的跳转表,比多层if-else更清晰case的穿透(fall-through) — 不写break时继续执行下一个case,可实现多值共享break跳出switch— 阻止穿透,强制结束整个switch块default分支 — 处理所有未匹配的情况,相当于if-else链的最终elseswitchvsif-else链 —switch适合整数值的离散多路分支,if-else适合范围判断- 三元表达式
?:回顾 — 在switch的case分支内简洁地做二选一赋值 - 字符串遍历取末字符 —
while (str[i])走到'\0',记录最后一个有效字符 - 日期推算星期 — 累计天数差 + 对 7 取模 + 闰年修正
- 函数组合调用 —
is_restricted(get_last_char(s), get_week_day(y, m, d))嵌套组合
代码框架
练习 1:取末字符
#include <stdio.h>
char get_last_char(char str[])
{
char c = 0; // 记录当前字符
int i = 0;
// while (str[i] != '\0') 遍历到字符串末尾
// 每次记录 c = str[i],然后 i++
return c; // 循环结束时 c 就是最后一个字符
}
int main(void)
{
char buf[64];
scanf("%s", buf);
printf("%c\n", get_last_char(buf));
return 0;
}练习 2:限行判断
#include <stdio.h>
enum day
{
MONDAY = 1,
TUESDAY, // 自动 = 2
WEDNESDAY, // 自动 = 3
THURSDAY, // 自动 = 4
FRIDAY, // 自动 = 5
SATURDAY, // 自动 = 6
SUNDAY // 自动 = 7
};
int is_restricted(int tail_num, enum day today)
{
int ret = 0;
switch (tail_num)
{
case 0: case 5:
ret = (today == MONDAY) ? 1 : 0; break;
case 1: case 6:
ret = (today == TUESDAY) ? 1 : 0; break;
case 2: case 7:
ret = (today == WEDNESDAY) ? 1 : 0; break;
case 3: case 8:
ret = (today == THURSDAY) ? 1 : 0; break;
case 4: case 9:
ret = (today == FRIDAY) ? 1 : 0; break;
default:
ret = 0; break;
}
return ret;
}
int main(void)
{
int weekday, tail_num;
scanf("%d %d", &weekday, &tail_num);
if (is_restricted(tail_num, (enum day)weekday))
printf("restricted\n");
else
printf("free\n");
return 0;
}填充框架的关键思考:enum 中只给 MONDAY = 1,后面的 TUESDAY ~ SUNDAY 分别取值多少?case 0: case 5: 为什么不写 break 也能共享同一段代码?default 在什么时候被触发?
TIP
先尝试自己实现三个函数。switch-case 的关键点在于理解 break 的「阻止穿透」作用——试着把 case 0 后面的 break 去掉,观察输出会有什么变化。
深度讲解
1. enum 枚举:给整型常量起有意义的名字
1.1 问题:魔数满天飞
if (today == 1) /* 周一限 0 和 5 */ // 1 是什么? // [!code warning]
return (tail == 0 || tail == 5);
if (today == 2) /* 周二限 1 和 6 */ // 2 是什么?
return (tail == 1 || tail == 6);代码中充斥着 1、2、5 这样的魔数——阅读者必须凭注释或记忆才能理解含义。这正是 enum 要解决的问题。
1.2 enum 的定义与自增规则
enum day
{
MONDAY = 1, // 显式赋值: 1
TUESDAY, // 自动 = 2 (前一个 + 1)
WEDNESDAY, // 自动 = 3 (前一个 + 1)
THURSDAY, // 自动 = 4
FRIDAY, // 自动 = 5
SATURDAY, // 自动 = 6
SUNDAY // 自动 = 7
};enum 的规则:
- 第一个枚举值如果没有显式赋值,默认为
0 - 每个后续未赋值的枚举值 = 前一个值 + 1
- 可以给任意位置显式赋值(如
SPECIAL = 100),后续仍按 +1 递增
#include <stdio.h>
enum example {
A, // 0 (默认从 0 开始)
B, // 1
C = 10, // 10 (显式赋值)
D, // 11 (自动从 10 继续)
E = 3, // 3 (可以比前一个值小!)
F // 4 (从 3 继续)
};
int main(void)
{
printf("A=%d B=%d C=%d D=%d E=%d F=%d\n", A, B, C, D, E, F);
// 输出: A=0 B=1 C=10 D=11 E=3 F=4
return 0;
}1.3 enum 的本质:编译期整型常量
enum day today = MONDAY; // 等价于 int today = 1;
printf("%d\n", today); // 输出 1
if (today == MONDAY) { ... } // 等价于 if (today == 1),但可读性天差地别enum 本质上是一组有名字的 int 编译期常量——不占用运行时内存(可执行文件中存的是数值),只是为数字赋予了人类可读的名字。编译器在编译阶段就完成了 MONDAY → 1 的替换。
1.4 enum 与 #define 的对比
| 方面 | enum day { MONDAY=1, ... }; | #define MONDAY 1 #define TUESDAY 2 |
|---|---|---|
| 分组 | 所有值在同一个类型名下 | 各自独立,无逻辑关联 |
| 调试 | gdb 中可以看到符号名 | 预处理后消失,只能看到数字 |
| 作用域 | 有作用域(可放在函数/结构体内) | 全局宏替换 |
| 类型检查 | 部分编译器支持 enum 类型警告 | 无类型 |
| 推荐场景 | 一组密切相关的常量 | 独立的、编译期配置用的常量 |
2. switch-case:多路分支的优雅语法
2.1 基本语法与执行流程
switch (表达式) // 必须是整型(int/char/enum),不能是 float 或字符串
{
case 值1: // 如果 表达式 == 值1
语句A;
break; // 跳出整个 switch
case 值2: // 如果 表达式 == 值2
语句B;
break;
default: // 所有 case 都没有匹配时执行
语句C;
break;
}执行流程:
- 计算
switch后面括号中的表达式 - 依次与每个
case的值比较(编译器通常优化为跳转表) - 匹配成功 → 从该
case开始执行,直到遇到break或switch结束 - 所有
case都不匹配 → 执行default(如果有的话)
2.2 穿透(Fall-through):特性而非 Bug
#include <stdio.h>
int main(void)
{
int n = 1;
switch (n)
{
case 1:
printf("A "); // 执行这句
// 没有 break!继续执行下一个 case!
case 2:
printf("B "); // 也被执行(穿透)
break;
case 3:
printf("C ");
break;
}
// 输出: A B
return 0;
}这种「穿透」行为是 switch 刻意设计的特性——允许多个 case 值共享同一段代码:
switch (tail_num)
{
case 0: // 尾号 0
case 5: // 尾号 5(穿透到这里,共享下面的代码)
ret = (today == MONDAY) ? 1 : 0;
break; // 现在才跳出
case 1:
case 6: // 尾号 1 和 6 共享
ret = (today == TUESDAY) ? 1 : 0;
break;
// ...
}等价于不使用穿透的写法:
if (tail_num == 0 || tail_num == 5)
ret = (today == MONDAY) ? 1 : 0;
else if (tail_num == 1 || tail_num == 6)
ret = (today == TUESDAY) ? 1 : 0;显然后者更冗长,且在 tail_num 被重复写了多次。
2.3 break 的作用与常见遗忘
// 错误: 忘了写 break
case 0:
process_zero();
// 忘了 break → 会继续执行 case 1 的代码!
case 1:
process_one();
break;WARNING
忘记 break 是 switch 中最常见的 bug。养成习惯:写完每个 case 的代码后立即写 break,再回去填充 case 体内容。
2.4 default 分支
switch (tail_num)
{
case 0: case 5: /* 周一 */ break;
case 1: case 6: /* 周二 */ break;
case 2: case 7: /* 周三 */ break;
case 3: case 8: /* 周四 */ break;
case 4: case 9: /* 周五 */ break;
default: // tail_num 不在 0~9 范围内
printf("Invalid tail number!\n");
ret = 0;
break;
}default 不一定要放在最后(但强烈建议放在最后,方便阅读)。它是保护网——捕捉所有意料之外的情况。
2.5 switch 的底层实现:跳转表
; 编译器将 switch 优化为跳转表(jump table)
; 比逐个比较 if-else 更高效
mov eax, [tail_num] ; 取 switch 表达式的值
cmp eax, 9 ; 检查是否在 0~9 范围
ja .L_default ; 超出范围 → 跳 default
jmp [.L_jump_table + eax*4] ; 查表跳转!
.L_jump_table:
.long .L_case0 ; tail_num == 0
.long .L_case1 ; tail_num == 1
...switch 对密集的整数值(如 0~9)会被优化为 O(1) 的跳转表——计算目标地址只需一次查表操作。而等效的 if-else 链在最坏情况下需要 O(n) 次比较。
3. switch-case vs if-else 链:选择的标准
3.1 适用场景对比
| 场景 | 推荐 | 理由 |
|---|---|---|
| 单个整型变量的离散值比较 | switch | 语法紧凑,跳转表优化 |
范围判断(x>0 && x<10) | if-else | case 不支持范围 |
| 浮点数比较 | if-else | switch 仅支持整型 |
| 字符串比较 | if-else | 同上 |
| 多变量的复合条件 | if-else | switch 只能判断一个表达式 |
| 少量分支(2~3 个) | switch 或 if-else | 两者可读性接近 |
3.2 等价的 if-else 写法
// 用 if-else 链实现 is_restricted 的等价版本
int is_restricted_if(int tail_num, int today)
{
if (tail_num == 0 || tail_num == 5)
return (today == MONDAY) ? 1 : 0;
else if (tail_num == 1 || tail_num == 6)
return (today == TUESDAY) ? 1 : 0;
else if (tail_num == 2 || tail_num == 7)
return (today == WEDNESDAY) ? 1 : 0;
else if (tail_num == 3 || tail_num == 8)
return (today == THURSDAY) ? 1 : 0;
else if (tail_num == 4 || tail_num == 9)
return (today == FRIDAY) ? 1 : 0;
else
return 0;
}TIP
当每个 case 都以 return 结束时,break 可以省略——return 已经退出了整个函数,break 成为死代码。这在实际代码中是一种常见且干净的写法。
4. 三元表达式回顾:switch 内的精致二选一
4.1 从 Lesson 04 复习三元表达式
// if-else 版本(4 行)
int result;
if (today == MONDAY)
result = 1;
else
result = 0;
// 三元表达式版本(1 行)
int result = (today == MONDAY) ? 1 : 0;
// └───条件───┘ └真┘ └假┘4.2 在 switch-case 中的使用
case 0: case 5:
ret = (today == MONDAY) ? 1 : 0; // 三元让逻辑一行搞定
break;为什么在这里用三元:每个 case 只做一个简单判断(今天是否等于限行日),用 if-else 写会增加嵌套层次。三元表达式在一行内完成了「判断 → 赋值」的语义。
NOTE
三元表达式只适用于简单的二选一赋值。如果需要在 case 中执行多条语句,还是应该用 if-else + { }。
5. 字符串遍历取末字符
5.1 核心算法
char get_last_char(char str[])
{
char c = 0;
int i = 0;
while (str[i] != '\0') // 等价于 while (str[i])
{
c = str[i]; // 记录当前的字符
i++; // 前进
}
// 循环结束时,i 指向 '\0',c 存的是 '\0' 前面那个字符
return c;
}追踪执行(对 "AB6"):
初始: i=0, c=0
第1轮: str[0]='A' → c='A', i=1
第2轮: str[1]='B' → c='B', i=2
第3轮: str[2]='6' → c='6', i=3
第4轮: str[3]='\0' → while 条件为假,退出
返回: c='6' ✓5.2 等价写法对比
// 写法 1: strlen 法(简洁但需要 <string.h>)
char get_last_char(char str[])
{
int len = strlen(str);
return (len > 0) ? str[len - 1] : 0;
}
// 写法 2: 遍历法(本课使用,不依赖库函数)
char get_last_char(char str[])
{
char c = 0; int i = 0;
while (str[i]) { c = str[i]; i++; }
return c;
}
// 写法 3: 指针法(最简洁)
char get_last_char(char *str)
{
while (*str) str++;
return *(str - 1);
}方法 1 简洁但依赖 strlen 会再次遍历整个字符串(两次遍历)。方法 2 和方法 3 各只遍历一次。方法 2 更直观适合入门,方法 3 是指针进阶写法。
6. 日期计算:从年月日推算星期
6.1 问题建模
给定日期(如 2013-01-01),求它是星期几。核心思路:计算从已知参考日到目标日的总天数差,对 7 取模。
已知: 2013-01-01 是星期二 (可以硬编码)
目标: year-month-day
步骤:
1. 计算从 2013-01-01 到 year-01-01 的整年天数
2. 加上 year 中前 month-1 个月的天数
3. 加上 day - 1(当月已过天数)
4. 总天数 = (整年天数 + 整月天数 + 日差)
5. 星期 = (总天数 + 2) % 7 // +2 是因为基准日是星期二6.2 实现细节:闰年与每月天数
int is_leap(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
enum day get_week_day(int year, int month, int day)
{
// 每月天数(非闰年默认,二月之后修正)
int month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (is_leap(year))
month_days[1] = 29; // 闰年二月 29 天
int total_days = 0;
// 1. 累加整年天数
for (int y = 2013; y < year; y++)
total_days += is_leap(y) ? 366 : 365;
// 2. 累加 year 年之前月份的天数
for (int m = 0; m < month - 1; m++)
total_days += month_days[m];
// 3. 加上当月已过天数
total_days += day - 1;
// 4. 基准日 2013-01-01 是星期二(+2),对 7 取模得星期索引
int weekday = (total_days + 2) % 7;
switch (weekday)
{
case 0: return SUNDAY;
case 1: return MONDAY;
case 2: return TUESDAY;
case 3: return WEDNESDAY;
case 4: return THURSDAY;
case 5: return FRIDAY;
case 6: return SATURDAY;
default: return SUNDAY; // 不应该到达这里
}
}6.3 取模 7 的本质
对 7 取模的目的:将「总天数」映射到「星期几」的循环中(0→周日, 1→周一, ... , 6→周六)。
total_days = 0 → weekday = (0+2)%7 = 2 → TUESDAY ✓ (2013-01-01)
total_days = 1 → weekday = (1+2)%7 = 3 → WEDNESDAY ✓
total_days = 6 → weekday = (6+2)%7 = 1 → MONDAY ✓
total_days = 7 → weekday = (7+2)%7 = 2 → TUESDAY ✓ (一周后回到周二)7. switch 的高级应用
7.1 状态机编程
switch 是 C 语言中实现有限状态机(Finite State Machine)最自然的方式:
enum state { IDLE, READING, WRITING, ERROR };
void handle_event(enum state *s, char event)
{
switch (*s)
{
case IDLE:
if (event == 'r') *s = READING;
if (event == 'w') *s = WRITING;
break;
case READING:
if (event == 'd') *s = IDLE; // done
if (event == 'e') *s = ERROR;
break;
case WRITING:
if (event == 'd') *s = IDLE;
if (event == 'e') *s = ERROR;
break;
case ERROR:
if (event == 'r') *s = IDLE; // reset
break;
}
}外层 switch 根据当前状态选择处理分支,每个分支内用 if 处理具体事件——这是嵌入式系统和协议解析中的标准范式。
7.2 printf 中的 switch
C 标准库的 printf 实现中,switch-case 是核心控制结构——根据 % 后面的格式符选择不同的类型转换和格式化逻辑:
// printf 内部对格式符的处理(简化示例)
switch (format_char)
{
case 'd': /* 输出 int */ put_int(args); break;
case 'c': /* 输出 char */ put_char(args); break;
case 's': /* 输出字符串 */ put_str(args); break;
case 'f': /* 输出 float */ put_float(args); break;
case 'x': /* 输出十六进制 */ put_hex(args); break;
case '%': /* 输出 % 自身 */ putchar('%'); break;
default: /* 未识别的格式 */ putchar('%');
putchar(format_char);
break;
}这正是在 Lesson 18(实现 printf)中会用到的核心结构。
7.3 Duff's Device:switch + 循环的经典组合
// Duff's Device: 利用 switch fall-through 实现循环展开
// 这是 C 语言中最著名的「奇怪但合法」的代码片段
void send(char *to, char *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);
}
}这是 Tom Duff 在 1983 年发明的「循环展开」技术——将 switch 和 do-while 交织在一起,利用 case 穿透实现不完整的第一次循环。这是合法 C 代码,但在此仅为展示 switch 的灵活性,不推荐在生产中使用。
参考解答
练习1: get_last_char(遍历取末字符)
#include <stdio.h>
char get_last_char(char str[])
{
char c = 0;
int i = 0;
while (str[i] != '\0')
{
c = str[i];
i++;
}
return c;
}
int main(void)
{
char buf[64];
scanf("%s", buf);
printf("%c\n", get_last_char(buf));
return 0;
}用 while (str[i] != '\0') 遍历整个字符串,每次循环把当前字符存入 c。循环结束时 i 指向 '\0',c 中保存的恰好是最后一个有效字符。
练习2: is_restricted(switch-case + 三元)
#include <stdio.h>
enum day
{
MONDAY = 1, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
};
int is_restricted(int tail_num, enum day today)
{
int ret = 0;
switch (tail_num)
{
case 0: case 5:
ret = (today == MONDAY) ? 1 : 0; break;
case 1: case 6:
ret = (today == TUESDAY) ? 1 : 0; break;
case 2: case 7:
ret = (today == WEDNESDAY) ? 1 : 0; break;
case 3: case 8:
ret = (today == THURSDAY) ? 1 : 0; break;
case 4: case 9:
ret = (today == FRIDAY) ? 1 : 0; break;
default:
ret = 0; break;
}
return ret;
}
int main(void)
{
int weekday, tail_num;
scanf("%d %d", &weekday, &tail_num);
if (is_restricted(tail_num, (enum day)weekday))
printf("restricted\n");
else
printf("free\n");
return 0;
}case 0: case 5: 利用穿透让尾号 0 和 5 共享同一段逻辑。每个分支用三元表达式判断 today == 限行日。default 处理非法尾号。
练习3: 完整限行程序(含日期推算)
#include <stdio.h>
enum day
{
MONDAY = 1, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
};
char get_last_char(char str[])
{
char c = 0; int i = 0;
while (str[i]) { c = str[i]; i++; }
return c;
}
int is_leap(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
enum day get_week_day(int year, int month, int day)
{
int month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (is_leap(year)) month_days[1] = 29;
int total = 0;
for (int y = 2013; y < year; y++)
total += is_leap(y) ? 366 : 365;
for (int m = 0; m < month - 1; m++)
total += month_days[m];
total += day - 1;
int wd = (total + 2) % 7; // 2013-01-01 was Tuesday
switch (wd)
{
case 0: return SUNDAY;
case 1: return MONDAY;
case 2: return TUESDAY;
case 3: return WEDNESDAY;
case 4: return THURSDAY;
case 5: return FRIDAY;
case 6: return SATURDAY;
default: return SUNDAY;
}
}
int is_restricted(int tail_num, enum day today)
{
switch (tail_num)
{
case 0: case 5: return (today == MONDAY) ? 1 : 0;
case 1: case 6: return (today == TUESDAY) ? 1 : 0;
case 2: case 7: return (today == WEDNESDAY) ? 1 : 0;
case 3: case 8: return (today == THURSDAY) ? 1 : 0;
case 4: case 9: return (today == FRIDAY) ? 1 : 0;
default: return 0;
}
}
int main(void)
{
char car_num[64];
int year, month, day;
scanf("%s %d %d %d", car_num, &year, &month, &day);
char last = get_last_char(car_num);
int tail = last - '0';
enum day today = get_week_day(year, month, day);
if (is_restricted(tail, today))
printf("restricted!\n");
else
printf("NOT restricted!\n");
return 0;
}三个函数各司其职:get_last_char 取车牌尾号 → get_week_day 算星期 → is_restricted 判限行。函数组合让 main 逻辑极简——只需 4 行核心逻辑。
对照检查:
enum day的第一个值赋了 1 吗?case 0: case 5:利用穿透了吗?每个分支都写了break吗(或用return替代)?get_week_day考虑了闰年吗?
课堂讨论
- 除了
switch-case,实现is_restricted还有其他写法吗?哪种更合适? - 如果考虑到每 3 个月轮换一次尾号限行制度(例如冬季和夏季的限行尾号不同),程序应该如何修改?
switch和if-else在什么场景下各有优势?编译器对两者的优化有什么不同?- 为什么
switch的条件表达式只能是整型(int/char/enum),而不能是字符串或浮点数? case 0: case 5:这种不写break的穿透写法,在工程上是推荐还是应该避免?
讨论答案
Q1: switch-case 还有其他实现方式吗?
有多种替代方案:
方案 A:if-else 链(见 §3.2 中的代码)——最直接但仍冗长。
方案 B:数组查表法(最简洁):
int is_restricted(int tail_num, enum day today)
{
// 映射: 尾号 → 对应的限行日
enum day restrict_map[] = {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY};
// 尾号: 0 1 2 3 4
// 5 6 7 8 9
return (today == restrict_map[tail_num]) ? 1 : 0;
}利用了尾号 0 和 5 对应同一个限行日(周一)的规律——把 0~9 映射到一个 10 元素的数组中。代码从 20+ 行缩短到 3 行,但需要额外理解映射关系。
选择标准:switch 最直观适合 5~10 个分支;数组查表法在映射有规律时最简洁;if-else 在分支少或条件复杂时更灵活。
Q2: 每 3 个月轮换限行制度如何修改?
需要将限行规则从「固定的尾号→星期」改为「动态的尾号→日期范围」:
// 用月份决定当前的限行方案
int get_scheme(int month)
{
// 每 3 个月换一组: 1-3月方案A, 4-6月方案B, ...
return (month - 1) / 3; // 0: 1-3月, 1: 4-6月, ...
}
int is_restricted_seasonal(int tail_num, int month, enum day today)
{
int scheme = get_scheme(month);
switch (scheme)
{
case 0: // 1-3月限行方案
// 尾号 0/5 周一限... (和现在一样)
break;
case 1: // 4-6月轮换限行方案
// 尾号 1/6 周一限... (整体错一位)
break;
// ...
}
}核心改动:将「固定的星期判断」变成「根据月份查不同的限行表」——底层仍然用 switch 或多维数组。
Q3: switch 和 if-else 在优化上的区别?
if-else 优化:编译器把 if-else 链编译为一系列的条件跳转指令(cmp + je/jne),最坏情况下需要 O(n) 次比较。
switch 优化(密集值时):编译器把 switch 编译为跳转表(jump table)——一次计算地址 + 一次无条件跳转,O(1):
; switch 的跳转表实现(伪代码)
jmp [jump_table + eax * 4] ; eax 是 switch 表达式的值
jump_table:
.long case0_addr ; tail_num == 0
.long case1_addr ; tail_num == 1
... ; 总检查 1 次 + 1 次跳转对于稀疏值,编译器可能回退到「二分查找 + 条件跳转」的混合方案。关键:switch 给了编译器更多优化空间。
Q4: 为什么 switch 只能是整型?
根本原因:switch 被设计为基于等值匹配的跳转表优化。跳转表需要将 case 值直接映射到代码地址——整数天然适合做数组索引。浮点数、字符串不满足这个特性:
- 浮点数:数值表示有精度误差,
3.14 == 3.14在浮点语义中并非总能成立 - 字符串:长度不固定,无法做 O(1) 的数组索引(除非先哈希但那是编译器做不了的)
如果确实需要「字符串版 switch」,通常用 if (strcmp(...)) 链或预先将所有字符串映射为整数枚举值后 switch。
Q5: case 穿透在工程中推荐还是避免?
有控制的穿透是推荐的——case 0: case 5: 这种「多值共享一个执行体」是完全可接受的标准写法。它利用了 switch 的设计特性来简化代码。
需要避免的是「无意的穿透」——忘了写 break:
case 0:
do_something();
// 忘了 break ← 这是一个 bug
case 1:
do_other(); // 会被意外执行编译器可以用 -Wimplicit-fallthrough 标记潜在的意外穿透,GCC 7+ 支持用 __attribute__((fallthrough)) 显式声明有意的穿透:
case 0:
do_something();
__attribute__((fallthrough)); // 告诉编译器:「我知道,这是故意的」
case 1:
do_shared();
break;课后练习
字符分类统计:编写程序统计输入文本中数字、字母、空白字符和其他字符各出现了多少次,以及总行数。
知识点提示:
getchar逐字符读取、switch按字符分类、\n计数行数printf 格式符统计:分析一条格式化打印语句,统计其中
%d、%c、%s、%f、%p各自出现的次数。知识点提示:遍历字符串匹配
%→ 看下一个字符 →switch分类简单计算器:输入两个整数和一个运算符(
+ - * /),用switch实现四则运算。知识点提示:
switch (op)四个 case、除法除零保护、default 处理非法运算符
练习1: 字符分类统计
#include <stdio.h>
int main(void)
{
int ch;
int digits = 0, letters = 0, spaces = 0, others = 0;
int lines = 1;
while ((ch = getchar()) != EOF)
{
switch (ch)
{
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
digits++; break;
case ' ': case '\t':
spaces++; break;
case '\n':
lines++; break;
default:
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))
letters++;
else
others++;
break;
}
}
printf("digits=%d letters=%d spaces=%d others=%d lines=%d\n",
digits, letters, spaces, others, lines);
return 0;
}用 switch 做字符分类:数字、空白和换行的 case 处理高频场景,字母和其他归入 default 中用 if 精细区分。
练习2: printf 格式符统计
#include <stdio.h>
int main(void)
{
char fmt[] = "num=%d str=%s ptr=%p float=%f char=%c";
int d = 0, c = 0, s = 0, f = 0, p = 0, i = 0;
while (fmt[i])
{
if (fmt[i] == '%' && fmt[i+1])
{
switch (fmt[i+1])
{
case 'd': d++; break;
case 'c': c++; break;
case 's': s++; break;
case 'f': f++; break;
case 'p': p++; break;
}
}
i++;
}
printf("%%d=%d %%c=%d %%s=%d %%f=%d %%p=%d\n", d, c, s, f, p);
return 0;
}先检测 % 字符,再用 switch 对紧随其后的字符分类。这与 printf 内部实现对格式符的解析逻辑完全一致——switch 是这类「查表分类」场景最自然的工具。
练习3: 简单计算器
#include <stdio.h>
int main(void)
{
int a, b;
char op;
scanf("%d %c %d", &a, &op, &b);
switch (op)
{
case '+': printf("%d\n", a + b); break;
case '-': printf("%d\n", a - b); break;
case '*': printf("%d\n", a * b); break;
case '/':
if (b == 0)
printf("Error: division by zero\n");
else
printf("%d\n", a / b);
break;
default:
printf("Unknown operator: %c\n", op);
break;
}
return 0;
}switch (op) 对运算符做四路分支。/ 分支中有额外的除零检查——这是 switch 和 if 自然嵌套的示例。
参考资料
- ISO C99 Standard, Section 6.8.4.2 —
switch语句的语法与语义规范 - Tom Duff on Duff's Device — 1983 年原始的 Duff's Device 邮件与注释
- GCC Warning Options: -Wimplicit-fallthrough — GCC 对 switch 穿透的警告选项
- The C Programming Language, Section 3.4 — Kernighan & Ritchie 对 switch 和 break 的经典讲解
"Programs must be written for people to read, and only incidentally for machines to execute." — Harold Abelson & Gerald Jay Sussman