8-6 Switch语句的贯穿和作用域

本节课将延续上一节课(第8.5节——switch语句基础)中关于switch语句的探讨。在上节课中,我们提到标签下的每组语句都应以break语句或return语句结束。

本节课我们将深入解析其原因,并探讨switch语句作用域问题——这些问题常令新手程序员感到困惑。


贯穿(Fallthrough)

当switch表达式匹配某个case标签或可选的default标签时,执行将从匹配标签后的首个语句开始。随后程序将按顺序执行,直至满足以下终止条件之一:

  • 到达switch代码块末尾。
  • 其他控制流语句(通常为break或return)导致switch代码块或函数退出。
  • 程序正常流程被其他事件中断(例如操作系统强制终止程序、宇宙坍缩等)

请注意,另一个案例标签的存在并非这些终止条件之一——因此,若未设置break或return语句,程序执行将溢出至后续案例。

以下程序演示了这种行为:

#include <iostream>

int main()
{
    switch (2)
    {
    case 1: // Does not match
        std::cout << 1 << '\n'; // Skipped
    case 2: // Match!
        std::cout << 2 << '\n'; // Execution begins here
    case 3:
        std::cout << 3 << '\n'; // This is also executed
    case 4:
        std::cout << 4 << '\n'; // This is also executed
    default:
        std::cout << 5 << '\n'; // This is also executed
    }

    return 0;
}

该程序输出以下内容:

image

这可能不是我们想要的效果!当执行流程从标签下方的语句流向后续标签下方的语句时,这种现象称为贯穿fallthrough

警告
一旦某个case或默认标签下的语句开始执行,它们将溢出(贯穿)到后续的case中。通常使用break或return语句来防止这种情况发生。

由于穿透行为通常并非预期或有意为之,许多编译器和代码分析工具会将其标记为警告。


[[fallthrough]] 属性

注释有意跳转是常见惯例,用于告知其他开发者此处存在预期跳转。虽然这能让其他开发者理解,但编译器和代码分析工具无法解析注释,因此无法消除警告。

为解决此问题,C++17新增了名为 [[fallthrough]] 的属性。

属性attribute是现代C++特性,允许程序员向编译器提供代码的附加信息。属性名称需用双括号括起,其本身并非语句——而是在上下文相关的绝大多数位置均可使用。

[[fallthrough]]属性用于修饰空语句,明确标记为有意跳转(且不应触发警告):

#include <iostream>

int main()
{
    switch (2)
    {
    case 1:
        std::cout << 1 << '\n';
        break;
    case 2:
        std::cout << 2 << '\n'; // Execution begins here
        [[fallthrough]]; // intentional fallthrough -- note the semicolon to indicate the null statement
    case 3:
        std::cout << 3 << '\n'; // This is also executed
        break;
    }

    return 0;
}

该程序输出:

image

并且不应产生任何关于穿透的警告。

最佳实践
使用 [[fallthrough]] 属性(配合空语句)来表示有意穿透。


顺序 case标签

您可以使用逻辑或运算符将多个测试组合为单个语句:

bool isVowel(char c)
{
    return (c=='a' || c=='e' || c=='i' || c=='o' || c=='u' ||
        c=='A' || c=='E' || c=='I' || c=='O' || c=='U');
}

这同样存在我们在switch语句介绍中提到的挑战:c会被多次求值,读者必须确保每次求值的都是c。

你也可以通过在switch语句中依次放置多个case标签来实现类似效果:

bool isVowel(char c)
{
    switch (c)
    {
    case 'a': // if c is 'a'
    case 'e': // or if c is 'e'
    case 'i': // or if c is 'i'
    case 'o': // or if c is 'o'
    case 'u': // or if c is 'u'
    case 'A': // or if c is 'A'
    case 'E': // or if c is 'E'
    case 'I': // or if c is 'I'
    case 'O': // or if c is 'O'
    case 'U': // or if c is 'U'
        return true;
    default:
        return false;
    }
}

DeepL(内置)
英语

请记住,执行从匹配的case标签后的第一条语句开始。case标签本身不是语句(它们是标签),因此不计入执行序列。

