跳转到内容

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 6restricted,输入 3 9free

练习 3(完整程序):实现 get_week_day() 从年月日推算星期,组合 get_last_char + is_restricted + get_week_day 完成完整限行判断。输入 A23456 + 2013 1 1restricted!

提示switch-casecase 分支如果不写 break 会「穿透」到下一个 case——这看似是陷阱,实则是特性(允许多个 case 共享同一段代码)。日期推算星期的核心是:计算从已知参考日到目标日期的总天数差,对 7 取模。

核心知识点

  • enum 枚举类型 — 为整数常量命名,第一个值默认为 0,后续自动递增
  • switch-case 多路分支 — 基于整数表达式的跳转表,比多层 if-else 更清晰
  • case 的穿透(fall-through) — 不写 break 时继续执行下一个 case,可实现多值共享
  • break 跳出 switch — 阻止穿透,强制结束整个 switch
  • default 分支 — 处理所有未匹配的情况,相当于 if-else 链的最终 else
  • switch vs if-else 链 — switch 适合整数值的离散多路分支,if-else 适合范围判断
  • 三元表达式 ?: 回顾 — 在 switchcase 分支内简洁地做二选一赋值
  • 字符串遍历取末字符 — while (str[i]) 走到 '\0',记录最后一个有效字符
  • 日期推算星期 — 累计天数差 + 对 7 取模 + 闰年修正
  • 函数组合调用 — is_restricted(get_last_char(s), get_week_day(y, m, d)) 嵌套组合

代码框架

练习 1:取末字符

get_last_char.c
c
#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:限行判断

is_restricted.c
c
#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 问题:魔数满天飞

bad_no_enum.c
c
if (today == 1)   /* 周一限 0 和 5 */    // 1 是什么? // [!code warning]
    return (tail == 0 || tail == 5);

if (today == 2)   /* 周二限 1 和 6 */    // 2 是什么?
    return (tail == 1 || tail == 6);

代码中充斥着 125 这样的魔数——阅读者必须凭注释或记忆才能理解含义。这正是 enum 要解决的问题。

1.2 enum 的定义与自增规则

enum_definition.c
c
enum day
{
    MONDAY = 1,      // 显式赋值: 1
    TUESDAY,         // 自动 = 2 (前一个 + 1)
    WEDNESDAY,       // 自动 = 3 (前一个 + 1)
    THURSDAY,        // 自动 = 4
    FRIDAY,          // 自动 = 5
    SATURDAY,        // 自动 = 6
    SUNDAY           // 自动 = 7
};

enum 的规则

  1. 第一个枚举值如果没有显式赋值,默认为 0
  2. 每个后续未赋值的枚举值 = 前一个值 + 1
  3. 可以给任意位置显式赋值(如 SPECIAL = 100),后续仍按 +1 递增
enum_demo.c
c
#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_essence.c
c
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_syntax.c
c
switch (表达式)      // 必须是整型(int/char/enum),不能是 float 或字符串
{
    case 值1:        // 如果 表达式 == 值1
        语句A;
        break;       // 跳出整个 switch

    case 值2:        // 如果 表达式 == 值2
        语句B;
        break;

    default:         // 所有 case 都没有匹配时执行
        语句C;
        break;
}

执行流程

  1. 计算 switch 后面括号中的表达式
  2. 依次与每个 case 的值比较(编译器通常优化为跳转表)
  3. 匹配成功 → 从该 case 开始执行,直到遇到 breakswitch 结束
  4. 所有 case 都不匹配 → 执行 default(如果有的话)

2.2 穿透(Fall-through):特性而非 Bug

fallthrough_demo.c
c
#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 值共享同一段代码:

shared_case.c
c
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;
    // ...
}

等价于不使用穿透的写法

c
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 的作用与常见遗忘

switch_break_bug.c
c
// 错误: 忘了写 break
case 0:
    process_zero();
    // 忘了 break → 会继续执行 case 1 的代码!

case 1:
    process_one();
    break;

WARNING

忘记 breakswitch 中最常见的 bug。养成习惯:写完每个 case 的代码后立即写 break,再回去填充 case 体内容。

2.4 default 分支

default_branch.c
c
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 的底层实现:跳转表

asm
; 编译器将 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<10if-elsecase 不支持范围
浮点数比较if-elseswitch 仅支持整型
字符串比较if-else同上
多变量的复合条件if-elseswitch 只能判断一个表达式
少量分支(2~3 个)switchif-else两者可读性接近

