Раздел 4 — Ветвления: if и switch

Как программа выбирает, что делать: условия, проверка данных и выбор из многих вариантов

Прогресс курса Раздел 4 из 20

Что вы освоите в этом разделе

3 академических часа теории и 6 часов практики: практическая работа №3 по if и №4 по switch — на портале. Это первые работы, в которых сдаётся программа. Все программы страницы — полные; собирайте их командой g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe.
01

Инструкция if и составной оператор

До сих пор инструкции выполнялись одна за другой — следование из раздела 1. Ветвление выполняет инструкцию только при условии:

if (условие) {
    инструкции       // выполняются, если условие истинно
} else {
    инструкции       // выполняются, если условие ложно
}

Условие — любое выражение в круглых скобках, чаще всего сравнение или логическое выражение из раздела 3. Если у выражения не bool, а число, то 0 означает «ложь», всё остальное — «истину». Часть else не обязательна. Фигурные скобки делают из нескольких инструкций одну — составной оператор, или блок (раздел 2).

#include <iostream>

int main()
{
    int number;
    std::cout << "Number: ";
    std::cin >> number;

    if (number < 0) {
        number = -number;
        std::cout << "negative, abs = " << number << '\n';
    }

    if (number % 2 == 0) {
        std::cout << "even\n";
    } else {
        std::cout << "odd\n";
    }
    return 0;
}
Number: -7                  Number: 12
negative, abs = 7           even
odd

Первое if без else: при неотрицательном числе блок пропускается целиком. Второе выбирает одну из двух ветвей — выполнится ровно одна.

Правило курса: фигурные скобки у каждой ветви, даже из одной инструкции. Без скобок к if относится только одна следующая инструкция; вторая, добавленная позже с тем же отступом, выполнится всегда — отступ компилятор не читает. Открывающая скобка ветви — в конце строки с if, else — на строке с закрывающей скобкой, как в примере (у функции скобка по-прежнему на отдельной строке).
02

Проверка входных данных

В разделе 1 у каждого исходного данного модели записаны ограничения, а у алгоритма — ветка «данные недопустимы». Теперь это код. Модель варианта 3 из раздела 1 — стоимость доставки посылки: до 1 кг — 250 ₽, каждый следующий начатый килограмм — 60 ₽, тяжелее 20 кг не принимаются.

// Стоимость доставки посылки — модель варианта 3 из раздела 1
#include <iostream>
#include <iomanip>
#include <cmath>

constexpr double BASE_PRICE = 250.0;        // до 1 кг включительно
constexpr double PRICE_PER_KG = 60.0;       // каждый следующий начатый килограмм
constexpr double MAX_WEIGHT = 20.0;

int main()
{
    double weight;
    std::cout << "Weight, kg: ";
    std::cin >> weight;

    if (weight <= 0 || weight > MAX_WEIGHT) {
        std::cout << "Error: weight must be in (0; " << MAX_WEIGHT << "] kg\n";
        return 1;
    }

    double cost = BASE_PRICE;
    if (weight > 1) {
        cost += PRICE_PER_KG * std::ceil(weight - 1);
    }
    std::cout << std::fixed << std::setprecision(2) << "Cost: " << cost << " RUB\n";
    return 0;
}
Weight, kg: 3.2             Weight, kg: 1               Weight, kg: 25
Cost: 430.00 RUB            Cost: 250.00 RUB            Error: weight must be in (0; 20] kg
Тесты ветвления берутся из модели: обычное значение (3.2), граница, которая допустима (1 и 20), граница, которая недопустима (0), значение за пределом (25). Каждая ветвь программы должна выполниться хотя бы на одном тесте — иначе ошибка в ней не будет найдена до сдачи. Так составляются таблицы тестов в практических работах.

Что будет, если ввести не число, а буквы, — раздел 6: там появится проверка самого ввода.

03

Вложенные условия и «висячий» else

В ветви if может стоять другой if — это вложенное условие. Оно нужно, когда второе условие имеет смысл проверять только при выполнении первого. Главная ловушка — else без скобок:

if (age >= 14)
    if (ticket)
        std::cout << "pass\n";
else
    std::cout << "too young\n";

