一个值,多个常量
一条反复把同一个变量与常量比较的 else if 链,在 C 中有专门的语句来表达:
if (choice == 1) { /* ... */ }
else if (choice == 2) { /* ... */ }
else if (choice == 3) { /* ... */ }
else { /* ... */ }
switch 表达得更直接。它对表达式求值一次,然后直接跳转到匹配的标签。
修改 choice 再运行一次。这种结构让所有合法的取值一目了然,这才是在这里优先使用它的真正原因。
语法与 case 标签的规则
switch (integerExpression) {
case CONSTANT_1:
statements
break;
case CONSTANT_2:
statements
break;
default:
statements
break;
}
C 强制执行四条约束:
- switch 表达式必须是整数类型:
int、char、short、long或enum。不能是float、double,也不能是字符串。 - 每个
case标签都必须是编译时已知的常量表达式。case n:(n是变量)是错误的;case 3 + 4:和case MAX:(#define或枚举常量)则没问题。 - 任意两个 case 不能有相同的值。重复会导致编译错误,这是一张小而实在的安全网。
- case 不能表示范围。
case 1 ... 5:是 GCC 扩展,不是标准 C;范围请用if。
default 是可选的,可以出现在代码块的任何位置,不过几乎所有人都约定把它放在最后。没有 default 时,不匹配的值会直接跳过整个 switch。
break:为什么不能省
C 的 switch 最让人意外的一点是,case 标签是跳转目标,而不是隔间。程序一旦落到某个标签上,就会一直执行它下面的每条语句,越过其他 case 标签,直到遇到 break 或右花括号。
第一个 switch 打印 one、two 和 three:它匹配了 case 1,然后把后面的所有内容都执行了。第二个只打印 one。这种行为叫作贯穿(fall-through),而忘写 break 是经典的 C switch bug:程序不会编译失败,只是悄无声息地出错。
用 gcc -Wall -Wextra -Wimplicit-fallthrough 编译,编译器会指出每一个贯穿到下一个 case 的地方。
最后一个 case 的 break 严格来说是多余的,因为代码块本来就在那里结束。但还是要写上,这样以后在它下面添加新的 case 时,就不会悄悄地产生贯穿。
有意的贯穿
如果是有意为之,贯穿就是一项特性。把多个标签叠在一起、中间不放语句,是表达“这些值执行相同操作”的常规写法:
三个标签共用一段代码,没有重复代码,也没有冗长的 || 条件。编译器不会对这种写法发出警告,因为中间没有可以贯穿的语句。
中间有语句的贯穿,即 case 1 执行完之后有意继续执行 case 2,则比较少见,值得加上明确的注释,因为必须告诉读者漏掉的 break 是刻意的决定:
switch (level) {
case 3:
printf("verbose\n");
/* fall through */
case 2:
printf("info\n");
/* fall through */
case 1:
printf("errors\n");
break;
}
/* fall through */ 是 GCC 警告能识别的注释,所以它也能消除这条诊断信息。
对字符使用 switch
因为 char 是整数类型,用 switch 做字符分类再自然不过:
注意 case 'a': 用的是单引号,表示字符常量,它就是该字符的整数值。case "a": 则是字符串,无法通过编译。
对枚举使用 switch
在 C 中,switch 和枚举是最佳搭档。case 标签变成了可读的名字,而且当 switch 没有处理所有枚举值时,编译器可以发出警告:
这里有两点值得借鉴。每个 case 都直接 return,所以不需要 break:return 会直接离开函数,switch 自然也就结束了。另外这里没有 default:启用 -Wswitch(包含在 -Wall 中)时,不写 default 能让编译器在有人添加第五个状态却忘了改这个函数时发出警告。写了 default 反而会掩盖这个问题。
在 switch 内部声明变量
switch 的主体是一个代码块,所以在某个 case 中的声明,在它后面的 case 中也可见,尽管不一定已经初始化。在 C 中,跳过初始化是错误的:
switch (n) {
case 1:
int x = 10; // error in C: a label cannot precede a declaration this way
printf("%d\n", x);
break;
case 2:
printf("%d\n", x); // x exists here, but was never initialised
break;
}
当某个 case 需要局部变量时,用一对花括号把它包起来:
switch (n) {
case 1: {
int x = 10;
printf("%d\n", x);
break;
}
case 2:
printf("two\n");
break;
}
用 switch 还是 if-else?
当以下条件全部满足时用 switch:你在判断一个表达式,它是整数类型,并且你在把它与固定常量比较。菜单处理、枚举状态机、字符分类和命令分发都是典型场景。好处是可读性好、能检查重复值,而且编译器能对未处理的枚举值发出警告。
当判断涉及范围(score >= 90)、浮点数、字符串(需要用 strcmp)、多个不同的变量,或用 && 和 || 组合的条件时,用 if-else。这些都无法写成 case 标签。
性能很少是决定因素。编译器会把密集的 switch 转换成跳转表,把稀疏的 switch 转换成一系列比较,所以只有几个 case 时,两种写法没有区别。选择能表达意图的那种。
常见问题
C 语言中 switch 语句是怎么工作的?
switch (expr) 对一个整数表达式求值一次,跳转到常量与之匹配的 case 标签,然后从那里开始执行,直到遇到 break 或 switch 的末尾。如果没有匹配项,有 default 时就跳到那里,否则跳过整个代码块。
C 语言的 switch 为什么需要 break?
因为 case 标签是跳转目标,而不是独立的代码块。没有 break,程序会直接继续执行下一个 case 的语句,这叫作贯穿(fall-through)。漏写 break 是 C 语言中最常见的 switch bug,gcc -Wimplicit-fallthrough 会把它标记出来。
C 语言中能对字符串使用 switch 吗?
不能。C 的 switch 要求整数表达式,所以 int、char 和 enum 值都可以,但字符串、浮点数和范围不行。要根据字符串分支,可以在 if-else 链中用 strcmp,或者先把字符串映射成枚举,再对枚举使用 switch。
C 语言中什么时候应该用 switch 而不是 if-else?
当你把一个整数或字符和一组固定常量比较时用 switch:菜单命令、枚举状态、字符分类。当判断涉及范围、多个变量,或者任何不是与编译期常量做相等比较的条件时,用 if-else 链。