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


944

我一直想知道-为什么您不能在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语法的说明,请参阅 stackoverflow.com/questions/1180550/weird-switch-error-in-obj-c/...
强尼

通常,这是关于switch语句和标签(ABC :) 的非常好的阅读
以太

4
我会说``为什么变量不能在switch语句中初始化而不是声明'',因为仅声明变量只会在MSVC中给我警告。
ZoomIn

Answers:


1141

Case语句只是标签。这意味着编译器会将其解释为直接跳转到标签。在C ++中,这里的问题是范围之一。大括号将范围定义为switch语句中的所有内容。这意味着您将拥有一个范围,在该范围内将进一步执行跳过跳过初始化的代码。

解决此问题的正确方法是定义特定于该case语句的作用域,并在其中定义变量:

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

94
相对于打开新作用域-支持代码的可读性和一致性。在过去,您可能会自动获得一个“额外的”堆栈框架,但是现在对于任何体面的优化编译器来说都不应该如此。
高高的杰夫

10
我同意Jeff的观点-由于大多数人使用缩进样式,因此在阅读switch语句时“假定”范围太容易了。我自己的风格是,如果每种情况/默认值都超过一行,则始终为其打开一个新范围。
出价

39
workmad3-如果您不声明任何新变量,您能找到我的C ++编译器,该编译器将生成新的堆栈框架吗?您曾为我担心过,但G ++ 3.1,Visual C ++ 7或Intel C ++ 8不会为您不声明任何变量的新作用域生成任何代码。
克里斯·杰斐逊

10
@ workmad3通过输入新的大括号块不会导致新的堆栈帧stackoverflow.com/questions/2759371/...
MTVS

3
@TallJef我不知道您指的是“过去的日子”。我从未遇到过40年后没有为该方法分配所有堆栈空间的编译器。
user207421 '17

331

这个问题最初标记为[C]和[C ++]在同一时间。原始代码确实在C和C ++中均无效,但是出于完全不同的不相关原因。

  • 在C ++中,此代码无效,因为case ANOTHER_VAL:标签newVal跳过了初始化而跳入了变量的范围。在C ++中,绕过自动对象初始化的跳转是非法的。大多数回答正确地解决了问题的这一方面。

  • 但是,在C语言中,绕过变量初始化不是错误。在C语言中,在初始化变量时跳入变量的范围是合法的。这仅表示该变量未初始化。出于完全不同的原因,原始代码无法在C中编译。case VAL:原始代码中的标签附加在变量的声明中newVal。在C语言中,声明不是语句。它们不能被标记。这就是当将此代码解释为C代码时导致错误的原因。

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

添加一个额外的{}块可以解决C ++和C问题,即使这些问题恰好有很大不同。在C ++方面,它限制的范围newVal,确保case ANOTHER_VAL:不再跳入该范围,从而消除了C ++问题。在C方面,额外{}引入了复合语句,从而使case VAL:标签适用于语句,从而消除了C问题。

  • 在C的情况下,不用容易解决问题{}。只需在case VAL:标签后添加一个空语句,代码就会生效

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

    请注意,即使从C的角度来看它现在是有效的,但从C ++的角度来看它仍然是无效的。

  • 对称地,在C ++情况下,无需使用即可轻松解决问题{}。只需从变量声明中删除初始化程序,代码将变为有效

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

    请注意,即使从C ++角度来看它现在是有效的,但从C角度来看它仍然是无效的。


4
@AnT:我了解为什么修复C ++的人不适用于C;为什么?但是,我无法理解它如何解决首先跳过初始化的C ++问题?它是否还会跳过newVal跳转到时的声明和赋值ANOTHER_VAL
legends2k

13
@ legends2k:是的,它仍然跳过它。但是,当我说“它可以解决问题”时,是指它可以解决C ++编译器错误。在C ++中,使用初始值设定项跳过标量声明是非法的,但是在没有初始值设定项的情况下跳过标量声明是完全可以的。此时case ANOTHER_VAL:变量newVal是可见的,但值不确定。
AnT

