7.4 – Основы работы с оператором switch
Хотя несколько операторов if
-else
можно связать вместе, но это будет трудно читать и неэффективно. Рассмотрим следующую программу:
#include <iostream>
void printDigitName(int x)
{
if (x == 1)
std::cout << "One";
else if (x == 2)
std::cout << "Two";
else if (x == 3)
std::cout << "Three";
else
std::cout << "Unknown";
}
int main()
{
printDigitName(2);
return 0;
}
Хотя этот пример не слишком сложен, x
вычисляется до трех раз (что неэффективно), и читатель должен быть уверен, что каждый раз вычисляется именно x
(а не какая-то другая переменная).
Поскольку проверка переменной или выражения на равенство с набором различных значений является обычным явлением, C++ предоставляет альтернативный условный оператор, называемый оператором switch
, который специализируется на этой задаче. Вот та же программа, что и выше, с использованием switch
:
#include <iostream>
void printDigitName(int x)
{
switch (x)
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}
int main()
{
printDigitName(2);
return 0;
}
Идея оператора switch
проста: выражение (иногда называемое условием) вычисляется для получения значения. Если значение выражения равно значению после любой из меток case
, выполняются инструкции после соответствующей метки case
. Если не удается найти соответствующее значение, и метка default
(по умолчанию) существует, выполняются инструкции после метки default
.
По сравнению с исходным оператором if
преимущество оператора switch
заключается в том, что он вычисляет выражение только один раз (что делает его более эффективным), и оператор switch
также дает читателю понять, что это одно и то же выражение проверяется на равенство в каждом случае.
Лучшая практика
Если есть выбор, предпочитайте использовать операторы switch
вместо цепочек if
-else
.
Давайте рассмотрим каждую из этих концепций более подробно.
Начало switch
Оператор switch
мы начинаем с ключевого слова switch
, за которым следует условное выражение в скобках, которое мы хотели бы вычислить. Часто выражение – это всего лишь одна переменная, но это может быть любое допустимое выражение.
Единственное ограничение заключается в том, что условие должно вычисляться как целочисленный тип (если вам нужно напоминание, какие базовые типы считаются целочисленными типами, просмотрите урок «4.1 – Введение в основные типы данных»). Небазовые типы, которые можно преобразовать в int
(например, перечисляемые типы и некоторые классы), также допустимы. Выражения, которые вычисляются как типы с плавающей запятой, строки и другие нецелочисленные типы, здесь не могут использоваться.
Для продвинутых читателей
Почему switch
допускает только целочисленные типы? Ответ в том, что операторы switch
разработаны с учетом высокой степени оптимизации. Исторически сложилось так, что наиболее распространенный способ реализации компиляторами операторов switch
– это таблицы переходов, а таблицы переходов работают только с целочисленными значениями.
Для тех из вас, кто уже знаком с массивами, таблица переходов работает во многом как массив, целочисленное значение используется в качестве индекса массива для «перехода» непосредственно к результату. Это может быть намного эффективнее, чем несколько последовательных сравнений.
Конечно, компиляторам не обязательно реализовывать switch
с помощью таблиц переходов, и иногда они так и делают. Технически нет причин, по которым C++ не мог ослабить ограничение для switch
на использование только целочисленных значений, просто это еще не сделано (по крайней мере, в C++20).
После условного выражения мы объявляем блок. Внутри блока мы используем метки для определения всех значений, которые мы хотим проверить на равенство. Есть два вида меток.
Метки case
Первый вид метки – это метка case
, которая объявляется с использованием ключевого слова case
, за которым следует константное выражение. Константное выражение должно либо соответствовать типу условного выражения, либо преобразовываться в этот тип.
Если значение условного выражения равно выражению после метки case
, выполнение начинается с первой инструкции после этой метки case
, а затем продолжается последовательно.
Вот пример условия, соответствующего метке case
:
#include <iostream>
void printDigitName(int x)
{
switch (x) // x вычисляется для получения значения 2
{
case 1:
std::cout << "One";
return;
case 2: // что соответствует этому выражению case
std::cout << "Two"; // поэтому выполнение начинается здесь
return; // и потом возвращаемся в вызывающую функцию
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}
int main()
{
printDigitName(2);
return 0;
}
Этот код печатает:
Two
В приведенной выше программе x
вычисляется для получения значения 2. Поскольку имеется метка case
со значением 2, выполнение переходит к инструкции под этой совпавшей меткой case
. Программа выводит Two, а затем выполняется оператор return
, который возвращает выполнение в вызывающую функцию.
Практического ограничения на количество меток case
, которые вы можете иметь, не существует, но все метки case
в switch
должны быть уникальными. То есть так делать нельзя:
switch (x)
{
case 54:
case 54: // ошибка: значение 54 уже использовано!
case '6': // ошибка: '6' преобразуется в целое число 54, которое уже используется
}
Метка default
Второй вид меток – это метка default
(часто называемая меткой по умолчанию), которая объявляется с использованием ключевого слова default
. Если условное выражение не соответствует ни одной метке case
, и метка default
существует, выполнение начинается с первой инструкции после метки default
.
Вот пример условия, соответствующего метке по умолчанию:
#include <iostream>
void printDigitName(int x)
{
switch (x) // x вычисляется для получения значения 5
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default: // что не соответствует ни одной метке case
std::cout << "Unknown"; // поэтому выполнение начинается здесь
return; // и потом возвращаемся в вызывающую функцию
}
}
int main()
{
printDigitName(5);
return 0;
}
Этот код печатает:
Unknown
Метка по умолчанию является необязательной, и для каждого оператора switch
может быть только одна метка default
. По соглашению метка по умолчанию помещается в блок switch
последней.
Лучшая практика
Помещайте метку default
в блок switch
последней.
Прекращение выполнения
В приведенных выше примерах мы использовали операторы return
, чтобы остановить выполнение инструкций после наших меток. Однако это также приводит к выходу из всей функции.
Инструкция break
(объявленная с помощью ключевого слова break
) сообщает компилятору, что мы закончили выполнение инструкций внутри switch
, и что выполнение должно продолжаться с инструкции после конца блока switch
. Это позволяет нам выйти из оператора switch
, не выходя из всей функции.
Вот немного измененный пример, переписанный с использованием break
вместо return
:
#include <iostream>
void printDigitName(int x)
{
switch (x) // x вычисляется как 3
{
case 1:
std::cout << "One";
break;
case 2:
std::cout << "Two";
break;
case 3:
std::cout << "Three"; // выполнение начинается здесь
break; // переходим в конец блока switch
default:
std::cout << "Unknown";
break;
}
// выполнение продолжается здесь
std::cout << " Ah-Ah-Ah!";
}
int main()
{
printDigitName(3);
return 0;
}
Приведенный выше пример напечатает:
Three Ah-Ah-Ah!
Лучшая практика
Каждый набор инструкций под меткой должен заканчиваться инструкцией break
или инструкцией return
.
Итак, что произойдет, если вы не завершите набор инструкций под меткой с помощью break
или return
? Мы рассмотрим эту и другие темы в следующем уроке.