switch 语句基础

虽然可以通过连续书写多个 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 语句的核心思想十分简洁:

  1. 先计算一个表达式(常称为“条件表达式”)的值;
  2. 然后执行下列三者之一:
    • 若表达式的值等于某个 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!

最佳实践:每个标签下的语句组应以 breakreturn 结束,包括 switch 的最后一个标签。

若未以 breakreturn 结束,将产生“贯穿”行为,下一节将深入讨论。

六、标签的缩进约定

在 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 块缩进一级,正确反映其作用域关系。后续课程将出现的其他类型标签也遵循同一不缩进约定。

最佳实践:标签不缩进,以便突出显示且不暗示额外嵌套作用域。

七、switchif-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

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"