5.3.2. Оператор switch

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

• Читать каждый введенный символ.

• Сравнить каждый символ с набором искомых гласных.

• Если символ соответствует одной из гласных букв, добавить 1 к соответствующему счетчику.

• Отобразить результаты.

Программа должна отобразить результаты в следующем виде:

Number of vowel а: 3195

Number of vowel e: 6230

Number of vowel i: 3102

Number of vowel o: 3289

Number of vowel u: 1033

Для непосредственного решения этой задачи можно использовать оператор switch.

// инициализировать счетчики для каждой гласной

unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;

char ch;

while (cin >> ch) {

 // если ch - гласная, увеличить соответствующий счетчик

 switch (ch) {

 case 'a':

  ++aCnt;

  break;

 case 'e':

  ++eCnt;

  break;

 case 'i':

  ++iCnt;

  break;

 case 'o':

  ++oCnt;

  break;

 case 'u':

  ++uCnt;

  break;

 }

}

// вывод результата

cout << "Number of vowel a: " << aCnt << ' '

     << "Number of vowel e: " << eCnt << ' '

     << "Number of vowel i: " << iCnt << ' '

     << "Number of vowel o: " << oCnt << ' '

     << "Number of vowel u: " << uCnt << endl;

Оператор switch вычисляет результат выражения, расположенного за ключевым словом switch. Это выражение может быть объявлением инициализированной переменной (см. раздел 5.2). Выражение преобразуется в целочисленный тип. Результат выражения сравнивается со значением, ассоциированным с каждым оператором case.

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

Более подробно оператор break рассматривается в разделе 5.5.1, а пока достаточно знать, что он прерывает текущий поток выполнения. В данном случае оператор break передает управление первому оператору после оператора switch. Здесь оператор switch является единственным оператором в теле цикла while, поэтому его прерывание возвращает контроль окружающему оператору while. Поскольку в нем нет никаких других операторов, цикл while продолжается, если его условие выполняется.

Если соответствия не найдено, выполнение сразу переходит к первому оператору после switch. Как уже упоминалось, в этом примере выход из оператора switch передает управление условию цикла while.

Ключевое слово case и связанное с ним значение называют также меткой case (case label). Значением каждой метки case является константное выражение (см. раздел 2.4.4).

char ch = getVal();

int ival = 42;

