switch fallthrough机制与作用域
  本节阅读量: 
  
  在上一课中,我们提到标签下的每一组语句都应该以break语句或return语句结尾。
在本课中,我们将探索原因,并讨论一些有时会绊倒新程序员的switch作用域问题。
Fallthrough机制
当switch表达式与case标签或可选的默认标签匹配时,执行从匹配标签之后的第一条语句开始。然后继续按顺序执行,直到发生以下终止条件之一:
- switch代码块结束
- 另一个控制流语句(通常是break或return)导致退出代码块或函数。
- 其它打断程序正常控制流的事情(操作系统杀死了对应的进程等其它原因)
请注意,另一个case标签的存在不是这些终止条件之一——因此,如果没有中断或返回,执行将溢出到后续的case情况中。
下面是一个显示此行为的程序:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 | #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;
}
 | 
 
该程序输出以下内容:
这可能不是我们想要的!当执行从标签下的语句流到后续标签下的语句时,这称为fallthrough。
由于很少需要有意进行fallthrough,因此许多编译器和代码分析工具将fallthrough标记为警告。
    警告
一旦case或默认标签下的语句开始执行,它们将溢出(fallthrough)到后续的case中。Break或return语句通常用于防止这种情况。
 
[[fallthrough]]属性
可以通过注释,来告诉其它开发人员,switch的fallthrough行为是有意设计的。虽然这对其他开发人员有效,但编译器和代码分析工具不知道如何解释注释,因此不会消除警告。
为了帮助解决这个问题,C++17添加了一个名为[[fallthrough]]的新属性。
属性是一种现代C++功能,它允许程序员向编译器提供有关代码的一些附加数据。要指定属性,请将属性名称放在双括号之间。属性不是语句——相反,它们几乎可以在上下文相关的任何地方使用。
下面的例子中,[[fallthrough]]属性修改null语句,以指示fallthrough是有意的(不应触发任何警告):
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 | #include <iostream>
int main()
{
    switch (2)
    {
    case 1:
        std::cout << 1 << '\n';
        break;
    case 2:
        std::cout << 2 << '\n'; // 这里开始执行
        [[fallthrough]]; // 有意的进行 fallthrough -- 注意这里的分号代表空语句
    case 3:
        std::cout << 3 << '\n'; // 这里也会执行到
        break;
    }
    return 0;
}
 | 
 
该程序打印:
并且它不应该生成任何关于fallthrough的警告。
    最佳实践
使用[[fallthrough]]属性(以及null语句)来指示有意的fallthrough。
 
连续case标签
您可以使用逻辑OR运算符将多个测试组合到单个语句中:
| 1
2
3
4
5
 | 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被多次求值。
通过按顺序放置多个case标签,可以使用switch语句执行类似的操作:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 | 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语句之后的第一个语句都返回true,因此如果任何case标签匹配,函数将返回true。
因此,我们可以“堆叠”case标签,以使所有这些case标签在之后共享相同的语句集。这不被认为是fallthrough行为,因此这里不需要使用注释或[[fallthrough]]。
switch语句中case的作用域
使用if语句,在if条件之后只能有一条语句,并且该语句被认为隐式地位于代码块内:
| 1
2
 | if (x > 10)
    std::cout << x << " is greater than 10\n"; // 该语句被认为隐式地位于代码块内
 | 
 
然而,对于switch语句,标签后的语句都作用于switch块。不会创建隐式块。
| 1
2
3
4
5
6
7
8
9
 | switch (1)
{
    case 1: // 不会创建隐式块
        foo(); // 在switch的作用域内,而不是case 1内
        break; // 在switch的作用域内,而不是case 1内
    default:
        std::cout << "default case\n";
        break;
}
 | 
 
在上面的示例中,case 1 标签和default标签之间的2条语句的作用域是switch块的一部分,而不是case 1 隐含的代码块。
case语句中的变量声明和初始化
您可以在case标签之前和之后声明或定义(但不能初始化)switch语句内的变量:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 | switch (1)
{
    int a; // okay: case标签之前可以声明变量
    int b{ 5 }; // 不合法: case 标签之前,不可以初始化变量
    case 1:
        int y; // okay 但不推荐
        y = 4; // okay: 赋值语句可以
        break;
    case 2:
        int z{ 4 }; // 不合法: 后面还有case标签,不允许初始化变量
        y = 5; // okay: y 在上面声明,所以这里可以赋值
        break;
    case 3:
        break;
}
 | 
 
在 case 1 中定义了变量y,在 case 2 中使用了它。switch内的所有语句都被视为同一作用域的一部分。因此,在 case 1 内声明或定义的变量可以在以后使用。
然而,变量的初始化,需要在运行时执行(因为需要将初始值设置给变量)。如果后续还有case标签,则不允许初始化变量(因为初始化可能被跳过,这将使变量未初始化)。在第一个case标签之前不允许初始化,因为这些语句永远不会执行,switch语句无法指定到它们。
如果case标签内需要定义和初始化新变量,最佳实践是在case语句下的显式块内进行定义和初始化:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 | switch (1)
{
    case 1:
    { // 这里有一个显示的代码块
        int x{ 4 }; // okay, 变量在一个新的代码块内初始化
        std::cout << x;
        break;
    }
    default:
        std::cout << "default case\n";
        break;
}
 | 
 
    最佳实践
如果定义case语句中使用的变量,请在case内的显示代码块中定义。
 
        
        
        