7.5 – Проваливание и область видимости в switch
Этот урок является продолжением нашего исследования операторов switch
, которое мы начали в предыдущем уроке «7.4 – Основы работы с оператором switch
». В предыдущем уроке мы упоминали, что каждый набор инструкций под меткой должен заканчиваться инструкцией break
или инструкцией return
.
В этом уроке мы выясним, почему, и поговорим о некоторых проблемах, связанных с областью видимости switch
, которые иногда сбивают с толку начинающих программистов.
Проваливание
Когда выражение switch
соответствует метке case
или необязательной метке default
, выполнение начинается с первой инструкции, следующей после соответствующей метки. Затем выполнение будет продолжаться последовательно до тех пор, пока не произойдет одно из следующих условий завершения:
- будет достигнут конец блока
switch
; - другая инструкция управления порядком выполнения программы (обычно
break
илиreturn
) вызовет выход из блокаswitch
или из функции; - что-то еще прервет нормальный порядок выполнения программы (например, ОС закроет программу, вселенная взорвется и т.д.).
Обратите внимание, что наличие другой метки case
не является одним из этих условий завершения – таким образом, без break
или return
выполнение будет переходить в последующие метки.
Вот программа, которая демонстрирует такое поведение:
#include <iostream>
int main()
{
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'; // Это тоже выполняется
}
return 0;
}
Эта программа выводит следующее:
2
3
4
5
Наверное, это не то, что мы хотели! Когда выполнение переходит от инструкции под меткой к инструкциям под следующей меткой, это называется проваливанием.
Предупреждение
Как только инструкции под меткой case
или default
начнут выполняться, они будут переходить (проваливаться) в последующие метки. Для предотвращения этого обычно используются инструкции break
или return
.
Поскольку проваливание редко бывает желательным или преднамеренным, многие компиляторы и инструменты анализа кода помечают проваливание предупреждением.
Атрибут [[fallthrough]]
Комментирование преднамеренного проваливания – это обычное правило, чтобы сообщить другим разработчикам, что оно намеренное. Хотя это работает с другими разработчиками, компиляторы и инструменты анализа кода не умеют интерпретировать комментарии, поэтому это не позволяет избавиться от предупреждений.
Чтобы помочь решить эту проблему, C++17 добавил новый атрибут, называемый [[fallthrough]]
, который можно использовать в сочетании с пустой инструкцией, чтобы указать, что проваливание является преднамеренным (и предупреждения не должны для него выдаваться):
#include <iostream>
int main()
{
switch (2)
{
case 1:
std::cout << 1 << '\n';
break;
case 2:
std::cout << 2 << '\n'; // Выполнение начинается здесь
[[fallthrough]]; // преднамеренное проваливание - обратите внимание
// на точку с запятой, указывающую на пустую инструкцию
case 3:
std::cout << 3 << '\n'; // Это тоже выполняется
break;
}
return 0;
}
Эта программа печатает:
2
3
И этот код не должен генерировать никаких предупреждений о проваливании.
Лучшая практика
Используйте атрибут [[fallthrough]]
(вместе с пустой инструкцией), чтобы указать на преднамеренное проваливание.
Последовательные метки case
Чтобы объединить несколько проверок в одну инструкцию, в случае с операторами if
вы можете использовать логический оператор ИЛИ:
bool isVowel(char c)
{
return (c=='a' || c=='e' || c=='i' || c=='o' || c=='u' ||
c=='A' || c=='E' || c=='I' || c=='O' || c=='U');
}
Этот подход страдает теми же проблемами, которые мы представили во введении к операторам switch
: вычисление выполняется несколько раз, и читатель должен убедиться, что каждый раз вычисляется именно c
.
Вы можете сделать что-то подобное, используя оператор switch
, разместив несколько меток case
последовательно:
bool isVowel(char c)
{
switch (c)
{
case 'a': // если c равно 'a'
case 'e': // или если c равно 'e'
case 'i': // или если c равно 'i'
case 'o': // или если c равно 'o'
case 'u': // или если c равно 'u'
case 'A': // или если c равно 'A'
case 'E': // или если c равно 'E'
case 'I': // или если c равно 'I'
case 'O': // или если c равно 'O'
case 'U': // или если c равно 'U'
return true;
default:
return false;
}
}
Помните, выполнение начинается с первой инструкции после соответствующей метки case
. Метки case
– это не инструкции, поэтому они не учитываются.
Первая инструкция после всех меток case
в приведенной выше программе – return true;
, поэтому, если есть совпадение с какой-либо меткой case
, функция вернет true
.
Таким образом, мы можем «складывать» метки case
, чтобы впоследствии все эти метки case
использовали один и тот же набор инструкций. Это не считается поведением проваливания, поэтому использование комментариев или [[fallthrough]]
здесь не требуется.
Область видимости switch
С операторами if
у вас может быть только одна инструкция после условия if
, и эта инструкция неявно помещается внутрь блока:
if (x > 10)
std::cout << x << " is greater than 10\n"; // эта строка неявно считается находящейся внутри блока
Однако с операторами switch
все инструкции после меток относятся к блоку switch
. Неявные блоки не создаются.
switch (1)
{
case 1:
foo();
break;
default:
std::cout << "default case\n";
break;
}
В приведенном выше примере 2 инструкции между case 1
и меткой default
имеют область видимости как часть блока switch
, а не как неявный блок для case 1
.
Объявление и инициализация переменных внутри инструкций case
Вы можете объявлять (но не инициализировать) переменные внутри switch
как до, так и после меток case
:
switch (1)
{
int a; // хорошо: объявление разрешено перед метками case
int b{ 5 }; // недопустимо: инициализация не разрешена до меток case
case 1:
int y; // хорошо, но это плохая практика: объявление разрешено внутри case
y = 4; // хорошо: присвоение разрешено
break;
case 2:
y = 5; // хорошо: y была объявлена выше, поэтому мы можем использовать ее и здесь
break;
case 3:
int z{ 4 }; // недопустимо: инициализация не разрешена внутри case
break;
}
Обратите внимание, что хотя переменная y
была определена внутри case 1
, она также использовалась внутри case 2
. Поскольку инструкции в каждом case
не находятся внутри неявных блоков, это означает, что все инструкции внутри switch
являются частью одной и той же области видимости. Таким образом, переменная, определенная в одном case
, может использоваться в более позднем case
, даже если case
, в котором определена переменная, никогда не выполняется! Другими словами, определение переменной без инициализатора просто сообщает компилятору, что с этого момента переменная находится в области видимости. Это не требует, чтобы определение действительно выполнялось.
Однако инициализация переменных запрещена и вызовет ошибку компиляции. Это связано с тем, что для инициализации переменной требуется выполнение, и инициализацию можно пропустить в зависимости от того, какие метки case
выполняются.
Если для case
необходимо определить и/или инициализировать новую переменную, лучше всего сделать это внутри блока под меткой case
:
switch (1)
{
case 1:
{ // обратите внимание на добавление здесь блока
int x{ 4 }; // хорошо, переменные можно инициализировать внутри блока внутри case
std::cout << x;
break;
}
default:
std::cout << "default case\n";
break;
}
Правило
Если вы определяете переменные, используемые в case
, делайте это в блоке внутри case
(или перед switch
, если необходимо)
Небольшой тест
Вопрос 1
Напишите функцию с именем calculate()
, которая принимает два целых числа int
и символ char
, представляющий одну из следующих математических операций: +
, -
, *
, /
или %
(остаток от деления). Используйте оператор switch
, чтобы выполнить соответствующую математическую операцию с целыми числами и вернуть результат. Если в функцию передан недопустимый оператор, функция должна вывести ошибку. Для оператора деления выполните целочисленное деление.
Подсказка: "operator
" – это ключевое слово, переменные не могут называться "operator".
Ответ
#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; }