По отступам кажется, что else относится к проверке возраста. Но правило языка: else относится к ближайшему if без else — здесь к if (ticket). Пятнадцатилетний без билета увидит «too young». g++ предупреждает:

dangling.cpp:7:8: warning: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]

Со скобками смысл виден и компилятору, и читателю:

if (age >= 14) {
    if (ticket) {
        std::cout << "pass\n";
    }
} else {
    std::cout << "too young\n";
}

Больше двух-трёх уровней вложенности читать трудно. Часто вложенность заменяется одним условием с && (age >= 14 && ticket) или ранним выходом, как проверка данных в пункте 02.

04

Цепочка else if

Когда вариантов больше двух — например, оценка по шкале курса 85 / 70 / 50, — условия выстраиваются цепочкой. Проверяются по порядку сверху вниз; выполняется ветвь первого истинного условия, остальные пропускаются.

#include <iostream>

int main()
{
    int score;
    std::cout << "Score: ";
    std::cin >> score;

    if (score < 0 || score > 100) {
        std::cout << "Error: score must be 0..100\n";
        return 1;
    }

    int mark;
    if (score >= 85) {
        mark = 5;
    } else if (score >= 70) {
        mark = 4;
    } else if (score >= 50) {
        mark = 3;
    } else {
        mark = 2;
    }
    std::cout << "Mark: " << mark << '\n';
    return 0;
}
Score: 85  →  Mark: 5        Score: 84  →  Mark: 4
Score: 50  →  Mark: 3        Score: 49  →  Mark: 2

Второе условие — просто score >= 70, без && score < 85: если выполнение дошло до него, первое условие уже оказалось ложным. Последний else ловит всё, что не подошло, — поэтому mark получает значение на любом пути, и компилятор не предупреждает о неинициализированной переменной.

Порядок условий важен. Если начать цепочку с score >= 50, то и 95 баллов дадут «3»: первое же условие истинно, до остальных дело не дойдёт. Для порогов «не меньше» проверяйте от большего к меньшему. Проверяйте и стыки интервалов: для 85 и 84, 70 и 69 должны выходить разные оценки — ровно на них путают > и >=.
05

Условная операция ?:

Условная операция условие ? выражение1 : выражение2 — ветвление внутри выражения: её значение равно первому выражению, если условие истинно, иначе второму. Она нужна, когда выбирается значение, а не действие.

#include <iostream>

int main()
{
    int a, b;
    std::cout << "Two numbers: ";
    std::cin >> a >> b;

    int larger = (a > b) ? a : b;
    std::cout << "max = " << larger << '\n';
    std::cout << a << (a == 1 ? " file" : " files") << '\n';
    return 0;
}
Two numbers: 1 5            Two numbers: 7 3
max = 5                     max = 7
1 file                      7 files

У ?: очень низкий приоритет — ниже <<, поэтому в выводе она всегда в скобках, как логические выражения (раздел 3, пункт 10). Оба выражения должны быть одного типа или приводиться к общему. Вложенные ?: (a ? b : c ? d : e) в курсе не используются — для трёх и больше вариантов есть цепочка else if.

06

Условия без ловушек

= вместо ==. if (count = 0) собирается: это присваивание нуля, его значение 0 — «ложь». Ветвь не выполнится никогда, а count молча испорчен. g++ с -Wall предупреждает:

assignif.cpp:6:15: warning: suggest parentheses around assignment used as truth value [-Wparentheses]

Точка с запятой после условия. if (balance < 0); — это if с пустой инструкцией ;. Строка ниже к условию не относится и выполняется всегда, как бы она ни была сдвинута:

emptyif.cpp:6:21: warning: suggest braces around empty body in an 'if' statement [-Wempty-body]
emptyif.cpp:6:5: warning: this 'if' clause does not guard... [-Wmisleading-indentation]

Сравнение вещественных чисел — с допуском, а не через == (раздел 3, пункт 04): числа считаются равными, если модуль разности меньше малого числа EPS. std::fabs из <cmath> — модуль вещественного числа. Допуск подбирается по задаче: для денег в рублях — 0.005, для геометрии в метрах — 1e-9.

Сокращённое вычисление && защищает от деления на ноль: если divisor != 0 ложно, деление справа не выполняется.

#include <iostream>
#include <cmath>

constexpr double EPS = 1e-9;

