Почему переменные не могут быть объявлены в операторе switch?

760

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

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

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

инициализация "newVal" пропускается меткой "case"

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

  • 9
    Для объяснения, основанного на грамматике C BNF, см. Stackoverflow.com/questions/1180550/weird-switch-error-in-obj-c/…
  • 0
    Вот действительно хорошее прочтение о выражениях переключателей и метках (ABC :) в целом.
Показать ещё 1 комментарий
Теги:
switch-statement

23 ответа

936
Лучший ответ
Операторы

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

switch (val)
{   
case VAL:  
{
  // This will work
  int newVal = 42;  
  break;
}
case ANOTHER_VAL:  
...
break;
}
  • 90
    По отношению к открытию новой области видимости - удобочитаемость и последовательность в коде. В старые времена вы могли автоматически получать «лишние» стековые фреймы, но теперь этого не должно быть у любого приличного оптимизирующего компилятора.
  • 8
    Я согласен с Джеффом - слишком легко «принять» область видимости при чтении оператора switch из-за стиля отступа, который использует большинство людей. Мой собственный стиль - всегда открывать новую область видимости для каждого случая / значения по умолчанию, если он длиннее одной строки.
Показать ещё 7 комментариев
153

Этот вопрос отмечен одновременно как [C] и [С++]. Исходный код действительно недействителен как для C, так и для С++, но для совершенно разных причин. Я считаю, что эта важная деталь была упущена (или запутана) существующими ответами.

  • В С++ этот код недействителен, поскольку метка case ANOTHER_VAL: переходит в область переменной newVal, минуя ее инициализацию. Переходы, которые обходят инициализацию локальных объектов, являются незаконными в С++. Эта сторона вопроса правильно решена большинством ответов.

  • Однако в языке 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, хотя эти проблемы бывают очень разными. На стороне С++ он ограничивает область newVal, убедившись, что case ANOTHER_VAL: больше не переходит в эту область, что устраняет проблему С++. На стороне 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, он остается недопустимым с точки зрения С++.

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

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

    Обратите внимание, что, хотя он теперь действителен с точки зрения С++, он остается недействительным с точки зрения C.

  • 2
    @AnT: я понимаю, почему тот, который исправляет C ++, не применим для C; Тем не менее, я не могу понять, как это решает проблему C ++ пропуска инициализации в первую очередь? Не будет ли он по-прежнему пропускать объявление и присваивание newVal при ANOTHER_VAL к ANOTHER_VAL ?
  • 8
    @ legends2k: Да, это все еще пропускает. Однако когда я говорю «это решает проблему», я имею в виду, что это исправляет ошибку компилятора C ++ . В C ++ нельзя пропускать скалярное объявление с помощью инициализатора , но вполне нормально пропускать скалярное объявление без инициализатора . В case ANOTHER_VAL: переменная точки newVal видна, но с неопределенным значением.
Показать ещё 3 комментария
123

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

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

  • 2
    "Ошибка, перепрыгивающая через инициализацию" ??? Не с моим GCC. Может выдавать предупреждение «j может использоваться в унифицированном виде» при использовании j под меткой, но ошибки нет. Однако в случае переключения возникает ошибка (серьезная ошибка, а не слабое предупреждение).
  • 9
    @Mecki: это незаконно в C ++. ISO C ++ '03 - 6.7 / 3: "... Программа, которая переходит из точки, в которой локальная переменная с автоматическим хранением находится вне области действия, в точку, где она находится в области видимости, если она не имеет типа POD (3.9) и объявляется без инициализатора (8.5). "
Показать ещё 2 комментария
32

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

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

    case ANOTHER_VAL:
      ...
    break;
}

Обратите внимание на скобки.

  • 3
    Измените комментарий «Это не будет работать». Фу так действительно быстро.
24

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

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. Здесь я также получаю ту же ошибку.


В С++, согласно спецификации,

labeled-declaration разрешено, но помечено как -инициализация.

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


Решение такого условия - это два

  • Либо используйте новую область с помощью {}

    case 1:
           {
               int x=10;
               printf(" x is %d", x);
           }
    break;
    
  • Или используйте фиктивный оператор с меткой

    case 1: ;
               int x=10;
               printf(" x is %d",x);
    break;
    
  • Объявить переменную перед switch() и инициализировать ее различными значениями в случае, если она соответствует вашему требованию

    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 ++, так это перепрыгивать через инициализацию переменной a в область видимости переменной a . Итак, с точки зрения C, проблема в case VAL: label, и вы правильно его описали. Но с точки зрения C ++ проблема заключается в case ANOTHER_VAL: label.
  • 0
    В C ++, в отличие от C, объявления являются подмножеством операторов.
22

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

