Раздел 5 — Ветвления

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

Прогресс курса Раздел 5 из 28

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

2 академических часа теории и 2 часа практики: практическая работа №3 «Разветвляющиеся программы» — на портале. Все программы страницы — полные; собирайте их командой gcc -std=c11 -Wall -Wextra -pedantic файл.c -o файл.
01

Условие: истина и ложь

Условие — выражение, значение которого понимается как «да» или «нет». В C нет отдельного логического значения в обычных выражениях: ноль — ложь, любое другое значение — истина. Операции сравнения дают int: 1, если верно, 0, если нет.

ОперацияСмыслПример при a = 5
==равноa == 5 → 1
!=не равноa != 5 → 0
< <=меньше, не большеa < 5 → 0, a <= 5 → 1
> >=больше, не меньшеa > 3 → 1

Для читаемости есть <stdbool.h>: тип bool и значения true (1) и false (0). bool is_leap = year % 4 == 0; читается лучше, чем int с тем же смыслом. Курс использует bool для переменных-флагов.

02

if, else и блок

if (условие) {
    инструкции, если условие истинно
} else {
    инструкции, если ложно          /* ветка else может отсутствовать */
}

Фигурные скобки объединяют инструкции в блок (составной оператор): для if блок — одна инструкция, сколько бы строк в нём ни было. Пример — корни квадратного уравнения ax² + bx + c = 0 при a ≠ 0:

/* Корни квадратного уравнения */
#include <stdio.h>
#include <math.h>

int main(void)
{
    double a = 0, b = 0, c = 0;
    printf("a b c: ");
    if (scanf("%lf %lf %lf", &a, &b, &c) != 3 || a == 0) {
        printf("Error: need three numbers, a != 0\n");
        return 1;
    }

    double d = b * b - 4 * a * c;
    if (d < 0) {
        printf("No real roots\n");
    } else if (d == 0) {
        printf("One root: %.4f\n", -b / (2 * a));
    } else {
        double root = sqrt(d);
        printf("Two roots: %.4f %.4f\n", (-b - root) / (2 * a), (-b + root) / (2 * a));
    }
    return 0;
}
a b c: 1 -3 2      ->  Two roots: 1.0000 2.0000
a b c: 1 2 1       ->  One root: -1.0000
a b c: 1 0 1       ->  No real roots
a b c: 0 2 1       ->  Error: need three numbers, a != 0

Переменная root объявлена внутри блока и видна только в нём: после закрывающей скобки её нет. Объявлять переменную там, где она нужна, — правило курса: так её не перепутать с другой и не использовать там, где у неё нет смысла.

d == 0 для вещественных. Здесь сравнение с нулём допустимо только потому, что пример учебный: при a, b, c, вычисленных с погрешностью, дискриминант «почти ноль» превратится в два почти равных корня. В практической работе сравнение вещественных — с допуском (раздел 3, пункт 04): fabs(d) < EPS.
Правило курса: фигурные скобки у каждой ветки, даже из одной инструкции. Без скобок к if относится только первая инструкция; вторая, добавленная позже с тем же отступом, выполнится всегда.
03

Логические операции и сокращённое вычисление

ОперацияЗначениеПример
&& (и)1, если истинны оба операндаx >= 1 && x <= 10 — x в отрезке [1; 10]
|| (или)1, если истинен хотя бы одинx < 1 || x > 10 — x вне отрезка
! (не)1, если операнд ложен!(x >= 1 && x <= 10) — то же, что строкой выше

Математическая запись 1 ≤ x ≤ 10 в C — только через &&. 1 <= x <= 10 собирается, но значит другое: (1 <= x) <= 10, а результат сравнения — 0 или 1, что всегда ≤ 10. gcc предупреждает: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses].

Сокращённое вычисление. && и || вычисляют левый операнд первым и не вычисляют правый, если результат уже ясен: у && — когда левый ложен, у || — когда левый истинен. На этом строятся защитные условия:

if (count != 0 && sum / count > 100) { … }      /* деления на 0 не будет */
if (scanf("%d", &n) != 1 || n < 1) { … }      /* n проверяется, только если прочитано */

Порядок важен: в sum / count > 100 && count != 0 деление выполнится раньше проверки. Законы де Моргана помогают переписать отрицание: !(a && b) = !a || !b, !(a || b) = !a && !b. «Не (x от 1 до 10)» — «x меньше 1 или больше 10», а не «и».

Високосный год — классическое условие из трёх частей: год делится на 4 и не делится на 100, или делится на 400:

bool is_leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;

2024 — високосный, 2100 — нет (делится на 100), 2000 — да (делится на 400), 2023 — нет. && выполняется раньше ||, поэтому скобки здесь не обязательны — но с ними условие читается без таблицы приоритетов, и gcc без них предупреждает suggest parentheses around '&&' within '||'.

04

Цепочка else if и вложенные ветвления

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

/* Оценка по баллам (0..100) */
#include <stdio.h>

