为什么不能在switch语句中声明变量?

1124
我一直想知道这个问题 - 为什么在switch语句的一个case标签后面不能声明变量呢?在C++中,你可以在几乎任何地方声明变量(而且在第一次使用前声明它们显然是一个好习惯),但是下面的代码仍然不会起作用:
switch (val)  
{  
case VAL:  
  // This won't work
  int newVal = 42;  
  break;
case ANOTHER_VAL:  
  ...
  break;
}  

上述代码出现了以下错误(MSC):
初始化“newVal”的操作被“case”标签跳过。
这似乎在其他语言中也存在限制。为什么会成为问题呢?

10
有关C BNF语法的解释,请参见https://dev59.com/AnM_5IYBdhLWcg3w4nnb#1181106%3E。 - johne
这是一篇关于switch语句和标签(ABC:)的好文章,值得一读:http://complete-concrete-concise.com/programming/c/keyword-switch-case-default。 - Etherealone
5
我会说,“为什么变量不能在switch语句中初始化而不是声明”。因为只声明变量在MSVC中只会给我一个警告。 - ZoomIn
4
如果你将case标签内的所有内容都放在花括号{}里,那么它就能正常工作。 - E Purdy
23个回答

1337
Case语句仅仅是标签。这意味着编译器将其解释为直接跳转到标签。在C++中,问题在于作用域。花括号定义了switch语句内的全部作用域。这意味着你留下了一个作用域,在其中会执行进一步跳转以跳过初始化。
正确处理这种情况的方法是为该case语句定义一个特定的作用域,并在其中定义变量:
switch (val)
{   
case VAL:  
{
  // This will work
  int newVal = 42;  
  break;
}
case ANOTHER_VAL:  
...
break;
}

4
@TallJef,我不知道你指的是哪个“旧时代”。在过去40年内,我从未遇到过分配方法堆栈空间时不会分配全部空间的编译器。 - user207421
@EJP:当使用_alloca()时,编译器无法在进入时知道需要多少空间,因此必须进行逐步调整。 - Ben Voigt
2
我在IAR编译器中遇到了一个奇怪的情况。在case语句中有一个数组(具有作用域),但是无论进入哪个case,内存都会被分配,只要进入函数即可。由于其他case导致堆栈比这个更深,最终导致堆栈溢出。 - Do-do-new
#define SCOPE(code) & DECLSPEC_NOINLINE { code }() - Stephen Eckels
1
@StephenEckels:请不要这样做。IIFE 不是 C++ 的惯用语,而且将其隐藏在宏中更糟糕。所有程序员对 break;return 等的期望都被违反了。 - Ben Voigt
显示剩余3条评论

454

这个问题最初被同时标记为。原始代码在C和C++中都是无效的,但原因完全不相关。

  • In C++ this code is invalid because the case ANOTHER_VAL: label jumps into the scope of variable newVal bypassing its initialization. Jumps that bypass initialization of automatic objects are illegal in C++. This side of the issue is correctly addressed by most answers.

  • However, in C language bypassing variable initialization is not an error. Jumping into the scope of a variable over its initialization is legal in C. It simply means that the variable is left uninitialized. The original code does not compile in C for a completely different reason. Label case VAL: in the original code is attached to the declaration of variable newVal. In C language declarations are not statements. They cannot be labeled. And this is what causes the error when this code is interpreted as C code.

      switch (val)  
      {  
      case VAL:             /* <- C error is here */
        int newVal = 42;  
        break;
      case ANOTHER_VAL:     /* <- C++ error is here */
        ...
        break;
      }
    

    Adding an extra {} block fixes both C++ and C problems, even though these problems happen to be very different. On the C++ side it restricts the scope of newVal, making sure that case ANOTHER_VAL: no longer jumps into that scope, which eliminates the C++ issue. On the C side that extra {} introduces a compound statement, thus making the case VAL: label to apply to a statement, which eliminates the C issue.

  • In C case the problem can be easily solved without the {}. Just add an empty statement after the case VAL: label and the code will become valid

      switch (val)  
      {  
      case VAL:;            /* Now it works in C! */
        int newVal = 42;  
        break;
      case ANOTHER_VAL:  
        ...
        break;
      }
    

    Note that even though it is now valid from C point of view, it remains invalid from C++ point of view.

  • Symmetrically, in C++ case the the problem can be easily solved without the {}. Just remove the initializer from variable declaration and the code will become valid

      switch (val)  
      {  
      case VAL: 
        int newVal;
        newVal = 42;  
        break;
      case ANOTHER_VAL:     /* Now it works in C++! */
        ...
        break;
      }
    

    Note that even though it is now valid from C++ point of view, it remains invalid from C point of view.

