Управляющие конструкции в C: if, switch case, циклы и операторы перехода

Все управляющие конструкции языка C в одном месте: условия if и else, оператор switch case с break и default, циклы for, while и do-while, а также break, continue и goto.

Каждый пример собран в GCC 13 и Clang 17, рядом настоящий вывод программы и семь ошибок, которые компилятор помогает поймать.

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 2872 статьи автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3526 экспертных мнений Подписаться на Телеграм-канал
Обложка: Управляющие конструкции в C: if, switch case, циклы и операторы перехода

Управляющие конструкции в C решают, какая строка программы выполнится следующей. Их три группы: ветвления (if, else, switch case, тернарный оператор), циклы (for, while, do-while) и операторы перехода (break, continue, goto, return). Ниже синтаксис каждой, рабочие примеры и ошибки, на которых новички теряют больше всего времени.

Каждый пример ниже скомпилирован дважды: в GCC 13.3 на Ubuntu 24.04 и в Apple clang 17 на macOS. Вывод в блоках кода скопирован из терминала. Если компилятор ещё не стоит, начните с инструкции как выбрать и установить компилятор C, а чем на этом языке зарабатывают, рассказываем в разборе кто такой разработчик на Си.

CheckroiCheckroiПодборка курсов по C (язык Си)15 курсов • 6 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Какие управляющие конструкции есть в C

Рой пишет программу на C с ветвлениями

Сводка на одном экране. Её удобно открыть во второй вкладке и сверяться по ходу чтения.

Конструкция Что делает Когда брать
if / else if / else выполняет ветку, если условие истинно диапазоны, сложные условия, сравнение строк через функции
switch / case / default прыгает на метку с совпавшей константой одна целая переменная и набор точных значений: команда меню, код ошибки, состояние
?: выбирает одно из двух значений короткое присваивание в одну строку
for цикл со счётчиком число повторов известно заранее, обход массива
while проверяет условие перед каждым шагом повторять, пока что-то не случится
do-while проверяет условие после шага тело должно выполниться хотя бы раз
break / continue выход из цикла или switch / переход к следующему шагу досрочная остановка, пропуск лишних элементов
goto / return прыжок на метку / выход из функции очистка ресурсов при ошибке / возврат результата

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

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

Условия: if, else if и тернарный оператор

Условие в C истинно, если выражение не равно нулю. Отдельного логического типа долго не было, поэтому if (count) читается как «если count не ноль».

#include <stdio.h>

int main(void) {
    int score = 73;

    if (score >= 85) {
        printf("отлично\n");
    } else if (score >= 70) {
        printf("хорошо\n");
    } else if (score >= 50) {
        printf("удовлетворительно\n");
    } else {
        printf("не сдал\n");
    }

    const char *parity = (score % 2 == 0) ? "чётное" : "нечётное";
    printf("%d — %s\n", score, parity);
    return 0;
}
хорошо
73 — нечётное

Цепочка else if проверяется сверху вниз и останавливается на первой истинной ветке. Поэтому порядок важен: поставьте score >= 50 первым, и оценка 73 превратится в «удовлетворительно».

Тернарный оператор условие ? a : b выбирает значение для присваивания или аргумента. Выполнить им блок кода не получится, для этого есть if. Вкладывать один тернарный оператор в другой можно, но читать такой код больно.

Совет: ставьте фигурные скобки даже вокруг одной строки. Без них else привязывается к ближайшему if, даже если по отступам он стоит под другим. GCC с флагом -Wall на это пишет suggest explicit braces to avoid ambiguous 'else'.

Канал основателя Checkroi Вани Буявца3 700 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Оператор switch case в C

switch вычисляет выражение один раз и передаёт управление на метку case с тем же значением. Если совпадений нет, срабатывает default, а без него тело просто пропускается.

#include <stdio.h>

int main(void) {
    int day = 3;

    switch (day) {
        case 1:
            printf("понедельник\n");
            break;
        case 2:
            printf("вторник\n");
            break;
        case 3:
            printf("среда\n");
            break;
        default:
            printf("другой день\n");
    }
    return 0;
}
среда

Зачем нужен break и что такое проваливание

Метка case отмечает только точку входа. Дальше код идёт подряд до break или до закрывающей скобки, и соседние метки его не останавливают. Это называют проваливанием (fallthrough). Уберём все break из примера выше и передадим day = 2:

вторник
среда
другой день

Выполнились три ветки вместо одной. GCC с флагом -Wextra предупреждает о каждом таком месте: this statement may fall through [-Wimplicit-fallthrough=]. Это поведение описано в документации GCC по предупреждениям.