int main(void)
{
    int score = 0;
    printf("Score: ");
    if (scanf("%d", &score) != 1 || score < 0 || score > 100) {
        printf("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;
    }
    printf("Mark: %d\n", mark);
    return 0;
}
Score: 85  ->  Mark: 5        Score: 84  ->  Mark: 4        Score: 50  ->  Mark: 3
Score: 49  ->  Mark: 2        Score: 0   ->  Mark: 2        Score: 101 ->  Error: score must be 0..100

Во второй ветке не нужно писать score >= 70 && score < 85: до неё доходят только баллы меньше 85. Поэтому порядок проверок важен — если начать с score >= 50, все от 50 до 100 получат «3». Тесты — на границах каждого условия: 85 и 84, 70 и 69, 50 и 49, 0 и 100, и за пределами: −1 и 101.

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

if (x > 0)
    if (y > 0)
        printf("first quarter\n");
else                                       /* относится к if (y > 0)! */
    printf("x <= 0\n");

При x = 1, y = −1 программа выведет x <= 0. gcc предупреждает: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]. Скобки у каждой ветки снимают проблему.

05

switch: выбор по значению

Когда действие выбирается по конкретному значению целого выражения (номер дня, код команды, символ операции), удобен switch:

/* Калькулятор: число, операция, число */
#include <stdio.h>

int main(void)
{
    double a = 0, b = 0;
    char op = 0;
    printf("a op b: ");
    if (scanf("%lf %c %lf", &a, &op, &b) != 3) {
        printf("Error: expected a op b\n");
        return 1;
    }

    switch (op) {
    case '+':
        printf("%g\n", a + b);
        break;
    case '-':
        printf("%g\n", a - b);
        break;
    case '*':
    case 'x':                              /* два значения — одно действие */
        printf("%g\n", a * b);
        break;
    case '/':
        if (b == 0) {
            printf("Error: division by zero\n");
            return 1;
        }
        printf("%g\n", a / b);
        break;
    default:
        printf("Error: unknown operation '%c'\n", op);
        return 1;
    }
    return 0;
}
a op b: 7 / 2      ->  3.5
a op b: 3 x 4      ->  12
a op b: 1 / 0      ->  Error: division by zero
a op b: 2 ^ 3      ->  Error: unknown operation '^'
Забытый break — самая частая ошибка switch: при op = '+' без первого break программа выведет и сумму, и разность. gcc с -Wextra предупреждает this statement may fall through [-Wimplicit-fallthrough=]. Если проваливание после инструкций задумано, перед следующей меткой ставят комментарий /* fall through */ — gcc его понимает и не предупреждает, а читатель видит, что break пропущен не случайно.
06

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

условие ? значение1 : значение2 — выражение: если условие истинно, его значение — значение1, иначе значение2. Удобно, когда нужно выбрать значение, а не действие:

int max = (a > b) ? a : b;
printf("%d item%s\n", count, (count == 1) ? "" : "s");   /* 1 item, 5 items */

Вложенные ?: читаются плохо: a > b ? (a > c ? a : c) : (b > c ? b : c) — это максимум из трёх, но понять это можно только трассировкой. Если выбор сложнее двух значений — if.

07

Проверка ввода

С этого раздела каждая программа курса проверяет ввод: результат scanf и все ограничения задачи. При ошибке — сообщение и завершение с кодом 1 (раздел 1, пункт 05): так скрипт или другая программа, запустившая эту, узнает о неудаче.

/* Шаблон: ввод с проверкой */
int students = 0, seats = 0;
printf("Students and seats: ");
if (scanf("%d %d", &students, &seats) != 2) {
    printf("Error: two integers expected\n");
    return 1;
}
if (students < 1 || seats < 1) {
    printf("Error: both numbers must be >= 1\n");
    return 1;
}
/* дальше данные гарантированно допустимы */
08

Ловушки ветвлений

ОшибкаЧто происходитПредупреждение gcc
if (x = 5) вместо ==x становится 5, условие всегда истинноsuggest parentheses around assignment used as truth value [-Wparentheses]
if (x > 0); — точка с запятойпустое тело; блок ниже выполняется всегдаsuggest braces around empty body in an 'if' statement [-Wempty-body]
1 <= x <= 10всегда истинноcomparisons like 'X<=Y<=Z' … [-Wparentheses]
else без скобок у вложенного ifelse относится к ближайшему if-Wdangling-else
нет break в caseвыполняется и следующий case-Wimplicit-fallthrough
d == 0.3 для doubleложь из-за погрешностинет (есть -Wfloat-equal, не входит в -Wall)
неверный порядок в цепочке else ifнижние ветки недостижимынет
граница > вместо >=неверный результат ровно на границенет
Последние три ошибки компилятор не видит. Их ловят тесты: по одному на каждую ветку и по паре на каждую границу условия — значение на границе и соседнее за ней.

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

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

  • Ноль — ложь, остальное — истина; сравнения дают 0 или 1; для флагов — bool
  • Фигурные скобки у каждой ветки; переменная блока видна только в нём
  • && и || не вычисляют правый операнд, если результат ясен по левому — так строятся защитные условия
  • «x от a до b» — x >= a && x <= b, не a <= x <= b
  • В цепочке else if выполняется первая истинная ветка; порядок проверок важен
  • switch — по значению целого выражения; break в каждом case, default обязателен
  • Каждая программа проверяет результат scanf и ограничения до вычислений; при ошибке — сообщение и код 1
  • Тесты ветвлений — по одному на ветку и по паре на каждую границу
Связи раздела. Опирается на раздел 2 — ветвление как базовая структура, блок-схема с ромбом; раздел 3 — погрешность double, char как число; раздел 4 — выражения, приоритеты, результат scanf. Нужен для: раздела 6 — условие цикла, проверка ввода в цикле, break; всех практических работ дальше — проверка ввода обязательна.
Практическая работа №3 и тест по разделу 5 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 4: Выражения, ввод и вывод Все разделы Раздел 6: Циклы