2008-09-18 4 views
752

Я всегда удивлялся этому - почему вы не можете объявлять переменные после метки случая в инструкции switch? В C++ можно объявлять переменные почти везде (и объявив их близко к первому использованию, очевидно, хорошая вещь), но следующее еще не будет работать:Почему переменные не могут быть объявлены в инструкции switch?

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

выше дает мне следующую ошибку (MSC):

initialization of 'newVal' is skipped by 'case' label

Это, по-видимому, и ограничение на других языках. Почему такая проблема?

+9

Для объяснения, основанного на грамматике C BNF, см. Http://stackoverflow.com/questions/1180550/weird- switch-error-in-obj-c/1181106 # 1181106% 3E – johne 2010-01-12 03:30:11

ответ

929

Case заявления являются только этикетки. Это означает, что компилятор интерпретирует это как переход непосредственно к метке. В C++ проблема здесь - одна из областей. Ваши фигурные скобки определяют область действия как внутри документа switch. Это означает, что вы остаетесь с областью действия, где скачок будет выполняться далее в коде, пропуская инициализацию.Правильный способ справиться с этим - определить область, специфичную для этого оператора case, и определить свою переменную внутри него.

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

Относительно открытия новой области - удобство чтения и согласованность кода. В старые времена вы могли бы автоматически получить «лишний» стек кадров, но теперь это не должно быть для любого достойного оптимизирующего компилятора. – 2008-09-18 14:37:37

+6

Я согласен с Джеффом - слишком легко «предположить» область при чтении инструкции switch из-за отступающего стиля, который используется большинством людей. Мой собственный стиль состоит в том, чтобы всегда открывать новую область для каждого случая/по умолчанию, если длина его больше одной строки. – Bids 2009-01-31 16:43:58

+36

workmad3 - Можете ли вы найти для меня какой-либо компилятор C++, который будет генерировать новый стек кадров, если вы не объявите какие-либо новые переменные? Вы немного меня беспокоили, но ни один из G ++ 3.1, Visual C++ 7 или Intel C++ 8 не генерирует какой-либо код для новых областей, где вы не объявляете никаких переменных. – 2009-02-07 20:11:13

34

Весь оператор switch находится в том же объеме. Чтобы обойти это, сделайте следующее:

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

    case ANOTHER_VAL: 
     ... 
    break; 
} 

Примечание скобки.

+2

изменить «Это не будет работать» комментарий. Пью SO очень быстро. – 2008-09-18 13:16:22

9

Попробуйте это:

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

Я считаю, что проблема в том, что стороны это утверждение было пропущено, и вы пытались использовать вар в другом месте, он не будет объявлен.

6

Вы можете объявлять переменные в распределительном заявлении если вы начинаете новый блок:

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

Причина заключается в том, чтобы делать с выделением (и исправляя) пространства в стеке для хранения локальных переменного (с).

+1

Переменная может быть объявлена, но ее нельзя инициализировать. Кроме того, я уверен, что проблема никак не связана со стеком и локальными переменными. – 2008-09-18 14:12:42

21

Вы не можете этого сделать, потому что метки case фактически являются точками входа в содержащий блок.

Это наиболее четко проиллюстрировано Duff's device. Вот код из Википедии:

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 unsigned (или еще лучше, всегда используйте size_t для counts/index), и проблема исчезнет. – 2010-07-03 20:58:30

+1

@R ..: Что ?! В системе дополнений двух сторон подпись не влияет на модули с полномочиями 2 (это просто И на нижних битах) и не влияет на деления по степеням 2, если ваша процессорная архитектура имеет арифметическую операцию смены вправо (`SAR` в x86, по сравнению с` SHR`, который предназначен для беззнаковых сдвигов). – 2010-09-02 14:00:41

+0