Clang в том же режиме молчит. Мы проверили: Apple clang 17 с -Wall -Wextra не выдал ни одного предупреждения, пока мы не добавили отдельный флаг -Wimplicit-fallthrough. Если пишете на Mac, включайте его явно.

Несколько case на одну ветку

Проваливание бывает полезным: подряд стоящие метки без кода между ними дают одну ветку на несколько значений.

#include <stdio.h>

int main(void) {
    char c = 'e';

    switch (c) {
        case 'a': case 'e': case 'i': case 'o': case 'u':
            printf("%c — гласная\n", c);
            break;
        case ' ':
            printf("пробел\n");
            break;
        default:
            printf("%c — согласная или знак\n", c);
    }
    return 0;
}
e — гласная

Пустые метки предупреждения не вызывают: компилятор ругается, только когда между метками есть код.

Тот же приём встречается в разборе команд в прошивках: несколько кодов кнопок ведут в одну ветку обработчика. Подробнее про эту работу в статье кто такой программист микроконтроллеров.

Осознанное проваливание и атрибут fallthrough

Иногда ветки должны выполняться каскадом: уровень 3 делает всё, что уровень 2, и ещё кое-что. Чтобы компилятор и коллега поняли, что break пропущен намеренно, в стандарте C23 есть атрибут [[fallthrough]];:

switch (level) {
    case 3:
        printf("бэкап базы\n");
        [[fallthrough]];
    case 2:
        printf("очистка кеша\n");
        [[fallthrough]];
    case 1:
        printf("запись в лог\n");
        break;
    default:
        printf("неизвестный уровень\n");
}

При level = 2 программа печатает «очистка кеша» и «запись в лог», а предупреждений нет. GCC 13 принимает атрибут и в режиме -std=c17, Clang тоже, но с флагом -pedantic напоминает, что это расширение C23. На старых компиляторах тот же смысл передаёт __attribute__((fallthrough)); или комментарий /* fallthrough */, который GCC понимает по шаблону.

Что можно и что нельзя в switch

Правила сведены в справочнике cppreference и в документации Microsoft по оператору switch. Все они проверяются на этапе сборки, задолго до запуска (как устроен этот этап, рассказываем в материале о компиляции простыми словами). Каждое ограничение мы проверили в GCC, ошибки ниже из его вывода:

  • Только целые типы: int, char, long, перечисления enum. Строку в switch не передать: switch quantity not an integer. Для строк остаётся цепочка if со strcmp.
  • После case только константа, известная при компиляции. Переменная даёт case label does not reduce to an integer constant. Подойдут литерал, enum или макрос #define.
  • Значения не повторяются: два case 1 дают duplicate case value.
  • default не больше одного, и стоять он может где угодно, не обязательно в конце.
  • Дробные числа не подходят: double и float сравнивайте через if.

Диапазоны вида case 18 ... 64: работают в GCC и Clang, но это расширение GNU, в стандарт C оно не входит. С -pedantic GCC пишет range expressions in switch statements are non-standard, так что для переносимого кода диапазон лучше проверить через if. Пробелы вокруг многоточия обязательны.

Переменная внутри case

До C23 метка не могла стоять прямо перед объявлением. Строка case 1: int x = 10; в режиме -std=c17 вызывает предупреждение у GCC 13 (с -pedantic) и у Clang 17, а GCC до 11-й версии выдаёт на ней ошибку, поддержка появилась в GCC 11. В C23 это разрешили, об изменении написано на странице cppreference об операторах.

Решение: оберните ветку в фигурные скобки, case 1: { int x = 10; ... break; }. Так переменная живёт только внутри своей ветки и не мешает соседним, а код собирается любым компилятором.

switch или if: что выбрать

Задача Лучше подходит
одна целая переменная, 4+ точных значения switch
диапазоны (x > 10 && x < 20) if
строки, дробные числа, указатели if
условие из нескольких переменных if
конечный автомат, разбор команд протокола switch по enum

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

Циклы for, while и do-while

Схема цикла из деревянных блоков и стрелок на столе

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

#include <stdio.h>

int main(void) {
    for (int i = 1; i <= 3; i++) {
        printf("for: шаг %d\n", i);
    }

    int left = 3;
    while (left > 0) {
        printf("while: осталось %d\n", left);
        left--;
    }

    int tries = 0;
    do {
        tries++;
        printf("do-while: попытка %d\n", tries);
    } while (tries < 0);

    return 0;
}
for: шаг 1
for: шаг 2
for: шаг 3
while: осталось 3
while: осталось 2
while: осталось 1
do-while: попытка 1