从C23开始,C语言中的所有标签都将被解释为隐含的空语句标记(N2508),即无法在C语言中在声明前放置标签的问题将不再存在,上述基于;的修复方法也将不再必要。

6
@AnT: 我理解为什么修复 C++ 的方法不适用于 C;但我不明白它如何修复 C++ 中跳过初始化的问题?当它跳到“ANOTHER_VAL”时,它不仍然会跳过“newVal”的声明和赋值吗?答案:我了解为什么C++中的修复方法不能适用于C语言;但是,我无法理解它如何解决跳过初始化的C ++问题。当代码跳转到“ANOTHER_VAL”时,“newVal”的声明和赋值仍然会被跳过吗? - legends2k
20
@legends2k: 是的,它仍然跳过了它。但是,当我说“它修复了问题”时,我的意思是它修复了C++编译器错误。在C++中,跳过带初始化程序的标量声明是非法的,但是跳过不带初始化程序的标量声明是完全可以的。在case ANOTHER_VAL:点,变量newVal是可见的,但具有不确定的值。 - AnT stands with Russia
3
有趣。我在阅读K&R C(第二版)的§A9.3:Compound Statement后找到了这个问题。该条目提到了compound-statement的技术定义,即{declaration-list[opt] statement-list[opt]}。 由于我曾认为声明就是语句,所以感到困惑,但是我查找后立刻发现了这个问题,其中展示了这种差异变得明显并实际上破坏了程序的示例。 我认为另一个解决方案(针对C语言)是在声明之前放置另一个语句(可能是空语句?),以便满足labeled-statement - Braden Best
1
哎呀,我刚刚注意到我建议的空语句解决方案已经在你的答案中了。那就算了吧。 - Braden Best
6
值得注意的是,在 C99 及以上版本中,添加一个空语句的修复方法才有效。在 C89 中,变量必须在其封闭块的开头声明。 - Arthur Tacca
显示剩余5条评论

145