3
迷人。§A9.3: Compound Statement从K&R C(第二版)阅读后,我发现了这个问题。条目提到复合语句的技术定义为{declaration-list[opt] statement-list[opt]}。感到困惑,因为我以为声明只是一个声明,所以我查找了一下并立即发现了这个问题,这个例子很明显,实际上是在破坏程序。我相信(针对C)的另一种解决方案是在声明之前放置另一条语句(可能为null语句?)以便满足标记语句
Braden Best

糟糕,我刚刚注意到您提出的空语句解决方案已经存在。没关系。
Braden Best,

3
值得注意的是,添加空语句的修复仅适用于C99及更高版本。在C89中,变量必须在其封闭块的开头声明。
亚瑟·塔卡

136

好。仅仅为了澄清这一点与声明无关。它仅与“跳过初始化”有关(ISO C ++ '03 6.7 / 3)

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

switch (i)
{
   case 0:
     int j; // 'j' has indeterminate value
     j = 0; // 'j' 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或聚合,则编译器会隐式添加一个初始化程序,因此不可能跳过这样的声明:

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中,跳过初始化不是错误。

正如其他人提到的那样,解决方案是添加一个嵌套块,以使变量的生存期限于单个大小写标签。


2
“错误跳过初始化” ???不与我的海湾合作委员会。在标签下使用j时,它可能会发出“ j可能被统一使用”警告,但没有错误。但是,在切换的情况下,会出现错误(硬错误,不是弱警告)。
Mecki'2

9
@Mecki:在C ++中是非法的。ISO C ++ '03-6.7 / 3:“ ...一个程序将从具有自动存储持续时间的局部变量不在范围内的点跳转到其范围不正确的点,除非该变量具有POD类型(3.9),并且没有初始化程序(8.5)进行声明。”
理查德·科登

1
是的,但是在C语言中这不是非法的(至少gcc表示它不是非法的)。j将被初始化(具有一些随机数),但是编译器会对其进行编译。但是,在使用switch语句的情况下,编译器甚至不会编译它,而且我看不到goto / label情况与switch情况之间的区别。
Mecki 2010年

8
@Mecki:通常,单个编译器的行为并不一定反映该语言实际上允许的行为。我已经检查了C'90和C'99,并且两个标准都包含了一个在switch语句中跳过初始化的示例。
理查德·科登

38

整个switch语句在同一范围内。要解决此问题,请执行以下操作:

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

    case ANOTHER_VAL:
      ...
    break;
}

注意括号。


30

阅读所有答案和更多研究后,我得到了一些东西。

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 ++中,根据规范,

允许带标签的声明,但不允许带标签的初始化。

参见http://codepad.org/ZmQ0IyDG


解决这种情况的方法是两个

  1. 使用{}使用新范围

    case 1:
           {
               int x=10;
               printf(" x is %d", x);
           }
    break;
  2. 或使用带有标签的伪语句

    case 1: ;
               int x=10;
               printf(" x is %d",x);
    break;
  3. 如果满足您的要求,则在switch()之前声明变量,并在case语句中使用不同的值对其进行初始化

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

带有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 ++不允许的是将变量的初始化跳过到 variable a的范围内a。因此,从C的角度来看,问题出在case VAL:标签上,而您正确地描述了它。但是从C ++的角度来看,问题出在case ANOTHER_VAL:标签上。
AnT

与C语言不同,在C ++中,声明是语句的子集。
基思·汤普森

20

您不能这样做,因为case标签实际上只是包含块中的入口点。

Duff的设备最清楚地说明了这一点。这是Wikipedia的一些代码:

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类型,因此%必须执行真正的除法/模运算。将计数设为无符号(或者更好的是,始终将size_t用于计数/索引),问题就消失了。
R .. GitHub停止帮助ICE,2010年

1
@R ..:什么?!在二进制补码系统中,只要您的处理器体系结构具有算术右移运算,则有符号性不会影响2的幂的模数(在最低位只是一个AND),也不会影响2的幂的除法。 (SAR在x86中,与SHR无符号移位相对)。
克里斯·杰斯特·杨

@Chris:我相信他的意思是,当编译器必须允许负值的情况下,“仅在最低位与”不成立。例如,-1%8使用g ++在该二进制补码系统上给出-1(在这种情况下,符号是根据5.6 / 4定义的实现)。

3
@克里斯:我同意你的看法,R正在扩大影响;我只看到您的评论,并且知道一个简单的操作还不够。

1
同样值得注意的是,原始的Wikipedia代码是用于将数据发送到内存映射的输出的,在这里看起来很奇怪,因为未提及它,并且每个字节都复制到了相同的“ to”位置。可以通过向其添加后缀++来解决该问题,或者提及用例是针对内存映射的IO。完全围绕原始问题:-)。
彼得