int main()
{
    double sum = 0.1 + 0.2;
    if (std::fabs(sum - 0.3) < EPS) {
        std::cout << "0.1 + 0.2 equals 0.3 within " << EPS << '\n';
    }

    int divisor;
    std::cout << "Divisor: ";
    std::cin >> divisor;
    if (divisor != 0 && 100 / divisor > 5) {
        std::cout << "100 / " << divisor << " > 5\n";
    } else {
        std::cout << "divisor is 0 or quotient <= 5\n";
    }
    return 0;
}
0.1 + 0.2 equals 0.3 within 1e-09
Divisor: 0                          <- при 10: "100 / 10 > 5"
divisor is 0 or quotient <= 5
Все три ловушки собираются и проходят невнимательную проверку; две из них видны только по предупреждениям. Поэтому правило «ни одного предупреждения» (раздел 2) в ветвлениях особенно важно. Порядок операндов у && тоже важен: 100 / divisor > 5 && divisor != 0 делит раньше, чем проверяет.
07

Инструкция switch

Когда действие выбирается по значению одной переменной — пункт меню, код операции, номер месяца, — цепочка if (x == 1) … else if (x == 2) … заменяется инструкцией switch:

switch (выражение) {
case константа1:
    инструкции
    break;
case константа2:
    инструкции
    break;
default:
    инструкции       // ни одна метка не подошла
}

Выражение вычисляется один раз, затем выполнение переходит на метку case с равным значением и идёт вниз до break, который выходит из switch. Если равной метки нет — переход на default, а если нет и его, switch ничего не делает. Калькулятор на четыре действия:

#include <iostream>

int main()
{
    double left, right;
    char operation;
    std::cout << "Expression (e.g. 7 / 2): ";
    std::cin >> left >> operation >> right;

    switch (operation) {
    case '+':
        std::cout << left + right << '\n';
        break;
    case '-':
        std::cout << left - right << '\n';
        break;
    case '*':
    case 'x':
        std::cout << left * right << '\n';
        break;
    case '/':
        if (right == 0) {
            std::cout << "Error: division by zero\n";
            return 1;
        }
        std::cout << left / right << '\n';
        break;
    default:
        std::cout << "Error: unknown operation '" << operation << "'\n";
        return 1;
    }
    return 0;
}
Expression (e.g. 7 / 2): 7 / 2      -> 3.5
Expression (e.g. 7 / 2): 6 x 7      -> 42
Expression (e.g. 7 / 2): 1 / 0      -> Error: division by zero
Expression (e.g. 7 / 2): 1 % 2      -> Error: unknown operation '%'

std::cin >> в переменную char читает один символ, пропуская пробелы, — поэтому выражение вводится как в тетради. Метки case '*': и case 'x': стоят подряд: для обоих символов выполняется одно и то же. Здесь right == 0 без допуска — правильно: опасен только точный ноль, а не «почти ноль». Внутри ветви case можно писать любые инструкции, в том числе if.

Правило курса: у каждого switch есть default. Он обрабатывает недопустимые значения — это та же проверка входных данных из пункта 02. Оформление: case на уровне switch, инструкции ветви сдвинуты на 4 пробела, break — последней строкой ветви.
08

Проваливание и переменные внутри case

Метка case — только точка входа, а не граница ветви. Без break выполнение идёт дальше — в инструкции следующей метки. Это проваливание (fall-through). Намеренно его используют, чтобы объединить несколько значений в одну ветвь, как '*' и 'x' выше. Число дней в месяце:

#include <iostream>

int main()
{
    int month, year;
    std::cout << "Month and year: ";
    std::cin >> month >> year;

    int days;
    switch (month) {
    case 1: case 3: case 5: case 7: case 8: case 10: case 12:
        days = 31;
        break;
    case 4: case 6: case 9: case 11:
        days = 30;
        break;
    case 2: {
        bool leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
        days = leap ? 29 : 28;
        break;
    }
    default:
        std::cout << "Error: month must be 1..12\n";
        return 1;
    }
    std::cout << "Days: " << days << '\n';
    return 0;
}
Month and year: 2 2024   ->  Days: 29       Month and year: 2 1900   ->  Days: 28
Month and year: 2 2000   ->  Days: 29       Month and year: 9 2026   ->  Days: 30