Обратите внимание на последнюю строку. Условие tries < 0 ложно с самого начала, но do-while выполнил тело один раз, потому что проверяет условие в конце. Так удобно писать запрос ввода: сначала спросить, потом проверить ответ.

Как устроен for

В заголовке три части через точку с запятой: инициализация, условие, шаг. Любую можно опустить, а for (;;) означает бесконечный цикл, из которого выходят через break. Объявлять счётчик прямо в заголовке, for (int i = 0; ...), разрешено со стандарта C99. Переменная тогда видна только внутри цикла.

Вложенные циклы

Внутренний цикл проходит целиком на каждом шаге внешнего. Классический пример: таблица умножения.

for (int row = 1; row <= 3; row++) {
    for (int col = 1; col <= 4; col++) {
        printf("%4d", row * col);
    }
    printf("\n");
}
   1   2   3   4
   2   4   6   8
   3   6   9  12

Если вы пришли из Python, логика while там та же, отличаются синтаксис и отсутствие do-while. Сравнить можно по нашему разбору цикла while в Python.

break, continue, goto и return

Операторы перехода меняют обычный ход цикла или функции:

  • break выходит из ближайшего цикла или switch. Из двух вложенных циклов одним break не выйти.
  • continue пропускает остаток текущего шага. В for после него всё равно выполняется шаг i++, в while сразу проверяется условие.
  • return завершает функцию и возвращает значение.
  • goto прыгает на метку внутри той же функции.
int data[] = {4, -1, 7, 0, 12, 5};
int n = sizeof data / sizeof data[0];
int sum = 0;

for (int i = 0; i < n; i++) {
    if (data[i] < 0) {
        continue;
    }
    if (data[i] == 0) {
        printf("встретили 0 на позиции %d, стоп\n", i);
        break;
    }
    sum += data[i];
}
printf("сумма до нуля: %d\n", sum);
встретили 0 на позиции 3, стоп
сумма до нуля: 11

Отрицательное −1 пропущено через continue, на нуле цикл остановился, и 12 с 5 в сумму не попали: 4 + 7 = 11.

Когда goto уместен

Прыжки по коду в любую сторону делают программу нечитаемой, поэтому новичкам goto обычно запрещают. Исключение одно, и оно описано в стиле кода ядра Linux, раздел 7: общий выход из функции с очисткой ресурсов. Там же советуют называть метки по действию (out_free_buffer), а не номерами вроде err1.

int process(const char *path) {
    int rc = -1;
    char *buf = NULL;

    FILE *f = fopen(path, "r");
    if (f == NULL) {
        goto out;
    }

    buf = malloc(4096);
    if (buf == NULL) {
        goto close_file;
    }

    size_t got = fread(buf, 1, 4096, f);
    printf("прочитали %zu байт\n", got);
    rc = 0;

    free(buf);
close_file:
    fclose(f);
out:
    return rc;
}

При ошибке на любом шаге функция освобождает ровно то, что успела занять, и выходит через одну точку. Без goto пришлось бы дублировать fclose в каждой ветке.

Пример: калькулятор на switch и цикле

Небольшая программа, в которой работают все три группы: бесконечный for, выход по break, выбор операции через switch и проверка деления на ноль через if.

#include <stdio.h>

int main(void) {
    double a, b;
    char op;

    for (;;) {
        printf("Пример (6 * 7) или q для выхода: ");
        if (scanf("%lf %c %lf", &a, &op, &b) != 3) {
            break;
        }

        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("на ноль делить нельзя\n");
                    break;
                }
                printf("= %g\n", a / b);
                break;
            default:
                printf("операция %c не поддерживается\n", op);
        }
    }
    printf("\nпока\n");
    return 0;
}

Прогон с вводом 6 * 7, 10 / 0, 2 ^ 3, 9 - 12.5 и q:

Пример (6 * 7) или q для выхода: = 42
Пример (6 * 7) или q для выхода: на ноль делить нельзя
Пример (6 * 7) или q для выхода: операция ^ не поддерживается
Пример (6 * 7) или q для выхода: = -3.5
Пример (6 * 7) или q для выхода:
пока

Здесь break внутри case '/' выходит только из switch, а цикл продолжает спрашивать. Из самого цикла выводит break после неудачного scanf: буква q числом не читается, функция возвращает не 3.

Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 700 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

Частые ошибки и как их найти

