На этом уроке мы рассмотрим еще один оператор управления потоком выполнения программы — оператор switch, а также то, зачем его использовать и как это делать эффективно.
Зачем использовать оператор switch?
Хоть мы и можем использовать сразу несколько операторов if/else вместе — читается и смотрится это не очень. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <iostream> enum Colors { COLOR_GRAY, COLOR_PINK, COLOR_BLUE, COLOR_PURPLE, COLOR_RED }; void printColor(Colors color) { if (color == COLOR_GRAY) std::cout << "Gray"; else if (color == COLOR_PINK) std::cout << "Pink"; else if (color == COLOR_BLUE) std::cout << "Blue"; else if (color == COLOR_PURPLE) std::cout << "Purple"; else if (color == COLOR_RED) std::cout << "Red"; else std::cout << "Unknown"; } int main() { printColor(COLOR_BLUE); return 0; } |
Использование ветвления if/else для проверки значения одной переменной — практика распространенная, но язык C++ предоставляет альтернативный и более эффективный условный оператор ветвления switch. Вот вышеприведенная программа, но уже с использованием оператора switch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> enum Colors { COLOR_GRAY, COLOR_PINK, COLOR_BLUE, COLOR_PURPLE, COLOR_RED }; void printColor(Colors color) { switch (color) { case COLOR_GRAY: std::cout << "Gray"; break; case COLOR_PINK: std::cout << "Pink"; break; case COLOR_BLUE: std::cout << "Blue"; break; case COLOR_PURPLE: std::cout << "Purple"; break; case COLOR_RED: std::cout << "Red"; break; default: std::cout << "Unknown"; break; } } int main() { printColor(COLOR_BLUE); return 0; } |
Общая идея операторов switch проста: выражение оператора switch (например, switch(color)
) должно производить значение, а каждый кейс (англ. «case») проверяет это значение на соответствие. Если кейс совпадает с выражением switch, то выполняются инструкции под соответствующим кейсом. Если ни один кейс не соответствует выражению switch, то выполняются инструкции после кейса default
(если он вообще указан).
Из-за своей реализации, операторы switch обычно более эффективны, чем цепочки if/else. Давайте рассмотрим это более подробно.
Оператор switch
Сначала пишем ключевое слово switch за которым следует выражение, с которым мы хотим работать. Обычно это выражение представляет собой только одну переменную, но это может быть и нечто более сложное, например, nX + 2
или nX − nY
. Единственное ограничение к этому выражению — оно должно быть интегрального типа данных (т.е. типа char, short, int, long, long long или enum). Переменные типа с плавающей точкой или неинтегральные типы использоваться не могут.
После выражения switch мы объявляем блок. Внутри блока мы используем лейблы (англ. «labels») для определения всех значений, которые мы хотим проверять на соответствие выражению. Существуют два типа лейблов.
Лейблы case
Первый вид лейбла — это case (или просто «кейс»), который объявляется с использованием ключевого слова case и имеет константное выражение. Константное выражение — это то, которое генерирует константное значение, другими словами: либо литерал (например, 5
), либо перечисление (например, COLOR_RED
), либо константу (например, переменную x
, которая была объявлена с ключевым словом const).
Константное выражение, находящееся после ключевого слова case, проверяется на равенство с выражением, находящимся возле ключевого слова switch. Если они совпадают, то тогда выполняется код под соответствующим кейсом.
Стоит отметить, что все выражения case должны производить уникальные значения. То есть вы не сможете сделать следующее:
1 2 3 4 5 6 |
switch (z) { case 3: case 3: // нельзя, значение 3 уже используется! case COLOR_PURPLE: // нельзя, COLOR_PURPLE вычисляется как 3! }; |
Можно использовать сразу несколько кейсов для одного выражения. Следующая функция использует несколько кейсов для проверки, соответствует ли параметр p
числу из ASCII-таблицы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
bool isDigit(char p) { switch (p) { case '0': // если p = 0 case '1': // если p = 1 case '2': // если p = 2 case '3': // если p = 3 case '4': // если p = 4 case '5': // если p = 5 case '6': // если p = 6 case '7': // если p = 7 case '8': // если p = 8 case '9': // если p = 9 return true; // возвращаем true default: // в противном случае, возвращаем false return false; } } |
В случае, если p
является числом из ASCII-таблицы, то выполнится первый стейтмент после кейса — return true;
.
Лейбл по умолчанию
Второй тип лейбла — это лейбл по умолчанию (так называемый «default case»), который объявляется с использованием ключевого слова default. Код под этим лейблом выполняется, если ни один из кейсов не соответствует выражению switch. Лейбл по умолчанию является необязательным. В одном switch может быть только один default. Обычно его объявляют самым последним в блоке switch.
В вышеприведенном примере, если p
не является числом из ASCII-таблицы, то тогда выполняется лейбл по умолчанию и возвращается false.
switch и fall-through
Одна из самых каверзных вещей в switch — это последовательность выполнения кода. Когда кейс совпал (или выполняется default), то выполнение начинается с первого стейтмента, который находится после соответствующего кейса и продолжается до тех пор, пока не будет выполнено одно из следующих условий завершения:
Достигнут конец блока switch.
Выполняется оператор return.
Выполняется оператор goto.
Выполняется оператор break.
Обратите внимание, если ни одного из этих условий завершения не будет, то выполняться будут все кейсы после того кейса, который совпал с выражением switch. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
switch (2) { case 1: // Не совпадает! std::cout << 1 << '\n'; // пропускается case 2: // Совпало! std::cout << 2 << '\n'; // выполнение кода начинается здесь case 3: std::cout << 3 << '\n'; // это также выполнится case 4: std::cout << 4 << '\n'; // и это default: std::cout << 5 << '\n'; // и это } |
Результат:
2
3
4
5
А это точно не то, что нам нужно! Когда выполнение переходит из одного кейса в следующий, то это называется fall-through. Программисты почти никогда не используют fall-through, поэтому в редких случаях, когда это все-таки используется — программист оставляет комментарий, в котором сообщает, что fall-through является преднамеренным.
switch и оператор break
Оператор break (объявленный с использованием ключевого слова break) сообщает компилятору, что мы уже сделали всё, что хотели с определенным switch (или циклом while, do while или for) и больше не намерены с ним работать. Когда компилятор встречает оператор break, то выполнение кода переходит из switch на следующую строку после блока switch. Рассмотрим вышеприведенный пример, но уже с корректно вставленными операторами break:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
switch (2) { case 1: // не совпадает - пропускается std::cout << 1 << '\n'; break; case 2: // совпало! Выполнение начинается со следующего стейтмента std::cout << 2 << '\n'; // выполнение начинается здесь break; // оператор break завершает выполнение switch case 3: std::cout << 3 << '\n'; break; case 4: std::cout << 4 << '\n'; break; default: std::cout << 5 << '\n'; break; } // Выполнение продолжается здесь |
Поскольку второй кейс соответствует выражению switch, то выводится 2
, и оператор break завершает выполнение блока switch. Остальные кейсы пропускаются.
Предупреждение: Не забывайте использовать оператор break в конце каждого кейса. Его отсутствие — одна из наиболее распространенных ошибок новичков!
Несколько стейтментов внутри блока switch
Еще одна странность в switch заключается в том, что вы можете использовать несколько стейтментов под каждым кейсом, не определяя новый блок:
1 2 3 4 5 6 7 8 9 10 11 |
switch (3) { case 3: std::cout << 3; boo(); std::cout << 4; break; default: std::cout << "default case\n"; break; } |
Объявление переменной и её инициализация внутри case
Вы можете объявлять, но не инициализировать переменные внутри блока case:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
switch (x) { case 1: int z; // ок, объявление разрешено z = 5; // ок, операция присваивания разрешена break; case 2: z = 6; // ок, переменная z была объявлена выше, поэтому мы можем использовать её здесь break; case 3: int c = 4; // нельзя, вы не можете инициализировать переменные внутри case break; default: std::cout << "default case" << std::endl; break; } |
Обратите внимание, что, хотя переменная z
была определена в первом кейсе, она также используется и во втором кейсе. Все кейсы считаются частью одной и той же области видимости, поэтому, объявив переменную в одном кейсе, мы можем спокойно использовать её без объявления и в других кейсах.
Это может показаться немного нелогичным, поэтому давайте рассмотрим это детально. Когда мы определяем локальную переменную, например, int y;
, то переменная не создается в этой точке — она фактически создается в начале блока, в котором объявлена. Однако, она не видна в программе до точки объявления. Само объявление не выполняется, оно просто сообщает компилятору, что переменная уже может использоваться в коде. Поэтому переменная, объявленная в одном кейсе, может использоваться в другом кейсе, даже если кейс, объявляющий переменную, никогда не выполняется.
Однако инициализация переменных непосредственно в кейсах запрещена и вызовет ошибку компиляции. Это связано с тем, что инициализация переменной требует выполнения, а кейс, содержащий инициализацию, может никогда не выполниться!
Если в кейсе нужно объявить и/или инициализировать новую переменную, то это лучше всего сделать, используя блок стейтментов внутри кейса:
1 2 3 4 5 6 7 8 9 10 11 12 |
switch (1) { case 1: { // обратите внимание, здесь указан блок int z = 5; // хорошо, переменные можно инициализировать внутри блока, который находится внутри кейса std::cout << z; break; } default: std::cout << "default case" << std::endl; break; } |
Правило: Если нужно инициализировать и/или объявить переменные внутри кейса — используйте блоки стейтментов.
Тест
Задание №1
Напишите функцию calculate(), которая принимает две переменные типа int и одну переменную типа char, которая, в свою очередь, представляет одну из следующих математических операций: +
, -
, *
, /
или %
(остаток от числа). Используйте switch для выполнения соответствующей математической операции над целыми числами, а результат возвращайте обратно в main(). Если в функцию передается недействительный математический оператор, то функция должна выводить ошибку. С оператором деления выполняйте целочисленное деление.
Ответ №1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> int calculate(int x, int y, char op) { switch (op) { case '+': return x + y; case '-': return x - y; case '*': return x * y; case '/': return x / y; case '%': return x % y; default: std::cout << "calculate(): Unhandled case\n"; return 0; } } int main() { std::cout << "Enter an integer: "; int x; std::cin >> x; std::cout << "Enter another integer: "; int y; std::cin >> y; std::cout << "Enter a mathematical operator (+, -, *, /, or %): "; char op; std::cin >> op; std::cout << x << " " << op << " " << y << " is " << calculate(x, y, op) << "\n"; return 0; } |
Задание №2
Определите перечисление (или класс enum) Animal
, которое содержит следующих животных: pig
, chicken
, goat
, cat
, dog
и ostrich
. Напишите функцию getAnimalName(), которая принимает параметр Animal
и использует switch для возврата типа животного в качестве строки. Напишите еще одну функцию — printNumberOfLegs(), которая использует switch для вывода количества лап соответствующего типа животного. Убедитесь, что обе функции имеют кейс default, который выводит сообщение об ошибке. Вызовите printNumberOfLegs() в main(), используя в качестве параметров cat
и chicken
.
Пример результата выполнения вашей программы:
A cat has 4 legs.
A chicken has 2 legs.
Ответ №2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#include <iostream> #include <string> enum Animal { ANIMAL_PIG, ANIMAL_CHICKEN, ANIMAL_GOAT, ANIMAL_CAT, ANIMAL_DOG, ANIMAL_OSTRICH }; std::string getAnimalName(Animal animal) { switch (animal) { case ANIMAL_CHICKEN: return "chicken"; case ANIMAL_OSTRICH: return "ostrich"; case ANIMAL_PIG: return "pig"; case ANIMAL_GOAT: return "goat"; case ANIMAL_CAT: return "cat"; case ANIMAL_DOG: return "dog"; default: return "getAnimalName(): Unhandled enumerator"; } } void printNumberOfLegs(Animal animal) { std::cout << "A " << getAnimalName(animal) << " has "; switch (animal) { case ANIMAL_CHICKEN: case ANIMAL_OSTRICH: std::cout << "2"; break; case ANIMAL_PIG: case ANIMAL_GOAT: case ANIMAL_CAT: case ANIMAL_DOG: std::cout << "4"; break; default: std::cout << "printNumberOfLegs(): Unhandled enumerator"; break; } std::cout << " legs.\n"; } int main() { printNumberOfLegs(ANIMAL_CAT); printNumberOfLegs(ANIMAL_CHICKEN); return 0; } |
Задание 1:
Второе задание:
Друге завдання
В первом задании сделал чуть сложнее, чем в ответе — в main добавил один if, который проверял знак и если знак был ‘/’, то вызывалась введенные переменные переводились в double и вызывалась функция для деления, возвращающая тип double. Иначе то решение, что есть, при вводе 3, к примеру, и 5 и потом выбирая / выдает 0 в ответе. А у меня — 0,6
После return в case не нужно писать break, так как перехода к следующему case не будет — switch закончится на return
Первое задание опять немного неправильно сделал, но уже сам.
Не обратил внимание о том что функция должна возвращать значение, но работает.
задание 2
задание 1
1.
2.
Выполнил задание 2, но добавил функцию выбора животного пользователем, чтобы программа приобрела больше смысла. Но странное дело, пришлось поменять целочисленные значения перечислителей. Когда счет начинался с "0", то на введение точки, скобки или чего нибудь другого (но не цифры) реагировал выбор PIG, который был равен 0. А когда нумерация пошла с 1, то все стало хорошо функционировать.
Почему так произошло???.
Задание 2:
main.ccp:
Могу лишь предполагать.
Переменные типа Animal работают только с целочисленными значениями. Соответственно если пользователь ввел не целочисленное значение, а символ вместо цифры, который затем явно должен преобразоваться в тип Animal в вашем случае, то быть может компилятор воспринимает его как за нулевое значение, то есть ничто.
P.S.: в кейсах уже присутствует оператор return с определенным значением, соответственно оператор break уже можно не писать я думаю.
Так оно и оказалось.
Если пользователь введет число, не соответствующее типу данных переменной, то оно интерпретируется как 0.
Если присвоить переменной double через cin просто букву, то компилятор воспринимает это число как за 0. Это можно понять, если вывести переменную через cout.
Первая задача
В ответе на задание 1 в функции int calculate(int x, int y, char op) есть строка :
Почему при вводе некоректного оператора на экран выводится 0 , а не текст : "calculate(): Unhandled case" ?
А вдруг кому-то пригодится 🙂
Добавил случай, когда используется default и возврат кода ошибки при завершении программы.
Добавил возврат кода ошибки (-1) при незнакомой операции.
народ, не пойму что за черное колдунство?
2 задача
Знаю, что структура не самая лучшая, но не понимаю из-за чего выдает ошибку:
46 строка: switch (name)
name — expression must have integral or enum type;
И к каждому кейсу этой же функции — this constant expression has type "const char *" instead of the required "std::string" type
В конструкции switch в case вариантах можно использовать только численные значения или перечисления, перевод ошибки компилятора)
Задание №2
Задание №1
Так и не понятно, можно инициализировать переменную в "case" или нет!? Автор пишет что нет и даёт объяснение, что "case" может не сработать. Каким образом тогда, в следующем абзаце идёт речь об инициализации внутри блока "case" ? Как блок может снимать запрет!?
У блока локальная область видимости
Switch-case это тоже блок, поэтому язык программирования С++ не запрещает объявить переменную в любом месте данного блока, при этом действует правило, что переменна должна быть объявлена в том же блоке до её инициализации. Т.е. следующий код будет работать:
Такой код, в принципе, допустим. А вот объявление переменных внутри блока оператора case — это дурной тон из-за того, что код читается компилятором сверху вниз.
Также нет правила для того, где должен стоять default. По соглашению, его обычно ставят в самом конце, но тут есть ещё одно уточнение для оператора switch: условия case имеет смысл ставить в порядке от наиболее до наименее ожидаемого. Если наиболее ожидаемый вариант default, то и располагать его имеет смысл в самом начале. И вот отсюда вытекает, что хоть переменную можно и в блоке case, но из-за порядка обработки условий можно словить неприятность.
Если переменная и объявляется, то только сразу после switch, либо внутри блока кода ({ }). Второй вариант предпочтительнее.
Язык С++ предоставляет широкий спектр возможностей с минимальными ограничениями, поэтому им нужно пользоваться очень осторожно.
Написал второе задание через структуры
Имеет ли жизнь такой вариант? Вроде как сразу видно что выводится. Или функцию main() лучше не засорять тем, что можно сделать в функциях об назв. животных и их лап?
Коза обычно имеет 4 "лапы". Может она травмирована?
Первый пример в статье: как switch может быть эффективнее, если с ним аж на 7 строк больше? 🙂 Притом что читабельность совсем не улучшилась (а может, даже ухудшилась из-за постоянных break). Очень редко юзаю switch.
№1. по-моему, неплохо)
ключевое слово break -то где, а?
а зачем писать break после return?
хочешь по лучше спрятать вещь — поставь её на самое видное место — ретурна я то и не просёк:-)
не боись, компилятор выкинуть должен ненужное, и бинарный код будет таким же, как и без break.
Добрый день Юрий.
Подобное пишу на bash. Правда можно было гораздо оптимальнее, но не будет об этом думать, учимся.
Очень длинная строка, а нельзя просто отправить проверить в списке enum не перечисляя каждое животное?
т.е. если в кейсах есть оператор возврата return, то оператор break не нужен?
Оцените пожалуйста
Задание 1:
Задание 2 (я усовершенствовал до ввода пользователем данных)
P.S. Я в курсе, что так не желательно делать, но ситуация требовала
code типа short
Да, это нечто.
Колупал не слабо 2е задание с этим перечислителями и enum.
Разобрался все же.
Нашел интересную особенность. Если enum с классом, то код весь верный, хотя "enum class" нету, только "enum". Все компилирует.
Господи, простейшие программы для теста. Ото бы не тупил, все элементарно.
Намного проще, чем думал и писал.
Правда почему здесь нету в конце "break;"?
Это же ошибка.
Какая ошибка? Там return'ы стоят. А они завершают конструкцию switch аналогично break'у. Поэтому break и не нужны там.
Немного доработал, ибо выполняло в любом случаи.
Единственный вопрос — как в if это оптимизировать. Сделать массив из значений для op.
printNumberOfLegs получается void
Точно, пропустил этот момент. Но программа работает и так.
Зачем в switch расписывать default, если с неправильным параметром функция просто не запустится? Выходит, что default не к чему.
В default вы можете просто вывести сообщение пользователю, что он ввёл некорректные значения, чтобы пользователь получил информативный ответ, почему что-то не работает/что он сделал не так и вообще, что ему нужно делать.
Задание №2
вот так я выполнил второе задание:
вот так получилось первое задание :
А если в Вашу функцию передать 0?
У меня QT ругается, если в функцию с возвратом стринг, засунуть только свитч:
switch control reaches end of non-void function [-Wreturn-type]
Вот сам кусочек кода.
То есть он не видит возврата внутри switch?
А еще у меня тут нет default: потому что на него тоже фреймворк ругается, говорит что ты учел все позиции из enum class Animal и default тебе не нужен, он не будет использован.
Думаю, что компилятор справедливо считает, что в switch может не выполнится ни один case и тогда никакой return не выполнится, что будет ошибкой, т.к. функция обязательно должна что-то вернуть
я один не понимаю смысл использования перечислений?
Я тоже их недолюбливаю, и никогда не использовал. Хотя, не отрицаю, что иногда с ними удобнее.
Зачем использовать break после default, если switch заканчивается?
Зачем использовать break в default, если после него switch заканчивается?
Обычно break после default и не ставят
Не лучше ли инициализировать и\или объявлять переменные до лейбла case?
В статье рассматривается как можно делать. Как делать конкретно вам — дело ваше.
Работает тоже, но вопрос другой не совсем пойму как работает std::cin, если ввести "10 +50" или "10+50" или 10 + 50" или любое количество пробелов, ответ будет правильным. Std::cin отбрасывает все пробелы? пробел это не char?
У вас в программе 3 cin-а подряд. Если вы ввели первое значение, а затем пробелы, то для первого cin эти пробелы означают окончание потока входных данных. Затем у вас сразу же после пробелов и появления второго значения срабатывает второй cin (в который записывается только второе значение, без пробелов), затем третий.
Если вы для одного cin введете число 3, а затем 4 пробела, а затем снова 6 и только тогда нажмете Enter, то в переменную сохраниться только 3. Всё, что идёт за пробелами, после первого значения, в входном потоке cin — игнорируется. Попробуйте сами.
А если вот мне просто не нравится switch априори могу же я продолжать использовать if else?
В этих уроках рассказывается, что есть в C++ и как это использовать. А уже использовать ли это вам — это дело ваше. Если вам лучше использовать if else — используйте if else.