3.2 等价的 if-else 写法

restrict_ifelse.c
c
// 用 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 复习三元表达式

ternary_review.c
c
// if-else 版本(4 行)
int result;
if (today == MONDAY)
    result = 1;
else
    result = 0;

// 三元表达式版本(1 行)
int result = (today == MONDAY) ? 1 : 0;
//            └───条件───┘    └真┘ └假┘

4.2 在 switch-case 中的使用

c
case 0: case 5:
    ret = (today == MONDAY) ? 1 : 0;   // 三元让逻辑一行搞定
    break;

为什么在这里用三元:每个 case 只做一个简单判断(今天是否等于限行日),用 if-else 写会增加嵌套层次。三元表达式在一行内完成了「判断 → 赋值」的语义。

NOTE

三元表达式只适用于简单的二选一赋值。如果需要在 case 中执行多条语句,还是应该用 if-else + { }


5. 字符串遍历取末字符

5.1 核心算法

get_last_char_algorithm.c
c
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 等价写法对比

short_get_last.c
c
// 写法 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 实现细节:闰年与每月天数

weekday_calc.c
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;       // 闰年二月 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)最自然的方式:

state_machine.c
c
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.c
c
// 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 + 循环的经典组合

duffs_device.c
c
// 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 年发明的「循环展开」技术——将 switchdo-while 交织在一起,利用 case 穿透实现不完整的第一次循环。这是合法 C 代码,但在此仅为展示 switch 的灵活性,不推荐在生产中使用。


参考解答

练习1: get_last_char(遍历取末字符)
solution_get_last_char.c
c
#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 + 三元)
solution_is_restricted.c
c
#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: 完整限行程序(含日期推算)
solution_restrict_full.c
c
#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 考虑了闰年吗?


课堂讨论

  1. 除了 switch-case,实现 is_restricted 还有其他写法吗?哪种更合适?
  2. 如果考虑到每 3 个月轮换一次尾号限行制度(例如冬季和夏季的限行尾号不同),程序应该如何修改?
  3. switchif-else 在什么场景下各有优势?编译器对两者的优化有什么不同?
  4. 为什么 switch 的条件表达式只能是整型(int/char/enum),而不能是字符串或浮点数?
  5. case 0: case 5: 这种不写 break 的穿透写法,在工程上是推荐还是应该避免?

讨论答案

Q1: switch-case 还有其他实现方式吗?

有多种替代方案

方案 A:if-else 链(见 §3.2 中的代码)——最直接但仍冗长。

方案 B:数组查表法(最简洁):

restrict_array.c
c
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 个月轮换限行制度如何修改?

需要将限行规则从「固定的尾号→星期」改为「动态的尾号→日期范围」:

rotate_restrict.c
c
// 用月份决定当前的限行方案
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):

asm
; 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

c
case 0:
    do_something();
    // 忘了 break ← 这是一个 bug
case 1:
    do_other();    // 会被意外执行

编译器可以用 -Wimplicit-fallthrough 标记潜在的意外穿透,GCC 7+ 支持用 __attribute__((fallthrough)) 显式声明有意的穿透:

c
case 0:
    do_something();
    __attribute__((fallthrough));   // 告诉编译器:「我知道,这是故意的」
case 1:
    do_shared();
    break;

课后练习

  1. 字符分类统计:编写程序统计输入文本中数字、字母、空白字符和其他字符各出现了多少次,以及总行数。

    知识点提示getchar 逐字符读取、switch 按字符分类、\n 计数行数

  2. printf 格式符统计:分析一条格式化打印语句,统计其中 %d%c%s%f%p 各自出现的次数。

    知识点提示:遍历字符串匹配 % → 看下一个字符 → switch 分类

  3. 简单计算器:输入两个整数和一个运算符(+ - * /),用 switch 实现四则运算。

    知识点提示switch (op) 四个 case、除法除零保护、default 处理非法运算符

练习1: 字符分类统计
ex1_char_count.c
c
#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 格式符统计
ex2_format_count.c
c
#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: 简单计算器
ex3_calculator.c
c
#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) 对运算符做四路分支。/ 分支中有额外的除零检查——这是 switchif 自然嵌套的示例。


参考资料

"Programs must be written for people to read, and only incidentally for machines to execute." — Harold Abelson & Gerald Jay Sussman

Released under the MIT License.