16

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

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

如前所述,解决此问题的一种好方法是使用花括号为您的案例创建一个范围。


1
32号先生,您误解了错误的所在:是的,这不会编译,但不是因为您在开关内声明了一个变量。这个错误是因为你想声明的声明后的变量,这是C.非法
MrZebra

1
现在在c90和较新版本的c中是合法的日子
Jeegar Patel 2011年

12

我最喜欢的邪恶开关技巧是使用if(0)跳过不需要的大小写标签。

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

但是非常邪恶。


非常好。原因示例:案例0和案例1可能会例如以不同的方式初始化变量,然后在案例2中使用该变量
。– hlovdal

1
如果您希望案例0和案例1都通过案例2落入(不让案例0通过案例1落入)。不知道它是否真的有用,但肯定可以。
Petruza 2011年

1
您可以跳转到所需的标签而goto不会混淆代码
SomeWittyUsername


7

如果开始新的块,可以在switch语句中声明变量:

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

原因与在堆栈上分配(和回收)空间以存储局部变量有关。


1
可以声明该变量,但不能初始化。另外,我非常确定该问题与堆栈和局部变量无关。
理查德·科登

6

考虑:

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

在没有break语句的情况下,有时newVal会被声明两次,并且您不知道它是否在运行时才声明。我的猜测是这种局限性是由于这种混乱造成的。newVal的范围是什么?按照惯例,它将是整个开关块(在大括号之间)。

我不是C ++程序员,但是在C语言中:

switch(val) {
    int x;
    case VAL:
        x=1;
}

工作良好。在switch块内声明一个变量就可以了。不是在事后保卫人员声明。


3
@ Mr.32:实际上,您的示例显示未执行printf,但是在这种情况下,int x不是语句而是声明,声明x,每次堆叠函数环境时都为其保留空间,参见:codepad.org/4E9Zuz1e
Petruza 2011年

我希望在阅读问题的标题时会发现此问题,因为问题不是关于在“ case:”标签中声明变量,而是在switch语句中声明变量。而且只有您(和VictorH,强调您的答案)实际上在谈论switch语句中的变量。
cesss

4

开关的整个部分都是单个声明上下文。您不能在这样的case语句中声明变量。尝试以下方法:

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

可以声明该变量,但不能初始化。
理查德·科登

@Richard Corden我相信初始化会起作用。您是否仍然断言它无法初始化?
chux-恢复莫妮卡

3

如果您的代码说“ int newVal = 42”,那么您可以合理地期望newVal永远不会被初始化。但是,如果转到该语句(这是您正在做的事情),那么这就是发生的情况-newVal在作用域内,但尚未分配。

如果那是您真正要发生的事情,那么该语言需要通过说“ int newVal; newVal = 42;”来使其明确。否则,您可以将newVal的范围限制为单个情况,这很可能是您想要的。

如果考虑相同的示例,但使用“ const int newVal = 42;”,可能会澄清一些问题。


3

