Перейти к содержимому
Topos Uranos

Краткое содержание

Программа, которая лишь выстраивает инструкции одну за другой, всегда делает одно и то же, каковы бы ни были её данные; чтобы откликаться на них, ей нужно принимать решения. В этом уроке изучаются ветвления в C++: if и else, которые выбирают между двумя путями в зависимости от условия; цепочки else if, которые выбирают между несколькими путями и в которых порядок условий определяет, какой случай обслуживает каждая ветвь; вложенные ветвления и ловушка висячего else; и switch с enum class, которые выбирают между дискретными именованными значениями, причём компилятор предупреждает, если случай пропущен или если один случай проваливается в следующий. Понятие, которое всё это объединяет, есть разбор случаев: правильное ветвление разбивает множество входных данных на непересекающиеся случаи, которые покрывают его полностью, и каждый случай проверяется одним тестом внутри него и ещё одним на каждой из его границ.

Цели обучения

  1. Писать простые, цепочечные и вложенные ветвления, которые обслуживают все случаи спецификации.
  2. Убеждаться, что случаи цепочки if и else if не пересекаются и исчерпывают все возможности, и составлять по одному тестовому входу на каждый случай.
  3. Использовать switch с enum class для дискретных значений и объяснять, о чём предупреждает компилятор, если случай пропущен.
  4. Обнаруживать висячий else и проваливание между случаями switch и исправлять их.

if и else

Форма ветвления

Урок От задачи к алгоритму представил три формы, из которых строится любой алгоритм: следование, ветвление и повторение. До сих пор программы курса были следованиями, если не считать отдельных if, появившихся заранее в разделе 1; этот урок записывает на C++ вторую форму. Ветвление вычисляет условие, то есть выражение типа bool, подобное выражениям урока Отношения и логика, и выполняет один блок, если условие истинно, и другой, если оно ложно:

