switch 的贯穿行为(fallthrough)与作用域

本节在前一课《switch 语句基础》的基础上,继续探讨 switch 语句。上一课提到,每个标签下的语句组应以 breakreturn 结束;本课将说明原因,并讨论初学者常遇到的 switch 作用域问题。

贯穿行为(fallthrough)

switch 表达式的值匹配某个 casedefault 标签时,执行从该标签后的第一条语句开始,随后顺序继续,直到出现以下任一终止条件:

  • 到达 switch 块的末尾;
  • 遇到控制流语句(通常是 breakreturn)导致退出 switch 块或函数;
  • 其他程序中断事件(如操作系统终止程序、宇宙坍缩等)。

注意:遇到下一个 case 标签不会自动终止执行;若无 breakreturn,执行将“溢出”到后续标签。

以下程序演示了贯穿:

#include <iostream>

int main()
{
    switch (2)
    {
    case 1:                 // 不匹配
        std::cout << 1 << '\n'; // 跳过
    case 2:                 // 匹配!
        std::cout << 2 << '\n'; // 从此处开始执行
    case 3:
        std::cout << 3 << '\n'; // 继续执行
    case 4:
        std::cout << 4 << '\n'; // 继续执行
    default:
        std::cout << 5 << '\n'; // 继续执行
    }
    return 0;
}

输出:

2
3
4
5

这显然并非预期结果。当执行从某标签“溢出”到下一标签时,称为贯穿(fallthrough)。

警告
一旦 casedefault 标签下的语句开始执行,它们将贯穿后续标签。通常用 breakreturn 阻止此行为。

由于贯穿极少是有意为之,多数编译器与静态分析工具会对其发出警告。

[[fallthrough]] 属性

传统做法是在有意贯穿处加注释,以告知其他开发者。然而注释无法被编译器识别,警告依然存在。

C++17 引入属性 [[fallthrough]] 来解决此问题。属性是现代 C++ 特性,允许程序员向编译器提供额外信息;属性名置于双方括号内,且不是语句,可在任何上下文相关位置使用。

[[fallthrough]] 必须修饰一条空语句(即以分号结尾),表明贯穿是故意的,不应触发警告:

#include <iostream>

int main()
{
    switch (2)
    {
    case 1:
        std::cout << 1 << '\n';
        break;
    case 2:
        std::cout << 2 << '\n';        // 从此处开始
        [[fallthrough]];               // 有意贯穿,注意分号
    case 3:
        std::cout << 3 << '\n';        // 继续执行
        break;
    }
    return 0;
}

输出:

2
3

编译器不应再发出贯穿警告。

最佳实践
若故意贯穿,请使用 [[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');
}

但这样 c 被多次求值,且阅读者需确认每次比较的都是 c

利用 switch 序贯标签可实现相同效果:

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

记住,执行从首个匹配的 case 标签后的第一条语句开始。case 标签不是语句而是标签,因此不阻断执行。上述所有 case 标签共享随后的 return true;,这不属于贯穿,无需注释或 [[fallthrough]]

标签不引入新作用域

if 语句的条件后只能跟一条语句,该语句视为隐式块内:

if (x > 10)
    std::cout << x << " is greater than 10\n"; // 视为隐式块内

switch 则不同:所有标签后的语句都属于 switch 块作用域,不会为每个标签创建隐式块。

switch (1)
{
case 1:   // 不创建隐式块
    foo(); // 属于 switch 作用域,而非 case 1 的隐式块
    break; // 同上
default:
    std::cout << "default case\n";
    break;
}

变量声明与初始化在 case 中的限制

可在 switch 内(包括标签前后)声明或定义变量,但初始化受限制:

switch (1)
{
    int a;     // 合法:可在标签前定义
    int b{5};  // 非法:标签前不可初始化

case 1:
    int y;     // 可定义,但不良实践
    y = 4;     // 合法:可赋值
    break;

case 2:
    int z{4};  // 非法:若后续仍有 case,不能初始化
    y = 5;     // 合法:y 已定义,可在此处使用
    break;

case 3:
    break;
}

变量 y 虽在 case 1 定义,却可在 case 2 使用,因为所有语句位于同一作用域。但若 switch 跳过 case 1y 未初始化即被使用会导致未定义行为,因此禁止在非最后 case 中直接初始化变量。标签前的初始化语句亦永不会被执行,故亦禁止。

若某分支需定义并初始化新变量,最佳做法是在该分支内使用显式语句块:

switch (1)
{
case 1:
{                         // 显式块
    int x{4};             // 合法:可在块内初始化
    std::cout << x;
    break;
}

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

最佳实践
如需在 case 内定义变量,请将其置于显式语句块中。

测验

问题 #1
编写函数 calculate,接受两个整数和一个字符(表示运算符:+-*/%)。使用 switch 语句完成相应运算并返回结果。若运算符无效,函数应输出错误信息。除法按整型除法处理,无需考虑除零。

提示:operator 是关键字,不可用作变量名。

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

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

公众号二维码

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