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? Мы рассмотрим эту и другие темы в следующем уроке.