Код на C++
if (условие) {    // выполняется, если условие истинно} else {    // выполняется, если условие ложно}

Условие всегда пишется в скобках. Часть else необязательна: без неё, если условие ложно, ничего не делается, и программа продолжает работу с инструкции, стоящей после ветвления. В обоих случаях два пути в конце сходятся, так что ветвление, если смотреть снаружи, есть одна инструкция с одним входом и одним выходом, как того требовал урок 1.1.

ПримерЕвклидов остаток

Урок Арифметические операции показал, что a % b имеет знак делимого, так что при a=−7a = -7 и b=2b = 2 он равен −1-1, тогда как евклидов остаток, который никогда не бывает отрицательным, равен 1; и указал, что надёжный способ это исправить состоит в том, чтобы прибавлять bb только тогда, когда остаток отрицателен. Программа предполагает, что b>0b > 0.

Программа на C++
#include <iostream>#include <print>​int main(){    int a{};    int b{};    std::cin >> a >> b;    int r{a % b};    if (r < 0) {        r = r + b;    }    std::println("Евклидов остаток от деления {} на {}: {}", a, b, r);}
Ввод программы
-7 2
Вывод
Евклидов остаток от деления -7 на 2: 1

Переменная r больше не константа, потому что ветвление может её изменить. Рисунок прослеживает оба возможных пути.

Доказательство

  1. Выполнение доходит до ветвления, когда r уже вычислено, и первым делом вычисляет условие r < 0: ромб есть единственная точка программы, где пути расходятся.

  2. Если условие истинно, как при −7-7 и 2, выполняется блок if: r переходит от −1-1 к 1.

  3. Если оно ложно, как при 7 и 2, блок пропускается целиком, и r сохраняет своё значение.

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

С else ветвление выбирает между двумя действиями, исключающими друг друга. Условие високосного года из урока Отношения и логика даёт bool; ветвление превращает его в поведение программы.

Программа на C++
#include <iostream>#include <print>​int main(){    int year{};    std::cin >> year;    const bool isLeap{(year % 4 == 0 && year % 100 != 0) || year % 400 == 0};    if (isLeap) {        std::println("{} високосный: в феврале 29 дней.", year);    } else {        std::println("{} не високосный: в феврале 28 дней.", year);    }}
Ввод программы
1900
Вывод
1900 не високосный: в феврале 28 дней.

Если дать условию имя, как isLeap, то решение о том, что выбирается, отделяется от того, что делается с этим выбором.

Фигурные скобки всегда

Грамматика C++ позволяет опускать фигурные скобки, когда блок состоит из одной инструкции: if (r < 0) r = r + b; допустимо. Однако без фигурных скобок ветвление управляет только непосредственно идущей за ним инструкцией, а то, что читатель считает управляемым, зависит от отступа, который компилятор не учитывает. Если это так, любое позднейшее изменение (добавить вторую инструкцию, закомментировать первую) может изменить смысл так, что текст этого не покажет. Отсюда следует правило курса: блок if или else всегда заключается в фигурные скобки, даже если в нём одна инструкция.

Самая короткая из всех ошибок подтверждает правило. Точка с запятой после скобок есть пустая инструкция, и ветвление управляет этой пустой инструкцией; идущий за ней блок уже не принадлежит if и выполняется всегда.

Программа на C++
#include <print>​int main(){    const int stock{12};    if (stock == 0);    {        std::println("Товар закончился");    }    std::println("Доступно единиц: {}", stock);}
Предупреждения компилятора
main.cpp: In function ‘int main()’:main.cpp:6:20: warning: suggest braces around empty body in an ‘if’ statement [-Wempty-body]    6 |     if (stock == 0);      |                    ^main.cpp:6:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]    6 |     if (stock == 0);      |     ^~main.cpp:7:5: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’    7 |     {      |     ^
Вывод
Товар закончилсяДоступно единиц: 12

Программа объявляет, что товар закончился, хотя осталось 12 единиц. GCC выдаёт два предупреждения: -Wempty-body советует поставить фигурные скобки вокруг пустого тела, а -Wmisleading-indentation указывает, что блок оформлен отступом так, будто им управляет if. Другая классическая ошибка, if (x = 0) вместо if (x == 0), рассматривалась в уроке Компиляция, запуск и настройка: присваивание имеет значение нуль, которое преобразуется в false, и -Wparentheses предупреждает об этом. В обоих случаях программа компилируется; без предупреждений ошибка осталась бы незамеченной.

У фигурных скобок есть ещё одно следствие: переменная, объявленная внутри блока if, существует только там. Если значение, вычисленное в одной ветви, используется после ветвления, переменная объявляется раньше, как r в примере с остатком; область видимости будет изучаться в уроке Параметры и область видимости.

Условный оператор

Когда обе ветви ветвления лишь выбирают значение, C++ предлагает более краткую форму: условие ? a : b равно a, если условие истинно, и b, если оно ложно, и вычисляется только выбранный элемент. В отличие от if, который есть инструкция, условный оператор есть выражение: у него есть значение и тип, и он может стоять везде, где стоит любое выражение, например в инициализации константы, чего if не позволил бы без отказа от const.

Программа на C++
#include <iostream>#include <print>​int main(){    int a{};    int b{};    std::cin >> a >> b;    const int larger{a > b ? a : b};    std::println("Большее: {}", larger);    std::println("{} {}", b, b == 1 ? "единица" : "единицы");    const auto mixed{a > b ? a : 0.5};    std::println("Общий тип: {:.1f}", mixed);    std::println("Без скобок: {}", 10 + a > b ? 1 : 0);    std::println("Со скобками: {}", 10 + (a > b ? 1 : 0));}
Ввод программы
7 3
Вывод
Большее: 73 единицыОбщий тип: 7.0Без скобок: 1Со скобками: 11

Первые две строки показывают использование, которое проясняет код: выбрать большее и согласовать слово с числом, не повторяя всю инструкцию в двух ветвях. Третья показывает, что у выражения один тип, какая бы ветвь ни была выбрана: если два результата имеют разные типы, они приводятся к общему типу обычными арифметическими преобразованиями из урока Арифметические операции; здесь int и double дают double, и выбранное 7 сохраняется как 7.0. Последние две показывают приоритет: условный оператор входит в число операторов с самым низким приоритетом, так что без скобок условием оказывается 10 + a > b, а значением 1, а не 11. Когда условный оператор входит в более крупное выражение, его заключают в скобки.

Оператор затемняет код, как только от него требуют большего, чем выбор значения. Вложенный, как в a > b ? (a > c ? a : c) : (b > c ? b : c), он заставляет мысленно восстанавливать вложенное ветвление без фигурных скобок, которые делают его читаемым; а используемый ради побочных эффектов, как в ok ? std::println("да") : std::println("нет"), он есть замаскированный if. Правило курса: использовать его только для выбора между двумя краткими значениями, без вложения; всё остальное пишется с if.