我只是想强调苗条观点。开关构造可创建一个完整的一流公民范围。因此可以在第一个case标签之前的switch语句中声明(并初始化)变量,而无需附加的括号对:

switch (val) {  
  /* This *will* work, even in C89 */
  int newVal = 42;  
case VAL:
  newVal = 1984; 
  break;
case ANOTHER_VAL:  
  newVal = 2001;
  break;
}

-1这里int newVal = 42; 将永远不会被执行。看到此codepad.org/PA1quYX3
Jeegar Patel

4
声明int newVal 被执行,而不是= 42赋值。
Petruza 2011年

3

到目前为止,答案一直是C ++。

对于C ++,您不能跳过初始化。您可以在C中使用。但是,在C中,声明不是语句,并且大小写标签后面必须带有语句。

因此,有效(但难看)的C,无效的C ++

switch (something)
{
  case 1:; // Ugly hack empty statement
    int i = 6;
    do_stuff_with_i(i);
    break;
  case 2:
    do_something();
    break;
  default:
    get_a_life();
}

相反,在C ++中,声明是一条语句,因此以下内容是有效的C ++,无效的C

switch (something)
{
  case 1:
    do_something();
    break;
  case 2:
    int i = 12;
    do_something_else();
}

1
第二个示例是无效的C ++(使用vc2010和gcc 4.6.1 C ++测试不允许跳过初始化部分。gcc错误消息是:'int i'的交叉初始化
zhaorufei 2011年

3

有趣的是,这很好:

switch (i)  
{  
case 0:  
    int j;  
    j = 7;  
    break;  

case 1:  
    break;
}

...但这不是:

switch (i)  
{  
case 0:  
    int j = 7;  
    break;  

case 1:  
    break;
}

我知道修复很简单,但是我还不明白为什么第一个示例不会打扰编译器。正如前面提到的(早两年),尽管有逻辑,但声明不是导致错误的原因。初始化是问题所在。如果在不同的行上初始化并声明了变量,则会对其进行编译。


1
首先在gcc 4.2上效果不佳:“错误:'int'之前的预期表达式”。正如彼得和Mr.32所说,“情况0 :; int j; ...”和“情况0 :; int j = 7; ...”都可以起作用。C中的问题仅在于“ case <label>:clarification”不是有效的C语法。
dubiousjim 2012年

3

我最初是为这个问题写的这个答案。但是,当我完成它时,我发现答案已经关闭。所以我在这里发布了它,也许喜欢标准参考的人会发现它很有帮助。

有问题的原始代码:

int i;
i = 2;
switch(i)
{
    case 1: 
        int k;
        break;
    case 2:
        k = 1;
        cout<<k<<endl;
        break;
}

实际上有两个问题:

1.为什么之后我要声明一个变量 case标签?

这是因为在C ++中,标签必须采用以下形式:

N3337 6.1 / 1

标记陈述:

...

  • attribute-specifier-seqopt case constant-expressionstatement

...

并且在C++ 声明中的声明也被视为声明(而不是C):

N3337 6/1:

声明

...

声明书

...

2.为什么我可以跳过变量声明然后使用它?

因为:N3337 6.7 / 3

可以转移到一个块中,但是不能以一种初始化绕过声明的方式。跳转的程序(switch语句的条件到case标签转移在这方面被视为跳转。)

从具有自动存储持续时间的变量不在范围内的点到其处于范围内的点的 格式不正确,除非该变量具有scalar type,具有普通默认构造函数和琐碎析构函数的类类型(具有cv限定版本)其中一种类型的数组,或者上述类型之一的数组,并且在没有初始化程序(8.5)的情况下进行声明。

由于k标量类型,并且未在声明时进行初始化,因此可以跳过声明。这在语义上是等效的:

goto label;

int x;

label:
cout << x << endl;

但是,如果x在声明时进行了初始化,那将是不可能的:

 goto label;

    int x = 58; //error, jumping over declaration with initialization

    label:
    cout << x << endl;

1

新变量只能在块范围内声明。您需要编写如下内容:

case VAL:  
  // This will work
  {
  int newVal = 42;  
  }
  break;

当然,newVal仅在括号内有作用域...

干杯,拉尔夫


1

switch是不一样的连续if/else if块。我很惊讶,没有其他答案可以清楚地解释它。

考虑以下switch语句:

switch (value) {
    case 1:
        int a = 10;
        break;
    case 2:
        int a = 20;
        break;
}

可能令人惊讶,但是编译器不会将其视为简单的if/else if。它将产生以下代码:

if (value == 1)
    goto label_1;
else if (value == 2)
    goto label_2;
else
    goto label_end;

{
label_1:
    int a = 10;
    goto label_end;
label_2:
    int a = 20; // Already declared !
    goto label_end;
}

label_end:
    // The code after the switch block

case声明被转换成标签,然后用所谓的goto。方括号创建了一个新的作用域,现在可以很容易地看出为什么不能在同一个变量中声明两个具有相同名称的变量switch块中。

它看起来可能很奇怪,但是有必要支持失败检查(即,不要break用来让执行继续到下一个case)。


0

我认为当前的问题是该语句已被跳过,并且您尝试在其他地方使用var,因此不会对其进行声明。


0

newVal存在于整个开关范围内,但是仅在VAL肢被击中时才初始化。如果您在VAL中的代码周围创建一个块,则应该没问题。


0

C ++ Standard具有:可以传输到块中,但不能以初始化绕过声明的方式进行。从具有自动存储持续时间的局部变量不在范围内的点跳转到其处于范围内的点的程序是错误的,除非该变量具有POD类型(3.9)且声明时没有初始化程序(8.5)。

用来说明此规则的代码:

#include <iostream>

using namespace std;

class X {
  public:
    X() 
    {
     cout << "constructor" << endl;
    }
    ~X() 
    {
     cout << "destructor" << endl;
    }
};

template <class type>
void ill_formed()
{
  goto lx;
ly:
  type a;
lx:
  goto ly;
}

template <class type>
void ok()
{
ly:
  type a;
lx:
  goto ly;
}

void test_class()
{
  ok<X>();
  // compile error
  ill_formed<X>();
}

void test_scalar() 
{
  ok<int>();
  ill_formed<int>();
}

int main(int argc, const char *argv[]) 
{
  return 0;
}

显示初始化效果的代码:

#include <iostream>

using namespace std;

int test1()
{
  int i = 0;
  // There jumps fo "case 1" and "case 2"
  switch(i) {
    case 1:
      // Compile error because of the initializer
      int r = 1; 
      break;
    case 2:
      break;
  };
}

void test2()
{
  int i = 2;
  switch(i) {
    case 1:
      int r;
      r= 1; 
      break;
    case 2:
      cout << "r: " << r << endl;
      break;
  };
}

int main(int argc, const char *argv[]) 
{
  test1();
  test2();
  return 0;
}

0

似乎可以在switch case语句中声明或创建匿名对象,原因是它们无法被引用,因此不能陷入下一种情况。考虑此示例在GCC 4.5.3和Visual Studio 2008上编译(可能是合规性问题,因此请专家们权衡一下)

#include <cstdlib>

struct Foo{};

int main()
{
    int i = 42;

    switch( i )
    {
    case 42:
        Foo();  // Apparently valid
        break;

    default:
        break;
    }
    return EXIT_SUCCESS;
}

如果您要投反对票,请解释原因。我很好奇,为什么创建一个匿名对象似乎是一种豁免。
奥卢米德(Olumide)

1
不是DV,而是:整个问题都与声明/范围内的命名变量有关。临时(“匿名对象”不是术语)既不是命名变量,也不是声明,也不受范围的约束(除非绑定到具有const自身范围的引用)。这是一个在其语句内(无论可能在哪里)生存和死亡的表达。因此,这是完全不相关的。
underscore_d

Foo();不是声明;问题是关于声明。
MM
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.