好的。仅仅为了澄清,这与声明严格没有任何关系。它只涉及到“跳过初始化”(ISO C++ '03 6.7/3)。

这里有很多帖子提到跳过声明可能会导致变量“未被声明”。这是不正确的。POD对象可以在没有初始化器的情况下声明,但它将具有不确定的值。例如:

switch (i)
{
   case 0:
     int j; // 'j' has indeterminate value
     j = 0; // 'j' set (not initialized) to 0, but this statement
            // is jumped when 'i == 1'
     break;
   case 1:
     ++j;   // 'j' is in scope here - but it has an indeterminate value
     break;
}

如果对象是非POD或聚合类型,编译器会隐式添加一个初始化程序,因此不可能跳过这样的声明:

如果对象是非POD或聚合类型,编译器会隐式添加一个初始化程序,因此不可能跳过这样的声明:

class A {
public:
  A ();
};

switch (i)  // Error - jumping over initialization of 'A'
{
   case 0:
     A j;   // Compiler implicitly calls default constructor
     break;
   case 1:
     break;
}

这个限制不仅适用于switch语句。使用'goto'跳过初始化也是错误的:

goto LABEL;    // Error jumping over initialization
int j = 0; 
LABEL:
  ;

有趣的小知识是,这是C++和C之间的差异。在C中,跳过初始化不会导致错误。

正如其他人提到的,解决方法是添加一个嵌套块,以便变量的生命周期仅限于个别的case标签。


2
“Error jumping over initialization”???不是我的GCC。当在标签下使用j时,它可能会给出“j可能未初始化”的警告,但没有错误。然而,在switch的情况下,会出现错误(硬错误,而不是弱警告)。 - Mecki
9
在C++中这是不合法的。根据ISO C++ '03标准的第6.7/3条规定:“……除非变量具有POD类型(3.9)且未使用初始化程序(8.5)声明,否则从局部自动存储持续时间不在作用域的点跳转到它在作用域内的点是非法的。” - Richard Corden
1
是的,但在C语言中并不违法(至少gcc表示不违法)。j将未初始化(具有某些随机数),但编译器会编译它。然而,在switch语句的情况下,编译器甚至不会编译它,我看不出goto/label case和switch case之间的区别。 - Mecki
8
通常情况下,单个编译器的行为并不一定反映出语言实际允许的内容。我已经查了C'90和C'99两个标准,两个标准都包括了一个在switch语句中跳过初始化的示例。 - Richard Corden

44

整个 switch 语句在同一个作用域中。为了解决这个问题,请执行以下操作:

switch (val)
{
    case VAL:
    {
        // This **will** work
        int newVal = 42;
    }
    break;

    case ANOTHER_VAL:
      ...
    break;
}

请注意括号。


36

在阅读了所有答案并进行了更多的研究后,我得到了一些结论。

Case statements are only 'labels'

根据规范,在C语言中,

§6.8.1 标记语句:

labeled-statement:
    identifier : statement
    case constant-expression : statement
    default : statement

在C语言中,没有任何允许“标记声明”的子句。这不是该语言的一部分。

因此

case 1: int x=10;
        printf(" x is %d",x);
break;

这段代码不能编译,请参考http://codepad.org/YiyLQTYw。GCC会报错:

label can only be a part of statement and declaration is not a statement

即使

  case 1: int x;
          x=10;
            printf(" x is %d",x);
    break;

这里也无法编译,参见http://codepad.org/BXnRD3bu


根据C++规范,

允许使用标记声明(labeled-declaration),但不允许使用标记初始化(labeled-initialization)。

请参见http://codepad.org/ZmQ0IyDG


解决此问题有两种方法:

  1. 可以使用新的作用域来解决这个问题,即使用 {}。

    case 1:
           {
               int x=10;
               printf(" x is %d", x);
           }
    break;
    
    或者使用带标签的虚拟语句。
    case 1: ;
               int x=10;
               printf(" x is %d",x);
    break;
    
  2. 在 switch() 语句之前声明变量,并在 case 语句中使用不同的值进行初始化,如果这符合您的要求。

  3. main()
    {
        int x;   // Declare before
        switch(a)
        {
        case 1: x=10;
            break;
    
        case 2: x=20;
            break;
        }
    }
    

使用 switch 语句的一些注意事项

不要在 switch 语句中编写任何不属于任何标签的语句,因为它们永远不会被执行:

switch(a)
{
    printf("This will never print"); // This will never executed

    case 1:
        printf(" 1");
        break;

    default:
        break;
}

请参见http://codepad.org/PA1quYX3


2
你正确地描述了C问题。但是声称在C++中不允许标记初始化完全是不正确的。在C++中,标记初始化没有任何问题。C++不允许跳过变量a的初始化进入变量a的作用域。因此,从C的角度来看,问题出在case VAL:标签上,你正确地描述了它。但从C++的角度来看,问题出在case ANOTHER_VAL:标签上。 - AnT stands with Russia
在C++中,与C不同的是,声明是语句的子集。 - Keith Thompson

21

由于 case 标签实际上只是进入包含块的入口点,因此您无法做到这一点。

这一点最清楚地体现在达夫设备上。以下是维基百科上的一些代码:

strcpy(char *to, char *from, size_t count) {
    int n = (count + 7) / 8;
    switch (count % 8) {
    case 0: do { *to = *from++;
    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);
    }
}

注意到case标签完全忽略了块边界。是的,这很邪恶。但这就是为什么你的代码示例不起作用的原因。跳转到case标签就像使用goto一样,因此你不能跳过带有构造函数的局部变量。

正如其他几位发帖者指出的那样,你需要加入自己的块:

switch (...) {
    case FOO: {
        MyObject x(...);
        ...
        break; 
    }
    ...
 }

1
这个Duff设备的实现存在一个错误,使其变得非常缓慢:count是int类型,因此%必须执行实际的除法/模操作。将count设置为无符号(或者更好的方法是始终使用size_t进行计数/索引),问题就会消失。 - R.. GitHub STOP HELPING ICE
1
@R..:什么?在二进制补码系统中,有符号位并不影响2的幂次方取模(只是对底部位进行AND运算),只要你的处理器架构具有算术右移操作(x86中的SAR,而无符号移位SHR则用于无符号移位),也不会影响2的幂次方除法。 - C. K. Young
@Roger:是的,编译器必须支持负值,并且这会增加额外的周期(因此无符号仍然更理想),但在x86上(我进行了测试),gcc仍然不会生成DIV指令。它执行的相当于:num < 0 ? ((num + 7) & 7) - 7 : num & 7(但完全没有分支、乘法或除法)。因此,它绝不是“非常慢”的。 - C. K. Young
3
@Chris:我同意你的观点,R夸大了影响。我看到你的评论后就知道一个简单的AND是不够的。 - Roger Pate
1
值得注意的是,原始的维基百科代码用于将数据发送到内存映射输出,这在这里看起来很奇怪,因为它没有被提及,每个字节都被复制到相同的“to”位置。可以通过向“to”添加后缀++或提及用例用于内存映射IO来解决这个问题。完全与原始问题无关 :-). - Peter
显示剩余4条评论

