在case语句中使用{}.为什么?

Using {} in a case statement. Why?

本文关键字:为什么 case 语句      更新时间:2023-10-16

case语句中使用{}有什么意义?通常,无论case语句中有多少行,所有的行都会被执行。这只是一条关于较旧/较新编译器的规则,还是有其背后的原因?

int a = 0;
switch (a) {
  case 0:{
    std::cout << "line1n";
    std::cout << "line2n";
    break;
  }
}

int a = 0;
switch (a) {
  case 0:
    std::cout << "line1n";
    std::cout << "line2n";
    break;
}

{}表示作用域的新块。

考虑以下非常做作的例子:

switch (a)
{
    case 42:
        int x = GetSomeValue();
        return a * x;
    case 1337:
        int x = GetSomeOtherValue(); //ERROR
        return a * x;
}

您将得到一个编译器错误,因为x已在作用域中定义。

将它们分离到自己的子范围将消除在switch语句之外声明x的需要。

switch (a)
{
    case 42: {
        int x = GetSomeValue();
        return a * x; 
    }
    case 1337: {
        int x = GetSomeOtherValue(); //OK
        return a * x; 
    }
}

警告:

声明和初始化case内部的变量而不包围{}是错误的:

#include <iostream>
using namespace std;
int main() {
    int b = 3;
    switch (b) {
    case 3:
        int a = 3; //compilation error: "initialization of 'a' skipped by 'case' label"
        return a * b;
    case 1:
        return a * b;
    }
}

TL;DR

事例中用初始化器或一些非平凡对象声明变量的唯一方法是使用{}或其他具有自己作用域的控制结构(如循环if语句)引入块作用域

华丽细节

我们可以看到,用例只是标记的语句,就像goto语句中使用的标签一样(这在C++标准草案第6.1节"标记的语句"中有介绍),我们可以从6.7节第3段中看到,在许多情况下都不允许跳过声明,包括那些具有初始化的情况:

可以转移到块中,但不能通过初始化绕过声明。从一个具有自动存储持续时间的变量不在作用域内的点跳到它在作用域中的点的87程序是格式错误的,除非该变量具有标量类型、具有平凡默认构造函数和平凡析构函数的类类型、这些类型之一的cv限定版本,或者前面类型之一的数组,并且在没有初始化器的情况下声明(8.5)

并提供了这个例子:

void f() {
 // ...
 goto lx; // ill-formed: jump into scope of a
 ly:
  X a = 1;
 // ...
 lx:
  goto ly; // OK, jump implies destructor
          // call for a followed by construction
          // again immediately following label ly
}

注意,这里有一些微妙之处,您可以跳过没有初始化的标量声明,例如:

switch( n ) 
{
    int x ;
    //int x  = 10 ; 
    case 0:
      x = 0 ;
      break;
    case 1:
      x = 1 ;
      break;
    default:
      x = 100 ;
      break ;
}

是完全有效的(实例)。当然,如果你想在每个情况下声明相同的变量,那么它们都需要自己的作用域,但在开关语句之外,它的工作方式也是一样的,所以这应该不会太令人惊讶。

至于不允许跳过初始化的理由,缺陷报告467虽然涵盖了一个略有不同的问题,但为自动变量提供了合理的理由:

[…]如果未显式初始化,自动变量可能具有不确定("垃圾")值,包括陷阱表示,[…]

更有趣的可能是,您将开关中的范围扩展到多个情况。最著名的例子可能是Duff的设备,它看起来像这样:

void send( int *to, const int *from, int  count)
{
        int n = (count + 7) / 8;
        switch(count % 8) 
        {
            case 0: do {    *to = *from++;   // <- Scope start
            case 7:         *to = *from++;
            case 6:         *to = *from++;
            case 5:         *to = *from++;
            case 4:         *to = *from++;
            case 3:         *to = *from++;
            case 2:         *to = *from++;
            case 1:         *to = *from++;
                        } while(--n > 0);    // <- Scope end
        }
}

这是一种习惯,允许您将带有结果析构函数(或范围冲突)的变量声明注入case子句。另一种观点是,他们正在为他们希望拥有的语言写作,其中所有的流控制都由块而不是语句序列组成。

检查一下这是一个基本的编译器限制,你会开始想知道发生了什么:

int c;
c=1;
switch(c)
{
    case 1:
    //{
        int *i = new int;
        *i =1;
        cout<<*i;
        break;
    //}
    default : cout<<"def";
}

这会给你一个错误:

error: jump to case label [-fpermissive]
error:   crosses initialization of ‘int* i’

而这个不会:

int c;
c=1;
switch(c)
{
    case 1:
    {
        int *i = new int;
        *i =1;
        cout<<*i;
        break;
    }
    default : cout<<"def";
}

在开关中使用括号表示Rotem所说的新范围块。

但当你阅读时,为了清晰起见,它也可以。要知道案例在哪里停止,因为它可能有条件中断。

原因可能是:

  1. 可读性,它在视觉上增强了每个案例的范围部分
  2. 为多个开关情况声明具有相同名称的不同变量
  3. 微观优化-一个非常昂贵的资源分配变量的范围,您希望在离开案例范围后立即销毁该变量,甚至是"GOTO"命令使用的更为复杂的场景