在上述程序中,所有case语句之后的第一条语句是return true,因此只要有任何case标签匹配,函数就会返回true。

由此,我们可以“堆叠”case标签,使所有这些case标签共享后续相同的语句集。这不被视为贯穿行为,因此在此处无需使用注释或[[fallthrough]]。


标签不会定义新的作用域

在if语句中,if条件后只能跟单个语句,该语句被视为隐式位于代码块内:

if (x > 10)
    std::cout << x << " is greater than 10\n"; // this line implicitly considered to be inside a block

然而,在switch语句中,标签后的语句都属于switch代码块的作用域。不会创建隐式代码块。

switch (1)
{
case 1: // does not create an implicit block
    foo(); // this is part of the switch scope, not an implicit block to case 1
    break; // this is part of the switch scope, not an implicit block to case 1
default:
    std::cout << "default case\n";
    break;
}

在上例中,case 1 与默认case之间的两个语句属于 switch 块的作用域,而非 case 1 的隐含作用域。


在case语句内部声明和初始化变量

您可以在switch语句内部声明或定义变量(但不能初始化),既可在case标签之前,也可在case标签之后进行:

switch (1)
{
    int a; // okay: definition is allowed before the case labels
    int b{ 5 }; // illegal: initialization is not allowed before the case labels

case 1:
    int y; // okay but bad practice: definition is allowed within a case
    y = 4; // okay: assignment is allowed
    break;

case 2:
    int z{ 4 }; // illegal: initialization is not allowed if subsequent cases exist
    y = 5; // okay: y was declared above, so we can use it here too
    break;

case 3:
    break;
}

image

尽管变量 y 在 case 1 中被定义,它在 case 2 中也被使用。switch 语句内部的所有语句都被视为同一作用域的一部分。因此,在一个 case 中声明或定义的变量可以在后续 case 中使用,即使该变量定义所在的 case 从未被执行(因为 switch 跳过了它)!

然而,变量的初始化确实需要定义语句执行。除最后一个case外,其他任何case中均禁止初始化变量(因为若后续存在定义的case,switch可能跳过初始化语句,导致变量未被定义,访问时将引发未定义行为)。同样禁止在第一个case之前初始化变量,因这些语句永远不会被执行——switch结构无法跳转至此。

若某个case需要定义和/或初始化新变量,最佳实践是在case语句下方显式使用代码块实现:

switch (1)
{
case 1:
{ // note addition of explicit block here
    int x{ 4 }; // okay, variables can be initialized inside a block inside a case
    std::cout << x;
    break;
}

default:
    std::cout << "default case\n";
    break;
}

image

最佳实践
若需定义案例语句中使用的变量,请在案例内部的代码块中进行定义。


测验时间

问题 #1

编写一个名为 calculate() 的函数,该函数接受两个整数和一个字符参数,该字符代表以下数学运算符之一:+、-、*、/ 或 %(取余)。使用switch语句对整数执行对应运算并返回结果。若传入无效运算符,函数应输出错误提示。除法运算需执行整数除法,无需处理除以零的情况。

提示:operator是关键字,变量名不能命名为operator。

显示方案

#include <iostream>

int calculate(int x, int y, char op)
{
    switch (op)
    {
    case '+':
        return x + y;
    case '-':
        return x - y;
    case '*':
        return x * y;
    case '/':
        return x / y;
    case '%':
        return x % y;
    default:
        std::cout << "calculate(): Unhandled case\n";
        return 0;
    }
}

int main()
{
    std::cout << "Enter an integer: ";
    int x{};
    std::cin >> x;

    std::cout << "Enter another integer: ";
    int y{};
    std::cin >> y;

    std::cout << "Enter a mathematical operator (+, -, *, /, or %): ";
    char op{};
    std::cin >> op;

    // We'll call calculate first so an invalid operator prints an error message on its own line
    int result{ calculate(x, y, op) };
    std::cout << x << ' ' << op << ' ' << y << " is " << result << '\n';

    return 0;
}
posted @ 2026-02-26 19:09  游翔  阅读(1)  评论(0)    收藏  举报