8.6 — Switch 穿透和作用域

本课程将继续我们从上节课 8.5 -- Switch 语句基础 开始的 switch 语句探索。在上一课中,我们提到标签下的每组语句都应以一个 break 语句 或一个 return 语句 结束。

在本课程中,我们将探讨原因,并讨论一些有时会让新程序员感到困惑的 switch 作用域问题。

穿透(Fallthrough)

当 switch 表达式匹配一个 case 标签或可选的 default 标签时,执行从匹配标签后的第一个语句开始。执行将顺序继续,直到以下终止条件之一发生:

  • 到达 switch 块的末尾。
  • 另一个控制流语句(通常是 breakreturn)导致 switch 块或函数退出。
  • 其他情况中断了程序的正常流程(例如,操作系统关闭程序,宇宙内爆等…)

请注意,存在另一个 case 标签不是这些终止条件之一——因此,如果没有 breakreturn,执行将溢出到后续的 case 中。

这是一个展示这种行为的程序:

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

该程序输出以下内容:

2
3
4
5

这可能不是我们想要的!当执行从一个标签下的语句流向后续标签下的语句时,这被称为穿透

警告

一旦 case 或 default 标签下的语句开始执行,它们将溢出(穿透)到后续的 case 中。breakreturn 语句通常用于防止这种情况。

由于穿透很少是期望或有意的,许多编译器和代码分析工具会将穿透标记为警告。

[[fallthrough]] 属性

注释有意的穿透是告诉其他开发人员穿透是有意为之的常见约定。虽然这对其他开发人员有用,但编译器和代码分析工具不知道如何解释注释,因此它不会消除警告。

为了解决这个问题,C++17 添加了一个名为 [[fallthrough]] 的新属性。

属性是现代 C++ 的一个特性,允许程序员向编译器提供一些关于代码的额外数据。要指定一个属性,属性名称放在双括号之间。属性不是语句——相反,它们几乎可以在任何上下文相关的地方使用。

[[fallthrough]] 属性修改一个 null 语句,以表明穿透是有意的(并且不应触发任何警告)。

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

这个程序打印

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');
}

这与我们在 switch 语句介绍中提出的挑战相同:c 被评估多次,读者必须确保每次评估的都是 c

您可以通过放置多个连续的 case 标签来使用 switch 语句执行类似的操作:

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

请记住,执行从匹配的 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 和 default 标签之间的 2 条语句被作用于 switch 块的一部分,而不是 case 1 的隐式块。

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

您可以在 switch 内部声明或定义(但不能初始化)变量,无论是在 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;
}

尽管变量 ycase 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;
}

最佳实践

如果在 case 语句中定义变量,请在 case 内的块中进行。

小测验时间

问题 #1

编写一个名为 calculate() 的函数,它接受两个整数和一个字符,代表以下数学运算之一:+、-、*、/ 或 %(求余)。使用 switch 语句对整数执行适当的数学运算,并返回结果。如果将无效运算符传递给函数,函数应打印错误消息。对于除法运算符,执行整数除法,不必担心除以零。

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

显示答案

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
342 评论
最新
最早 最多投票
内联反馈
查看所有评论