Управляющие конструкции в C решают, какая строка программы выполнится следующей. Их три группы: ветвления (if, else, switch case, тернарный оператор), циклы (for, while, do-while) и операторы перехода (break, continue, goto, return). Ниже синтаксис каждой, рабочие примеры и ошибки, на которых новички теряют больше всего времени.
Каждый пример ниже скомпилирован дважды: в GCC 13.3 на Ubuntu 24.04 и в Apple clang 17 на macOS. Вывод в блоках кода скопирован из терминала. Если компилятор ещё не стоит, начните с инструкции как выбрать и установить компилятор 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. Вкладывать один тернарный оператор в другой можно, но читать такой код больно.
Канал основателя Checkroi Вани Буявца3 700 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединитьсяСовет: ставьте фигурные скобки даже вокруг одной строки. Без них
elseпривязывается к ближайшемуif, даже если по отступам он стоит под другим. GCC с флагом-Wallна это пишетsuggest explicit braces to avoid ambiguous 'else'.
Оператор 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.
Частые ошибки и как их найти

Пять ошибок из семи компилятор находит сам, если включить предупреждения. Собирайте учебный код с -Wall -Wextra, это две секунды на ввод и минус час отладки.
- Забытый break. Симптом: выполняется несколько веток подряд. GCC с
-Wextraпоказывает строку, Clang только с-Wimplicit-fallthrough. - Присваивание вместо сравнения.
if (x = 5)записывает 5 в x, и условие всегда истинно. Оба компилятора с-Wallпредупреждают (-Wparentheses). - Точка с запятой после условия. В
if (balance > 0);телом стала пустая инструкция, а блок в скобках выполняется всегда. Наша проверка сbalance = -50напечатала «можно списать». Clang предупреждает о пустом теле даже без флагов, GCC с-Wextra(-Wempty-body). Сfor (...);цикл прокручивается вхолостую, а тело срабатывает один раз. - Беззнаковый счётчик вниз до нуля.
for (unsigned int i = 3; i >= 0; i--)не заканчивается никогда: после 0 идёт 4294967295. GCC с-Wextraпишетcomparison of unsigned expression in '>= 0' is always true, Clang 17 с теми же флагами промолчал. - Сравнение дробного счётчика на равенство. Цикл
for (double x = 0.0; x != 1.0; x += 0.1)проскочил единицу, мы остановили его на 2.00000000000000044. Для дробей используйте<или целый счётчик. - Ошибка на единицу. Для массива из n элементов условие
i <= nобращается к несуществующемуdata[n]. Правильноi < n. - Строка в switch. Не соберётся. Сравнивайте через
strcmpвifили переводите команду вenumзаранее.
Где освоить язык C системно
С управляющих конструкций начинается почти любой курс по C. Дальше идут функции, указатели, работа с памятью и файлами, и именно там самоучки чаще всего застревают без разбора чужого кода и проверки заданий.
Ниже курсы по языку C из нашего каталога, полный список с ценами и программами собран на странице курсов по C. Если целитесь в игры или высоконагруженные системы, посмотрите заодно курсы C++: управляющие конструкции там почти те же.
| Курс | Школа | Стоимость со скидкой | В рассрочку | Длительность | Обзор курса от Checkroi |
|---|---|---|---|---|---|
| Инженер-программист Перейти на сайт курса | 159 408 ₽ | 4428 ₽/мес. | 12 месяцев | Обзор курса | |
| Специалист по информационной безопасности: расширенный курс Перейти на сайт курса | 117 300 ₽ | 4018 ₽/мес. | 13 месяцев | Обзор курса | |
| Внедрение и работа в DevSecOps Перейти на сайт курса | 88 000 ₽ | 8800 ₽/мес. | 5 месяцев | Обзор курса | |
| Блокчейн Перейти на сайт курса | 168 768 ₽ | 4153 ₽/мес. | 4 месяца | Обзор курса | |
| Инженер-программист C++ Перейти на сайт курса | 7970 ₽ | 221 ₽/мес. | 1 месяц | Обзор курса |
Больше программ — в полном каталоге курсов по C (язык Си)
План входа в профессию по месяцам есть в статье как стать программистом на Си с нуля, а сравнение с соседней специальностью есть в обзоре профессии программиста C++.




