虽然可以通过连续书写多个 if-else
语句来实现多分支判断,但这样既不直观又低效。观察以下示例:
#include <iostream>
void printDigitName(int x)
{
if (x == 1)
std::cout << "One";
else if (x == 2)
std::cout << "Two";
else if (x == 3)
std::cout << "Three";
else
std::cout << "Unknown";
}
int main()
{
printDigitName(2);
std::cout << '\n';
return 0;
}
在 printDigitName()
中,变量 x
可能最多被求值三次(取决于传入值),且读者必须反复确认每次比较的都是同一个变量 x
。
为了“将一个变量或表达式与多个不同值进行相等比较”这一常见需求,C++ 提供了专门的替代条件语句——switch
语句。以下示例与上例功能完全一致,但改用 switch
实现:
#include <iostream>
void printDigitName(int x)
{
switch (x)
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}
int main()
{
printDigitName(2);
std::cout << '\n';
return 0;
}
switch
语句的核心思想十分简洁:
- 先计算一个表达式(常称为“条件表达式”)的值;
- 然后执行下列三者之一:
- 若表达式的值等于某个
case
标签后的常量,则从该标签后的第一条语句开始执行; - 若找不到匹配值且存在
default
标签,则从default
标签后的第一条语句开始执行; - 若既无匹配值又无
default
标签,则整个switch
被跳过。
- 若表达式的值等于某个
下文将逐一详细说明这些概念。
一、启动 switch
语句
以关键字 switch
开头,后跟一对圆括号,括号内放置待求值的条件表达式。表达式通常为单个变量,但亦可是任何合法表达式。
switch
的条件表达式结果必须属于整型(参见 4.1 节《基本数据类型简介》中的整型列表)或枚举类型(将在 13.2 节《无作用域枚举》与 13.6 节《作用域枚举(enum class)》中介绍),或可转换为上述类型。浮点类型、字符串以及大多数非整型类型均不可直接用于 switch
。
进阶说明:
为何 switch
仅限整型或枚举?因为 switch
被设计为可高度优化。历史上,编译器最常见的实现方式是“跳转表”,而跳转表仅适用于整型值。熟悉数组的读者可把跳转表看作数组:用整型值做索引即可直接“跳”到目标代码,远快于一系列顺序比较。当然,编译器并不强制使用跳转表,有时也不会使用;理论上 C++ 可放宽限制,但截至 C++23 尚未实现。
在条件表达式之后,需声明一个语句块。块内使用两类标签来列出所有待比较的值,下文分别介绍。
二、case
标签
第一类标签为 case
标签,由关键字 case
后接一个常量表达式构成。该常量表达式的类型必须与条件表达式一致或可转换为其类型。
当条件值与某 case
标签后的常量相等时,程序从该标签后的第一条语句开始顺序执行。
示例:条件匹配 case
标签
#include <iostream>
void printDigitName(int x)
{
switch (x) // x 求值为 2
{
case 1:
std::cout << "One";
return;
case 2: // 匹配该 case
std::cout << "Two"; // 从此处开始执行
return; // 返回调用者
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}
int main()
{
printDigitName(2);
std::cout << '\n';
return 0;
}
输出:
Two
case
标签数量无实际上限,但同一 switch
内所有 case
常量必须互不相同。例如:
switch (x)
{
case 54:
case 54: // 错误:值 54 已使用
case '6': // 错误:字符 '6' 转换为整数 54,已存在
}
若条件值与任何 case
均不匹配,且存在 default
标签,则从 default
标签开始执行;若无 default
标签,则跳过整个 switch
。
三、default
标签
第二类标签为 default
标签(常称“默认分支”),由关键字 default
引入。若条件值未匹配任何 case
且存在 default
,则从 default
后的第一条语句开始执行。
示例:条件匹配 default
标签
#include <iostream>
void printDigitName(int x)
{
switch (x) // x 求值为 5
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default: // 无匹配 case
std::cout << "Unknown"; // 从此处开始执行
return;
}
}
int main()
{
printDigitName(5);
std::cout << '\n';
return 0;
}
输出:
Unknown
default
标签可选,且每个 switch
至多一个。约定俗成将其置于 switch
块末尾。
最佳实践:将 default
分支放在 switch
块的最后。
四、无匹配 case
且无 default
若条件值既未匹配任何 case
,又无 default
分支,则 switch
内任何分支都不会执行,程序继续执行 switch
块后的语句。
#include <iostream>
void printDigitName(int x)
{
switch (x) // x 求值为 5
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
// 无匹配 case,也无 default
}
// 因此执行流继续到此处
std::cout << "Hello";
}
int main()
{
printDigitName(5);
std::cout << '\n';
return 0;
}
输出:
Hello
五、使用 break
退出 switch
前述示例用 return
终止后续语句并返回函数。若需仅退出 switch
而保留函数继续执行,可使用 break
语句(以关键字 break
声明)。
示例:使用 break
替代 return
#include <iostream>
void printDigitName(int x)
{
switch (x) // x 求值为 3
{
case 1:
std::cout << "One";
break;
case 2:
std::cout << "Two";
break;
case 3:
std::cout << "Three"; // 从此处开始执行
break; // 跳到 switch 块尾
default:
std::cout << "Unknown";
break;
}
// 执行流继续到此处
std::cout << " Ah-Ah-Ah!";
}
int main()
{
printDigitName(3);
std::cout << '\n';
return 0;
}
输出:
Three Ah-Ah-Ah!
最佳实践:每个标签下的语句组应以 break
或 return
结束,包括 switch
的最后一个标签。
若未以 break
或 return
结束,将产生“贯穿”行为,下一节将深入讨论。
六、标签的缩进约定
在 2.9 节《命名冲突与命名空间简介》中提到,代码通常缩进一级以表明其属于嵌套作用域。switch
的花括号确为新的作用域,故内部语句一般缩进一级。
然而,标签本身不引入嵌套作用域,因此标签后的语句约定不缩进。
若将标签与后续语句同层缩进,可读性极差:
// 难读版
void printDigitName(int x)
{
switch (x)
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
...
}
}
折中做法是将后续语句再缩进一级,虽可读性稍好,却暗示了并不存在的嵌套作用域(下一节将展示在某 case 中定义的变量可被后续 case 使用)。因此该做法可接受,但并非首选。
约定写法是标签不缩进:
// 推荐版
void printDigitName(int x)
{
switch (x)
{
case 1: // 不缩进
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}
此写法易于识别各标签,且语句仅相对于 switch
块缩进一级,正确反映其作用域关系。后续课程将出现的其他类型标签也遵循同一不缩进约定。
最佳实践:标签不缩进,以便突出显示且不暗示额外嵌套作用域。
七、switch
与 if-else
的取舍
switch
语句最适合:
- 对单个非布尔整型或枚举表达式;
- 与少量离散值进行相等比较。
若 case
标签过多,switch
可读性下降。
与等价的 if-else
相比,switch
:
- 可读性更高;
- 明确指出每次比较的是同一表达式;
- 表达式仅求值一次,效率更优。
然而,if-else
灵活性更高,以下情形应优先使用 if-else
:
- 需进行非相等比较(如
x > 5
); - 需同时测试多个条件(如
x == 5 && y == 6
); - 需判断值是否落在区间(如
x >= 5 && x <= 10
); - 表达式类型不被
switch
支持(如double d == 4.0
); - 表达式求值为
bool
类型。
最佳实践:
当对单个非布尔整型或枚举表达式与少量离散值进行相等比较时,优先使用 switch
而非 if-else
。