8-5 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;
}

image

在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;
}

image

switch语句switch statement的原理很简单:先求值一个表达式(有时称为条件)以产生一个值。

随后会发生以下情况之一:

  • 若表达式的值等于任何case标签后的值,则执行匹配case标签后的语句。
  • 若未找到匹配值且存在默认标签,则执行默认标签后的语句。
  • 若未找到匹配值且不存在默认标签,则跳过该switch语句。

下面我们将详细探讨这些概念。


启动switch语句

我们使用 switch 关键字启动switch语句,随后用括号括起需要在内部求值的条件表达式。通常该表达式仅包含单个变量,但也可以是任何有效的表达式。

开关语句中的条件必须求值为整数的类型(若需回顾哪些基本类型属于整数的类型,请参阅第4.1课——基础数据类型介绍),或枚举类型(将在后续第13.2课——无作用域枚举第13.6课——作用域枚举(枚举类)中讲解),或可转换为此类类型。求值结果为浮点类型、字符串及大多数其他非整数的类型的表达式在此处不可使用。

对于进阶读者

为什么switch语句只允许使用整型的(或枚举型)类型?答案在于switch语句的设计初衷就是高度优化。历史上,编译器实现switch语句最常见的方式是通过跳转表——而跳转表只能处理整数的值。

对于熟悉数组的读者,跳转表的工作原理类似数组:整数的值作为数组索引,可直接“跳转”至对应结果。这种方式远比逐个顺序比较高效。

当然,编译器并非必须使用跳转表实现 switch 语句,有时也会采用其他方式。从技术角度而言,C++ 完全可以放宽限制允许使用其他类型,只是目前(截至 C++23 标准)尚未实现这一变更。

在条件表达式之后,我们声明一个代码块。在代码块内部,我们使用标签来定义所有需要进行相等性测试的值。switch语句中使用的标签有两种类型,我们将在后续部分进行讨论。


Case 标签

第一类标签是case 标签case label,使用case关键字声明,后跟constant表达式。该constant表达式必须与条件类型匹配,或可转换为该类型。

若条件表达式的值等于 case标签 后的表达式,则从该 case标签 后的首个语句开始执行,并按顺序继续执行。

以下是条件与案例标签匹配的示例:

#include <iostream>

void printDigitName(int x)
{
    switch (x) // x is evaluated to produce value 2
    {
    case 1:
        std::cout << "One";
        return;
    case 2: // which matches the case statement here
        std::cout << "Two"; // so execution starts here
        return; // and then we return to the caller
    case 3:
        std::cout << "Three";
        return;
    default:
        std::cout << "Unknown";
        return;
    }
}

int main()
{
    printDigitName(2);
    std::cout << '\n';

    return 0;
}

此代码输出:

image

在上面的程序中,x 的计算结果为 2。由于存在值为 2 的 case标签,执行流程跳转到该 case标签 对应的语句。程序输出 Two 后,执行 return 语句返回调用方。

case标签 的数量没有实际限制,但 switch 语句中的所有 case标签 必须唯一。也就是说,不能这样写:

switch (x)
{
case 54:
case 54:  // error: already used value 54!
case '6': // error: '6' converts to integer value 54, which is already used
}

image

如果条件表达式与任何案例标签都不匹配,则不会执行任何案例。我们稍后将展示一个示例。


默认标签

第二种标签是默认标签default label(常称为默认casedefault case),使用default关键字声明。若条件表达式与任何情况标签都不匹配且存在默认标签,则执行从 默认标签 后的第一条语句开始。

以下是条件匹配 默认标签 的示例:

#include <iostream>

void printDigitName(int x)
{
    switch (x) // x is evaluated to produce value 5
    {
    case 1:
        std::cout << "One";
        return;
    case 2:
        std::cout << "Two";
        return;
    case 3:
        std::cout << "Three";
        return;
    default: // which does not match to any case labels
        std::cout << "Unknown"; // so execution starts here
        return; // and then we return to the caller
    }
}

int main()
{
    printDigitName(5);
    std::cout << '\n';

    return 0;
}

此代码输出:

image

默认标签 是可选的,每个switch语句中只能有一个 默认标签。按惯例,默认情况应置于switch代码块的末尾。

最佳实践
将默认情况放在switch块的最后。


无匹配的 case标签 且未提供 默认case

若条件表达式的值与任何case标签均不匹配,且未提供 默认case,则switch语句块内的所有case语句均不执行。程序执行将跳过switch块继续进行。

#include <iostream>

void printDigitName(int x)
{
    switch (x) // x is evaluated to produce value 5
    {
    case 1:
        std::cout << "One";
        return;
    case 2:
        std::cout << "Two";
        return;
    case 3:
        std::cout << "Three";
        return;
    // no matching case exists and there is no default case
    }

    // so execution continues here
    std::cout << "Hello";
}