switch(ch) {

case 3.14: // ошибка: метка case не целое число

case ival: // ошибка: метка case не константа

// ...

Одинаковые значения меток case недопустимы. Существует также специальная метка default, рассматриваемая ниже.

Порядок выполнения в операторе switch

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

Однако возможны ситуации, когда необходимо именно стандартное поведение оператора switch. У каждой метки case может быть только одно значение, однако две или более метки могут совместно использовать единый набор действий. В таких ситуациях достаточно пропустить оператор break и позволить программе пройти несколько меток case.

Например, можно было бы посчитать общее количество гласных так:

unsigned vowelCnt = 0;

// ...

switch (ch) {

// для инкремента vowelCnt подойдет любая буква а, е, i, о или u

case 'a':

case 'e':

case 'i':

case 'o':

case 'u':

 ++vowelCnt;

 break;

}

Здесь расположено несколько меток case подряд без оператора break. Теперь при любой гласной в переменной ch будет выполняться тот же код.

Поскольку язык С++ не требует обязательно располагать метки case в отдельной строке, весь диапазон значений можно указать в одной строке:

switch (ch) {

// альтернативный допустимый синтаксис

case 'a': case 'e': case 'i': case 'o': case 'u':

 ++vowelCnt;

 break;

}

Случаи, когда оператор break пропускают преднамеренно, довольно редки, поэтому их следует обязательно комментировать, объясняя логику действий.

Пропуск оператора break — весьма распространенная ошибка

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

// внимание: преднамеренно неправильный код!

switch (ch) {

case 'a' :

 ++aCnt; // Упс! Необходим оператор break

case 'e':

 ++eCnt; // Упс! Необходим оператор break

case 'i':

 ++iCnt; // Упс! Необходим оператор break

case 'o':

 ++oCnt; // Упс! Необходим оператор break

case 'u':

 ++uCnt;

}

Чтобы понять происходящее, предположим, что значением переменной ch является 'e'. Выполнение переходит к коду после метки case 'e', где происходит инкремент переменной eCnt. Выполнение продолжается далее через метки case, увеличивая также значения переменных iCnt, oCnt и uCnt.

Несмотря на то что оператор break и не обязателен после последней метки оператора switch, использовать его все же рекомендуется. Ведь если впоследствии оператор switch будет дополнен еще одной меткой case, отсутствие оператора break после прежней последней метки не создаст проблем.

Метка default

Операторы после метки default выполняются, если ни одна из меток case не соответствует значению выражения оператора switch. Например, в рассматриваемый код можно добавить счетчик негласных букв. Значение этого счетчика по имени otherCnt будет увеличиваться в случае default:

// если ch гласная, увеличить соответствующий счетчик

switch (ch) {

case 'a': case 'e': case 'i': case 'o': case 'u':

 ++vowelCnt;

 break;

default:

 ++otherCnt;

 break;

}

В этой версии, если переменная ch не содержит гласную букву, управление перейдет к метке default и увеличится значение счетчика otherCnt.

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

Метка не может быть автономной; она должна предшествовать оператору или другой метке case. Если оператор switch заканчивается разделом default, в котором не осуществляется никаких действий, за меткой default должен следовать пустой оператор или пустой блок.

Определение переменной в операторе switch

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

Ответ прост: недопустим переход с места, где переменная с инициализатором уже вышла из области видимости к месту, где эта переменная находится в области видимости.

case true:

 // этот оператор switch недопустим, поскольку инициализацию

 // можно обойти

 string file_name; // ошибка: выполнение обходит неявно

                   // инициализированную переменную

 int ival = 0;     // ошибка: выполнение обходит неявно

                   // инициализированную переменную

 int jval;         // ok: поскольку jval не инициализирована

 break;

case false:

 // ok: jval находится в области видимости, но она не инициализирована

 jval = next_num();     // ok: присвоить значение jval

 if (file_name.empty()) // file_name находится в области видимости, но

                        // она не инициализирована

 // ...

Если бы этот код был допустим, то любой переход к случаю false обходил бы инициализацию переменных file_name и ival, но они оставались бы в области видимости и код вполне мог бы использовать их. Однако эти переменные не были бы инициализированы. В результате язык не позволяет перепрыгивать через инициализацию, если инициализированная переменная находится в области видимости в пункте, к которому переходит управление.

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

case true:

 {

  // ok: оператор объявления в пределах операторного блока

  string file_name = get_file_name();

  // ...

 }

 break;

case false:

 if (file_name.empty()) // ошибка: file_name вне области видимости

Упражнения раздела 5.3.2

Упражнение 5.9. Напишите программу, использующую серию операторов if для подсчета количества гласных букв в тексте, прочитанном из потока cin.

Упражнение 5.10. Программа подсчета гласных имеет одну проблему: она не учитывает заглавные буквы как гласные. Напишите программу, которая подсчитывает гласные буквы как в верхнем, так и в нижнем регистре. То есть значение счетчика aCnt должно увеличиваться при встрече как символа 'a', так и символа 'A' (аналогично для остальных гласных букв).

Упражнение 5.11. Измените рассматриваемую программу так, чтобы она подсчитывала также количество пробелов, символов табуляции и новой строки.

Упражнение 5.12. Измените рассматриваемую программу так, чтобы она подсчитывала количество встреченных двухсимвольных последовательностей: ff, fl и fi.

Упражнение 5.13. Каждая из приведенных ниже программ содержит распространенную ошибку. Выявите и исправьте каждую из них.

Код для упражнения 5.13

(a) unsigned aCnt = 0, eCnt = 0, iouCnt = 0;

    char ch = next_text();

    switch (ch) {

    case 'a': aCnt++;

    case 'e': eCnt++;

    default: iouCnt++;

    }

(b) unsigned index = some_value();

    switch (index) {

    case 1:

     int ix = get_value();

     ivec[ix] = index;

     break;

    default:

     ix = ivec.size()-1;

     ivec[ix] = index;

(c) unsigned evenCnt = 0, oddCnt = 0;

    int digit = get_num() % 10;

    switch (digit) {

    case 1, 3, 5, 7, 9:

     oddcnt++;

     break;

    case 2, 4, 6, 8, 10:

     evencnt++;

     break;

    }

(d) unsigned ival=512, jval=1024, kval=4096;

    unsigned bufsize;

    unsigned swt = get_bufCnt();

    switch(swt) {

    case ival:

     bufsize = ival * sizeof (int);

     break;

    case jval:

     bufsize = jval * sizeof(int);

     break;

    case kval:

     bufsize = kval * sizeof(int);

     break;

    }