@ Крис: Я считаю, что он имеет в виду, когда компилятор должен допускать отрицательные значения, когда «просто И на нижних битах» не выполняется; например, -1% 8 дает -1 в этой системе дополнений с помощью g ++ (знак в этом случае определяется реализацией на 5.6/4). – 2010-09-02 15:45:12

0

newVal существует во всем объеме переключателя, но инициализируется только если конечность VAL ударил. Если вы создадите блок вокруг кода в VAL, это должно быть ОК.

4

Весь раздел коммутатора представляет собой единый контекст декларации. Вы не можете объявить переменную в таком случае. Попробуйте вместо этого:

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

Переменная может быть объявлена, но ее нельзя инициализировать. – 2008-09-18 14:11:28

3

Если ваш код говорит «int newVal = 42», вы бы разумно ожидали, что newVal никогда не будет инициализирован. Но если вы переходите к этому утверждению (это то, что вы делаете), тогда это именно то, что происходит - newVal - в области видимости, но не назначено.

Если это то, что вы на самом деле должны были случиться, тогда для этого языка необходимо сделать его явным, указав «int newVal; newVal = 42;». В противном случае вы можете ограничить область действия newVal единственным случаем, что более вероятно, что вы хотели.

Это может прояснить ситуацию, если вы рассматриваете тот же пример, но с «const int newVal = 42;»

6

Рассмотрим:

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

При отсутствии заявлений перерыва, иногда newVal получает в два раза объявлен, и вы не знаете, не делает ли он до времени выполнения. Я предполагаю, что ограничение связано с этим путаницей. Каким будет объем newVal? Конвенция будет диктовать, что это будет весь блок переключателя (между фигурными скобками).

Я не программист C++, но в C:

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

работает отлично. Объявление переменной внутри блока переключателя является прекрасным. Объявление после того, как защитник дела не является.

1

Новые переменные могут быть декалированы только при объеме блока. Вы должны написать что-то вроде этого:

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

Конечно, newVal имеет только объем внутри скобок ...

Cheers, Ральф

124

Ok. Просто для того, чтобы прояснить это, это не имеет ничего общего с декларацией. Он относится только к «перепрыгиванию через инициализацию» (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, это не ошибка перепрыгнуть инициализация.

Как уже упоминалось, решение состоит в том, чтобы добавить вложенный блок, чтобы время жизни переменной ограничивалось отдельной меткой case.

+2

«Ошибка при переходе через инициализацию» ??? Не с моим GCC. Это может привести к тому, что предупреждение j можно использовать унифицированным при использовании j под меткой, но ошибки нет. Однако в случае переключения есть ошибка (жесткая ошибка, а не слабое предупреждение). – Mecki 2010-02-20 01:20:53

+8

@Mecki: В C++ это незаконно. ISO C++ '03 - 6.7/3: «... Программа, которая перескакивает из точки, где локальная переменная с длительностью автоматического хранения не находится в области до точки, где она находится в области, плохо сформирована, если только переменная не имеет тип POD (3.9) и объявляется без инициализатора (8.5) ». – 2010-02-22 18:21:25

+1

Да, но это не незаконно в C (по крайней мере, gcc говорит, что это не так). j будет неинициализирован (имеет некоторое случайное число), но компилятор компилирует его. Однако в случае оператора switch компилятор даже не компилирует его, и я не вижу разницы между случаем goto/label и корпусом коммутатора. – Mecki 2010-02-22 22:01:05

16

Большинство ответов до сих пор заблуждаются в одном: вы можете объявлять переменные после сазе, но вы не может инициализировать их:

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

Как упоминалось ранее, хороший путь вокруг этого - использовать фигурные скобки, чтобы создать область для вашего случая.

12

Мой любимый трюк злого трюка - использовать if (0), чтобы пропустить ярлык нежелательного случая.

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

Но очень злой.

+0

Зачем вам это делать? – Landon 2008-09-18 17:05:29

+0

Очень приятно. Пример: case 0 и case 1 могут, например, инициализировать переменную по-разному, которая затем используется в случае 2. – hlovdal 2009-03-19 10:02:32

3

Я просто хотел подчеркнуть slim's point. Конструкция коммутатора создает целую область первого класса. Таким образом, это объявить Возможное (и инициализировать) переменную в распределительном заявлении перед первым случае метки, без дополнительного кронштейна пару:

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

До сих пор ответы были на C++.

Для C++ вы не можете перепрыгнуть через инициализацию. Вы можете на C. Однако, в C, декларация не является выражением, а для ярлыков case должны следовать утверждения.

Так, срок действия (но некрасиво) 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(); 
} 