int main()
{
    printDigitName(5);
    std::cout << '\n';

    return 0;
}

image

在上例中,x 求值为 5,但没有与 5 匹配的 case标签,也没有 默认case。因此,没有cases被执行。执行流程跳过 switch 块继续,最终输出 Hello。


执行 break 语句

在上面的示例中,我们使用 return 语句来停止标签后语句的执行。但这也会退出整个函数。

break语句break statement(使用 break 关键字声明)会告知编译器:我们已完成 switch 块内语句的执行,应继续执行 switch 块结束后的语句。这使我们能够退出 switch 语句而不退出整个函数。

以下示例将 return 替换为 break 进行重写:

#include <iostream>

void printDigitName(int x)
{
    switch (x) // x evaluates to 3
    {
    case 1:
        std::cout << "One";
        break;
    case 2:
        std::cout << "Two";
        break;
    case 3:
        std::cout << "Three"; // execution starts here
        break; // jump to the end of the switch block
    default:
        std::cout << "Unknown";
        break;
    }

    // execution continues here
    std::cout << " Ah-Ah-Ah!";
}

int main()
{
    printDigitName(3);
    std::cout << '\n';

    return 0;
}

上述示例输出:

image

最佳实践
标签下方的每组语句都应以break语句或return语句结束。这包括switch语句中最后一个标签下方的语句。

那么,如果在标签下的语句块末尾没有使用break或return语句结束,会发生什么情况呢?我们将在下一课中探讨这个话题及其他相关内容。


标签通常不缩进

第2.9课——命名冲突与命名空间介绍中,我们提到代码通常会缩进一级以帮助识别其属于嵌套作用域区域。由于switch语句的波浪括号定义了新的作用域区域,我们通常会将括号内的所有内容缩进一级。

然而标签本身并不定义嵌套作用域。因此标签后面的代码通常不缩进。

但若将标签及其后续语句统一缩进相同层级,最终效果如下所示:

// Unreadable version
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;
   }
}

这使得很难确定每个案例的起止位置。

我们有两种选择。首先,我们可以无论如何都缩进标签后的语句:

// Acceptable but not preferred version
void printDigitName(int x)
{
    switch (x)
    {
        case 1: // indented from switch block
            std::cout << "One"; // indented from label (misleading)
            return;
        case 2:
            std::cout << "Two";
            return;
        case 3:
            std::cout << "Three";
            return;
        default:
            std::cout << "Unknown";
            return;
    }
}

虽然这种写法比之前的版本更易读,但它暗示每个标签下的语句处于嵌套作用域中,实际并非如此(下节课我们将看到相关示例:在某个分支定义的变量可在另一个分支使用)。这种格式虽被视为可接受(因其可读性),但并非首选。

传统做法是标签不缩进:

// Preferred version
void printDigitName(int x)
{
    switch (x)
    {
    case 1: // not indented from switch statement
        std::cout << "One";
        return;
    case 2:
        std::cout << "Two";
        return;
    case 3:
        std::cout << "Three";
        return;
    default:
        std::cout << "Unknown";
        return;
    }
}

这使得每个标签都易于识别。由于这些语句仅比switch块缩进一层,因此正确地暗示了它们都属于switch块的作用域。

在后续课程中,我们将遇到其他类型的标签——出于相同原因,这些标签也通常不缩进。

最佳实践
建议不要缩进标签。这样既能让标签从周围代码中突出显示,又不会暗示它们定义了嵌套作用域区域。


switch 语句与 if-else 语句的对比

当需要将单个表达式(非布尔整数的类型或枚举类型)与少量值进行相等性比较时,switch 语句最为适用。若 case 标签数量过多,switch 语句将难以阅读。

相较于等效的if-else语句,switch语句更具可读性,能清晰体现每个分支测试的是同一表达式,且具有仅求值一次表达式的优势(提升效率)。

但if-else的灵活性显著更高。以下场景通常更适合使用if或if-else:

  • 测试非相等比较(如x > 5)
  • 需要同时满足多个条件(例如 x == 5 && y == 6)
  • 判断值是否在特定区间内(例如 x >= 5 && x <= 10)
  • 表达式类型不被 switch 支持(例如 d == 4.0)
  • 表达式最终求值结果为布尔值

最佳实践
当需要将单个表达式(非布尔整数的类型或枚举类型)与少量值进行相等性比较时,应优先使用switch语句而非if-else语句。

posted @ 2026-02-26 18:02  游翔  阅读(0)  评论(0)    收藏  举报