16

到目前为止,大部分回答在一个方面都是错误的:你可以在case语句之后声明变量,但你不能初始化它们:

case 1:
    int x; // Works
    int y = 0; // Error, initialization is skipped by case
    break;
case 2:
    ...

正如之前提到的,一个不错的解决方法是使用花括号为您的案例创建一个作用域。


1
32先生,您误解了您的错误:是的,那不会编译,但不是因为您在switch内声明变量。错误是因为您试图在语句后声明变量,在C中这是非法的。 - Zebra North
1
现在,在C90和更新版本的C中,这是合法的。 - Jeegar Patel

13

我最喜欢的邪恶开关技巧是使用if(0)来跳过不需要执行的case标签。

switch(val)
{
case 0:
// Do something
if (0) {
case 1:
// Do something else
}
case 2:
// Do something in all cases
}

但非常邪恶。


非常好。为什么这样说呢?例如,case 0和case 1可能会以不同的方式初始化一个变量,然后在case 2中使用。 - hlovdal
1
如果您想让case 0和case 1都进入case 2(而不是case 0进入case 1),这个方法可能有用,但并不确定。 - Petruza
1
你可以使用 goto 直接跳转到所需的标签,而不会使代码混乱。 - SomeWittyUsername
非常阴险。我祝贺你。 - KeyC0de

10

试试这个:

switch (val)
{
    case VAL:
    {
        int newVal = 42;
    }
    break;
}

7
您可以在 switch 语句中声明变量,只要您开始一个新的代码块:

if


switch (thing)
{ 
  case A:
  {
    int i = 0;  // Completely legal
  }
  break;
}

原因是为了在栈上分配(和回收)空间来存储局部变量。

1
变量可以声明,但不能初始化。此外,我非常确定问题与堆栈和本地变量无关。 - Richard Corden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接