Год високосный, если делится на 4, но не на 100, — или делится на 400: 1900 не високосный, 2000 — високосный. Метки без инструкций между ними (case 1: case 3: …) — законное объединение, о нём компилятор не предупреждает. А вот пропущенный break после инструкций -Wextra замечает:

fallthrough.cpp:8:22: warning: this statement may fall through [-Wimplicit-fallthrough=]

Если проваливание после инструкций действительно нужно, это пишут явно — строкой [[fallthrough]]; на месте break: предупреждение исчезнет, а читатель увидит, что так задумано.

Переменная внутри case — только в фигурных скобках, как leap в ветви февраля. Все метки switch лежат в одном блоке, и переход на следующую метку «перепрыгнул» бы инициализацию переменной. Без скобок сборка останавливается ошибкой jump to case label с пояснением crosses initialization of 'bool leap'.
09

Ограничения switch; switch и enum class

switch проверяет только равенство целому значению. Отсюда ограничения — все проверяет компилятор:

НельзяСообщение g++Что делать
выражение типа double или std::stringswitch quantity not an integerцепочка else if
метка — переменная: case limit:the value of 'limit' is not usable in a constant expressionметка — литерал, constexpr или константа enum
две одинаковые меткиduplicate case valueобъединить ветви
диапазон: «от 70 до 84»такой записи нетцепочка else if (пункт 04)

switch по перечислению — естественная пара: каждая константа enum — своя ветвь. Для enum class g++ проверяет, что разобраны все значения: если забыть одно, -Wall предупредит enumeration value 'GREEN' not handled in switch [-Wswitch]. Число из ввода превращается в enum class через static_cast — после проверки диапазона:

#include <iostream>

enum class Light { RED, YELLOW, GREEN };

int main()
{
    int code;
    std::cout << "Light (0 red, 1 yellow, 2 green): ";
    std::cin >> code;
    if (code < 0 || code > 2) {
        std::cout << "Error: code must be 0..2\n";
        return 1;
    }
    Light light = static_cast<Light>(code);

    switch (light) {
    case Light::RED:
        std::cout << "stop\n";
        break;
    case Light::YELLOW:
        std::cout << "wait\n";
        break;
    case Light::GREEN:
        std::cout << "go\n";
        break;
    }
    return 0;
}
Light (0 red, 1 yellow, 2 green): 2
go

Здесь default нет намеренно — это исключение из правила пункта 07: недопустимые значения отсеяны до switch, а без default компилятор напомнит о новой константе, если её добавят в Light.

ЗадачаИнструкция
одно условие, одно или два действияif / if-else
выбрать одно из двух значений?:
интервалы, сравнения < >, вещественные числа, несколько переменныхцепочка else if
одна целая переменная, char или enum, много отдельных значенийswitch

В Windows API switch встречается постоянно: по коду ошибки GetLastError() (раздел 14), по коду нажатой клавиши (раздел 15), по состоянию объекта ожидания (раздел 16).

Ключевые выводы раздела

Запомните главное

  • if выполняет ветвь при истинном условии, else — при ложном; скобки { } — у каждой ветви
  • Входные данные проверяются сразу после ввода; при нарушении — сообщение и return 1;
  • else относится к ближайшему if без else; в цепочке else if выполняется первая истинная ветвь — пороги от большего к меньшему
  • = в условии, ; после условия и == для double — ошибки; первые две видны только по предупреждениям
  • switch переходит на равную метку и идёт до break; без break — проваливание в следующую ветвь
  • У switch есть default; переменные внутри case — в фигурных скобках; метки — только константы целого типа
  • Тесты подбираются так, чтобы каждая ветвь выполнилась, плюс значения на границах каждого условия
Связи раздела. Опирается на раздел 1 — ограничения модели и ветвление в блок-схеме; раздел 2 — код завершения return; раздел 3 — сравнения, логические операции и их приоритет, bool, enum class, точность double. Нужен для: разделов 5–6 — условия циклов и выход из них, проверка ввода и меню программы на switch; раздела 7 — функции, возвращающие результат проверки; разделов 14–19 — проверка результата каждой функции Windows API и разбор кода ошибки.
Практические работы №3 и №4 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 3: Типы и операции Практика на портале