Conversly, в C++, декларация является утверждение, поэтому справедливо следующее C++, недействительны C

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

Интересное что это хорошо:

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

case 1: 
    break; 
} 

... но это не так:

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

case 1: 
    break; 
} 

Я понимаю, что исправление достаточно просто, но я еще не понимаю, почему первый пример не мешает компилятору. Как упоминалось ранее (на два года раньше хехе), декларация не является причиной этой ошибки, даже несмотря на логику. Инициализация - проблема. Если переменная инициализируется и объявляется в разных строках, она компилируется.

26

После прочтения всех ответов и еще нескольких исследований я получаю несколько вещей.

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++, в соответствии со спецификацией,

меченного декларация допускается, но помечены -initialization не допускается.

См. 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. Объявите переменную перед переключателем() и инициализировать его с различными значениями в случае заявления, если оно удовлетворяет ваше требование

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

Некоторые другие вещи с переключателем заявления

Никогда не пишите любые заявления в коммутаторе, которые не являются частью какой-либо метки, поскольку они никогда не будут выполнены:

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

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

    default: 
     break; 
} 

См. http://codepad.org/PA1quYX3.

0

Стандарт C++ имеет: Можно передать в блок, но не таким образом, чтобы обходить декларации с инициализацией. Программа, которая перескакивает с точки, где локальная переменная с продолжительностью автоматического хранения не находится в области до точки, где она находится в области видимости, плохо сформирована, если переменная не имеет тип 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; 
} 
-1

Оказывается, что анонимные объекты могут быть объявлены или созданы в переключатель сазе по той причине, что на них нельзя ссылаться и, как таковые, не могут перейти к следующему делу. Рассмотрим этот пример компилируется на 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; 
} 
198

Этот вопрос помечается как [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++.

  • Симметрично, в случае С ++ проблема может быть легко решена без {}. Просто удалите инициализатору от объявления переменных и код станет действительным

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

    Обратите внимание, что несмотря на то, что в настоящее время действует с точки зрения C++, остается недействующими с точки зрения C.

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).

3

Я написал этот ответ в оригинале для this question. Однако, когда я закончил, я обнаружил, что ответ был закрыт. Поэтому я разместил его здесь, может быть, кому-то, кому нравятся ссылки на стандарт, будет полезно.

Оригинальный код в вопросе:

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

Есть на самом деле 2 вопроса:

1. Почему я могу объявить переменную после case этикетки?

Это потому, что в C++ метка должна быть в форме:

N3337 6,1/1

labeled-statement:

...

  • attribute-specifier-seqoptcase constant-expression : statement

...

И в C++операторе объявления также рассматривается как заявление (в отличие от C):

N3337 6/1:

statement:

...

declaration-statement

...

2. Почему я могу перепрыгнуть через объявление переменной, а затем использовать его?

Потому что: N3337 6,7/3

It is possible to transfer into a block, but not in a way that bypasses declarations with initialization. A program that jumps (The transfer from the condition of a switch statement to a case label is considered a jump in this respect.)

from a point where a variable with automatic storage duration is not in scope to a point where it is in scope is ill-formedunless the variable has scalar type, class type with a trivial default constructor and a trivial destructor, a cv-qualified version of one of these types, or an array of one of the preceding types and is declared without an initializer (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;