Рой радуется, что программа собралась без предупреждений

Пять ошибок из семи компилятор находит сам, если включить предупреждения. Собирайте учебный код с -Wall -Wextra, это две секунды на ввод и минус час отладки.

  1. Забытый break. Симптом: выполняется несколько веток подряд. GCC с -Wextra показывает строку, Clang только с -Wimplicit-fallthrough.
  2. Присваивание вместо сравнения. if (x = 5) записывает 5 в x, и условие всегда истинно. Оба компилятора с -Wall предупреждают (-Wparentheses).
  3. Точка с запятой после условия. В if (balance > 0); телом стала пустая инструкция, а блок в скобках выполняется всегда. Наша проверка с balance = -50 напечатала «можно списать». Clang предупреждает о пустом теле даже без флагов, GCC с -Wextra (-Wempty-body). С for (...); цикл прокручивается вхолостую, а тело срабатывает один раз.
  4. Беззнаковый счётчик вниз до нуля. for (unsigned int i = 3; i >= 0; i--) не заканчивается никогда: после 0 идёт 4294967295. GCC с -Wextra пишет comparison of unsigned expression in '>= 0' is always true, Clang 17 с теми же флагами промолчал.
  5. Сравнение дробного счётчика на равенство. Цикл for (double x = 0.0; x != 1.0; x += 0.1) проскочил единицу, мы остановили его на 2.00000000000000044. Для дробей используйте < или целый счётчик.
  6. Ошибка на единицу. Для массива из n элементов условие i <= n обращается к несуществующему data[n]. Правильно i < n.
  7. Строка в switch. Не соберётся. Сравнивайте через strcmp в if или переводите команду в enum заранее.

Где освоить язык C системно

С управляющих конструкций начинается почти любой курс по C. Дальше идут функции, указатели, работа с памятью и файлами, и именно там самоучки чаще всего застревают без разбора чужого кода и проверки заданий.

Ниже курсы по языку C из нашего каталога, полный список с ценами и программами собран на странице курсов по C. Если целитесь в игры или высоконагруженные системы, посмотрите заодно курсы C++: управляющие конструкции там почти те же.

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Инженер-программист
Перейти на сайт курса
GeekBrainsGeekBrains159 408 ₽4428 ₽/мес.12 месяцевОбзор курса
Специалист по информационной безопасности: расширенный курс
Перейти на сайт курса
НетологияНетология117 300 ₽4018 ₽/мес.13 месяцевОбзор курса
Внедрение и работа в DevSecOps
Перейти на сайт курса
OTUSOTUS88 000 ₽8800 ₽/мес.5 месяцевОбзор курса
Блокчейн
Перейти на сайт курса
GeekBrainsGeekBrains168 768 ₽4153 ₽/мес.4 месяцаОбзор курса
Инженер-программист C++
Перейти на сайт курса
GeekBrainsGeekBrains7970 ₽221 ₽/мес.1 месяцОбзор курса

Больше программ — в полном каталоге курсов по C (язык Си)

План входа в профессию по месяцам есть в статье как стать программистом на Си с нуля, а сравнение с соседней специальностью есть в обзоре профессии программиста C++.

Часто задаваемые вопросы

Можно ли использовать строки в switch case в C?

Нет. Выражение в switch и значения после case должны быть целыми: int, char, long или enum. На строку GCC отвечает ошибкой «switch quantity not an integer». Строки сравнивают через strcmp в цепочке if или заранее переводят команду в значение enum.

Обязательна ли ветка default в switch?

Нет, без неё switch просто ничего не выполнит, если совпадений не нашлось. Но default полезен для неожиданных значений, например чтобы вывести ошибку. В одном switch разрешён только один default, и стоять он может в любом месте.

Что будет, если забыть break в switch?

Выполнение продолжится в следующую ветку и дальше, пока не встретится break или конец switch. Это называется проваливанием. GCC с флагом -Wextra предупреждает о нём, Clang только с отдельным флагом -Wimplicit-fallthrough.

Чем цикл while отличается от do-while?

while проверяет условие перед каждым шагом и может не выполниться ни разу. do-while проверяет условие после шага, поэтому тело выполняется хотя бы один раз. do-while удобен для запроса ввода: сначала спросить, потом проверить ответ.

Как выйти сразу из двух вложенных циклов в C?

Одним break не получится, он выходит только из ближайшего цикла. Обычно вложенные циклы выносят в отдельную функцию и выходят через return. Другие варианты: флаг, который проверяет внешний цикл, или goto на метку сразу после циклов.

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!