Это наиболее ярко иллюстрируется устройством Duff. Вот какой код из Википедии:

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 для счетчиков / индексов), и проблема исчезнет.
  • 1
    @R ..: Что ?! В системе дополнения до двух подпись не влияет на модули степенями 2 (это просто «И» на младших битах) и не влияет на деления степенями 2, если ваша архитектура процессора имеет арифметическую операцию сдвига вправо ( SAR в x86, по сравнению с SHR для беззнаковых смен).
Показать ещё 7 комментариев
16

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

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

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

  • 0
    codepad.org/rTAActCN смотрите здесь объявление также не собирается компилировать ..
  • 1
    Мистер 32, вы неправильно поняли, в чем ваша ошибка: да, это не скомпилируется, но не потому, что вы объявляете переменную внутри переключателя. Ошибка в том, что вы пытаетесь объявить переменную после оператора, что недопустимо в C.
Показать ещё 1 комментарий
12

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

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

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

  • 0
    Зачем ты это делаешь?
  • 0
    Очень хорошо. Пример того, почему: case 0 и case 1 могут, например, инициализировать переменную по-другому, что затем используется в случае 2.
Показать ещё 2 комментария
9

Попробуйте следующее:

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

Вы можете объявить переменные в инструкции switch, если вы запустите новый блок:

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

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

  • 1
    Переменная может быть объявлена, но не может быть инициализирована. Кроме того, я почти уверен, что проблема никак не связана со стеком и локальными переменными.
5

Рассмотрим:

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

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

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

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

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

  • 0
    -1 здесь int x никогда не будет выполнен. см. этот codepad.org/PA1quYX3
  • 3
    @ Mr.32: на самом деле ваш пример показывает, что printf не выполняется, но в этом случае int x является не оператором, а объявлением, x объявляется, пространство для него резервируется каждый раз, когда функциональная среда складывается, см .: codepad.org/4E9Zuz1e
Показать ещё 1 комментарий
4

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

switch (val)  
{  
case VAL:
{
  // This will work
  int newVal = 42;
  break;
}
case ANOTHER_VAL:  
  ...
  break;
}
  • 0
    Переменная может быть объявлена, но не может быть инициализирована.
  • 0
    @Richard Corden Я уверен, что инициализация будет работать. Вы все еще утверждаете, что это не может быть инициализировано?
3

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

Оригинальный код:

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

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

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

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

N3337 6.1/1

меченый-оператор:

...

  • attribute-specifier-seqopt case constant-expression: statement

...

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

N3337 6/1:

утверждение:

...

декларация-выражение

...

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

Потому что: N3337 6.7/3

Можно передать в блок , но не таким образом, чтобы обходить объявления с инициализацией. программа, которая прыгает ( передача из условия оператора оператора на ярлык case рассматривается как прыжок в этом отношении.)

из точки, где переменная с продолжительностью автоматического хранения не находится в области до точки, где она находится в области, неформатирована , если переменная не имеет скалярный тип тип класса с тривиальным значением по умолчанию конструктор и тривиальный деструктор, 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;
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 »». Как сказал Питер и г-н 32, «case 0:; int j; ...» и «case 0:; int j = 7; ...» выполняют обе функции. Проблема в C только в том, что "case <label>: объявление" не является допустимым синтаксисом C.
3

До сих пор ответы были для С++.

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

Итак, действительный (но уродливый) 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

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'
3

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

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

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

2

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

switch (val) {  
  /* This *will* work, even in C89 */
  int newVal = 42;  
case VAL:
  newVal = 1984; 
  break;
case ANOTHER_VAL:  
  newVal = 2001;
  break;
}
  • 0
    -1 здесь int newVal = 42; никогда не будет казнен. см. этот codepad.org/PA1quYX3
  • 4
    будет выполнено объявление int newVal , но не присвоение = 42 .
1

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

1

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

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

Конечно, newVal имеет только рамки в фигурных скобках...

Приветствия, Ральф

0

Похоже, анонимные объекты могут быть объявлены или созданы в операторе case switch по той причине, что они не могут быть указаны и как таковые не могут пройти до следующего случая. Рассмотрим этот пример, который компилируется в 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;
}
  • 0
    Если вы собираетесь проголосовать, объясните почему. Мне любопытно узнать, почему создание анонимного объекта является исключением.
  • 1
    не DV, но: весь вопрос о декларировании / области именованных переменных. Временный («анонимный объект» не является термином) не является именованной переменной, не является декларацией и не является предметом области видимости (если он не связан с const ссылкой с собственной областью видимости). Это выражение, которое живет и умирает в своем утверждении (где бы оно ни было). Поэтому это совершенно не имеет значения.
Показать ещё 1 комментарий
0

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

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

-2

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню