Перейти к основному содержимому

Управляющие конструкции и операторы С

Разработчику Архитектору

Перед чтением: Операторы — общие понятия оператора, операнда, приоритетов и типов операций без привязки к языку.

Сначала: Циклы в коде — общая идея повторений, виды циклов и типичные ошибки без привязки к синтаксису языка.


Как читать эту статью

Блоки про if, switch, циклы и операторы; демо иллюстрирует идеи. Нужны типы; дальше — функции.


Условия и циклы в С - нулевое и ненулевое в выражении

Условие в C — это выражение с нулём/ненулём; понимание этого снимает путаницу с bool в C99+ и типичными багами циклов.


Си — if, switch, for, while

Ветвление — if / else, многовариантное — switch. Циклы — for, while, do-while. Условие в if и циклах — выражение с ненулевым / нулевым результатом, не отдельный тип bool (хотя <, == дают 0 или 1).


Условные конструкции — if и else

if выполняет блок, когда условие не равно нулю.

Синтаксис простой формы if выглядит так:

if (условие) {
// код, выполняемый при истинности условия
}

Разбор:

  • if проверяет выражение в круглых скобках и выполняет блок только при ненулевом результате.
  • В C условие трактуется как числовое: 0 означает ложь, любое ненулевое значение означает истину.
  • Фигурные скобки задают границы блока и снижают риск ошибок при дальнейшем расширении конструкции.

Условие — это выражение, результат которого интерпретируется как логическое значение. В С любое целочисленное значение, отличное от нуля, считается истинным. Значение 0 считается ложным.

Пример:

int temperature = 25;

if (temperature > 20) {
printf("Тепло.\n");
}

Разбор:

  • Переменная temperature хранит текущее значение и участвует в сравнении с порогом 20.
  • Оператор > возвращает 1 или 0, поэтому выражение напрямую подходит для условия if.
  • При истинном условии вызывается printf, который выводит диагностическое сообщение пользователю.

В этом примере строка "Тепло." будет выведена на экран, потому что значение переменной temperature больше 20.

Если требуется указать альтернативное действие на случай, когда условие ложно, используется конструкция else:

if (условие) {
// действие при истинности
} else {
// действие при ложности
}

Пример:

int age = 16;

if (age >= 18) {
printf("Можно голосовать.\n");
} else {
printf("Голосовать ещё нельзя.\n");
}

Разбор:

  • Условие age >= 18 даёт 0 (ложь) для значения 16, поэтому выполняется ветка else.
  • Ветки if и else взаимоисключающие: сработает ровно одна из них.
  • printf в каждой ветке формирует понятный пользовательский ответ без дополнительных проверок.

Здесь программа проверяет возраст и выводит соответствующее сообщение.

Для обработки нескольких условий подряд применяется цепочка else if:

if (условие1) {
// действие 1
} else if (условие2) {
// действие 2
} else if (условие3) {
// действие 3
} else {
// действие по умолчанию
}

Эта форма удобна, когда нужно выбрать один вариант из нескольких взаимоисключающих.

Пример:

int score = 85;

if (score >= 90) {
printf("Отлично\n");
} else if (score >= 75) {
printf("Хорошо\n");
} else if (score >= 60) {
printf("Удовлетворительно\n");
} else {
printf("Неудовлетворительно\n");
}

Каждое условие проверяется последовательно сверху вниз. Как только одно из них оказывается истинным, выполняется соответствующий блок, и остальные условия игнорируются.

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


Множественный выбор — switch

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

Синтаксис:

switch (выражение) {
case значение1:
// действия
break;
case значение2:
// действия
break;
default:
// действия по умолчанию
}

Разбор:

  • switch вычисляет одно выражение и выбирает ветку case, соответствующую его значению.
  • Метки case служат точками входа, а break завершает текущую ветку без провала в следующую.
  • default закрывает непредусмотренные варианты и формирует безопасное поведение по умолчанию.

Выражение внутри switch должно иметь целочисленный тип (int, char, перечисления и т.п.). Каждый case представляет собой метку, связанную с конкретным значением. Если значение выражения совпадает с одним из case, выполнение начинается с этой точки.

Оператор break завершает выполнение текущего case и передаёт управление за пределы конструкции switch. Без break выполнение "проваливается" в следующий case, что иногда используется намеренно, но чаще приводит к ошибкам.

Пример:

Код ITЗагрузка примера кода…

Вывод: Хорошо.

Если убрать break после case 'B', программа также выполнит case 'C' и выведет оба сообщения. Такое поведение называется "провалом" (fall-through) и требует особого внимания.

Метка default необязательна, но рекомендуется. Она обрабатывает все значения, не перечисленные явно в case.

Интерактивное демо — пошаговый цикл на примере JavaScript (for, while). В С синтаксис другой, но порядок шагов тот же. Обобщённо: циклы в коде.

Play ITЗагрузка интерактивного демо…


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

Циклы позволяют многократно выполнять один и тот же блок кода. В С существует три основных вида циклов: for, while и do-while. Все они решают похожие задачи, но имеют разные формы записи и области применения.


Цикл for

Цикл for наиболее удобен, когда известно количество повторений заранее. Он объединяет три элемента: инициализацию, условие продолжения и изменение счётчика — в одной строке.

Синтаксис:

for (инициализация; условие; изменение) {
// тело цикла
}

Разбор:

  • Первая часть for выполняется один раз и обычно инициализирует счётчик итераций.
  • Вторая часть определяет условие продолжения; при 0 цикл завершается.
  • Третья часть обновляет состояние после каждой итерации и поддерживает прогресс к завершению.

Порядок выполнения:

  1. Выполняется инициализация (один раз в начале).
  2. Проверяется условие.
  3. Если условие истинно, выполняется тело цикла.
  4. После тела выполняется изменение.
  5. Возвращаемся к шагу 2.

Пример:

for (int i = 0; i < 5; i++) {
printf("Шаг %d\n", i);
}

Разбор:

  • int i = 0 выполняется один раз перед первой проверкой условия.
  • Условие i < 5 контролирует число итераций: при i == 5 цикл завершается.
  • i++ увеличивает счётчик после каждого прохода тела цикла.
  • %d в printf подставляет текущее значение i в строку вывода.

Вывод:

Шаг 0
Шаг 1
Шаг 2
Шаг 3
Шаг 4

Переменная i инициализируется значением 0, проверяется условие i < 5, и после каждой итерации i увеличивается на 1.

Цикл for гибок: любая из трёх частей может быть опущена. Например, бесконечный цикл записывается как:

for (;;) {
// бесконечное выполнение
}

Также можно управлять несколькими переменными одновременно:

for (int i = 0, j = 10; i < j; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}

Цикл while

Цикл while проверяет условие до выполнения тела. Если условие ложно с самого начала, тело цикла не выполнится ни разу.

Синтаксис:

while (условие) {
// тело цикла
}

Пример:

int count = 3;
while (count > 0) {
printf("Обратный отсчёт: %d\n", count);
count--;
}

Разбор:

  • while сначала проверяет условие count > 0, и только потом выполняет тело.
  • count-- — постфиксный декремент: сначала используется текущее значение, затем оно уменьшается.
  • Когда count становится 0, условие ложно, и цикл завершается без лишней итерации.

Вывод:

Обратный отсчёт: 3
Обратный отсчёт: 2
Обратный отсчёт: 1

Цикл while подходит, когда количество итераций заранее неизвестно, но есть чёткое условие завершения.


Цикл do-while

Цикл do-while отличается тем, что тело цикла выполняется хотя бы один раз, потому что проверка условия происходит после выполнения тела.

Синтаксис:

do {
// тело цикла
} while (условие);

Пример:

int input;
do {
printf("Введите число больше 10: ");
scanf("%d", &input);
} while (input <= 10);

Разбор:

  • do { ... } while (...) гарантирует минимум одно выполнение тела, даже если условие изначально ложно.
  • scanf("%d", &input) читает целое число и записывает его по адресу &input.
  • Условие input <= 10 повторяет запрос, пока введённое значение не станет больше 10.

Этот код гарантирует, что пользователь введёт число хотя бы один раз, даже если сразу введёт корректное значение.

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


Операторы в языке С

Play ITЗагрузка интерактивного демо…

Операторы — это символы или комбинации символов, которые выполняют определённые действия над данными. В С операторы делятся на несколько категорий в зависимости от типа выполняемых операций — арифметические, логические, побитовые, сравнения, присваивания и другие. Они составляют основу выражений, из которых строятся инструкции программы.


Арифметические операторы

Арифметические операторы применяются для выполнения базовых математических операций над числами. С поддерживает следующие арифметические операторы:

  • + — сложение
  • - — вычитание
  • * — умножение
  • / — деление
  • % — остаток от деления (деление по модулю)

Эти операторы работают с целыми и вещественными числами. Однако оператор % применим только к целочисленным типам (int, char, long и т.п.), потому что остаток от деления не определён для вещественных чисел.

Примеры:

int a = 10, b = 3;
int sum = a + b; // 13
int diff = a - b; // 7
int product = a * b; // 30
int quotient = a / b; // 3 (целочисленное деление)
int remainder = a % b; // 1

Разбор:

  • Пример показывает базовые арифметические операции в int, включая целочисленное деление и остаток.
  • Выражение a / b в типе int отбрасывает дробную часть, поэтому получается 3, а не 3.33.
  • Оператор % возвращает остаток от деления и часто используется в проверках чётности и циклической логике.

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

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

double x = 10.0, y = 3.0;
double result = x / y; // примерно 3.333333

Существуют также инкремент (++) и декремент (--) — специальные операторы, увеличивающие или уменьшающие значение переменной на единицу. Они бывают двух форм: префиксной и постфиксной.

  • ++x — сначала увеличивает x, затем использует новое значение
  • x++ — сначала использует текущее значение x, затем увеличивает его

Пример:

int i = 5;
int a = ++i; // i становится 6, a = 6
int b = i++; // b = 6, затем i становится 7

Эти операторы часто встречаются в циклах и индексных выражениях.


Операторы сравнения

Операторы сравнения используются для проверки отношений между двумя значениями. Результатом всегда является целое число: 1 (истина) или 0 (ложь).

Основные операторы сравнения:

  • == — равно
  • != — не равно
  • < — меньше
  • > — больше
  • <= — меньше или равно
  • >= — больше или равно

Пример:

int x = 7, y = 10;
printf("%d\n", x < y); // 1
printf("%d\n", x == y); // 0
printf("%d\n", x != y); // 1

Эти операторы активно используются в условиях if, while, for и других управляющих конструкциях.

Важно не путать оператор присваивания = с оператором равенства ==. Ошибка вида if (x = 5) присваивает значение 5 переменной x и всегда даёт истинный результат (потому что 5 != 0). Современные компиляторы обычно предупреждают об этом, но ошибка остаётся частой среди начинающих.


Логические операторы

Логические операторы позволяют комбинировать несколько условий в одно сложное выражение. В С поддерживаются три логических оператора:

  • && — логическое И (AND)
  • || — логическое ИЛИ (OR)
  • ! — логическое НЕ (NOT)

Оператор && возвращает 1, только если оба операнда истинны.
Оператор || возвращает 1, если хотя бы один операнд истинен.
Оператор ! инвертирует значение: !0 даёт 1, !1 даёт 0.

Пример:

int age = 20;
int hasLicense = 1;

if (age >= 18 && hasLicense) {
printf("Можно водить.\n");
}

if (age < 16 || age > 90) {
printf("Возраст вне допустимого диапазона.\n");
}

С использует ленивые вычисления (short-circuit evaluation):

  • В выражении A && B, если A ложно, B не вычисляется.
  • В выражении A || B, если A истинно, B не вычисляется.

Это поведение полезно для предотвращения ошибок. Например:

if (ptr != NULL && ptr->value > 0) {
// Безопасный доступ к указателю
}

Разбор:

  • Первая часть ptr != NULL проверяет валидность указателя перед обращением к полям структуры.
  • Из-за short-circuit в && правая часть не вычисляется, если левая ложна, поэтому разыменование безопасно.
  • Запись ptr->value читается только при корректном указателе и предотвращает аварийное завершение программы.

Если ptr равен NULL, вторая часть условия не будет проверена, и программа не завершится аварийно.


Побитовые операторы

Побитовые операторы работают непосредственно с отдельными битами двоичного представления чисел. Они применяются в системном программировании, работе с аппаратными регистрами, шифровании и оптимизации.

Основные побитовые операторы:

  • & — побитовое И
  • | — побитовое ИЛИ
  • ^ — побитовое исключающее ИЛИ (XOR)
  • ~ — побитовое НЕ (инверсия всех битов)
  • &lt;&lt; — сдвиг влево
  • &gt;&gt; — сдвиг вправо

Примеры:

unsigned char a = 0b10101010; // 170 в десятичной
unsigned char b = 0b11001100; // 204

unsigned char and_result = a & b; // 0b10001000
unsigned char or_result = a | b; // 0b11101110
unsigned char xor_result = a ^ b; // 0b01100110
unsigned char not_a = ~a; // 0b01010101 (для 8 бит)

Операторы сдвига перемещают биты влево или вправо:

int x = 5; // 0b00000101
int left = x << 1; // 0b00001010 = 10
int right = x >> 1; // 0b00000010 = 2

Сдвиг влево на n позиций эквивалентен умножению на 2^n.
Сдвиг вправо на n позиций эквивалентен целочисленному делению на 2^n.

Побитовые операции особенно эффективны, когда нужно упаковать несколько флагов в одно число или работать с цветами, масками, сетевыми адресами.


Тернарный условный оператор (?:)

Тернарный оператор — это компактная форма записи условия, возвращающая одно из двух значений в зависимости от результата проверки.

Синтаксис:

условие ? значение_если_истина : значение_если_ложь

Пример:

int a = 10, b = 20;
int max = (a > b) ? a : b; // max = 20

Тернарный оператор часто используется для инициализации переменных или возврата значений в функциях:

char* getStatus(int score) {
return (score >= 60) ? "Сдано" : "Не сдано";
}

Он может быть вложен:

int grade = (score >= 90) ? 5 :
(score >= 75) ? 4 :
(score >= 60) ? 3 : 2;

Хотя такая запись экономит место, чрезмерное вложение снижает читаемость. В таких случаях предпочтительнее использовать if-else.


Приоритет и ассоциативность операторов

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

Например, в выражении a + b * c сначала выполняется умножение, потому что у * выше приоритет, чем у +.

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

Сжатая шпаргалка (сверху — выполняется раньше):

УровеньОператорыПример
1(), [], ->, .arr[i], obj.field
2унарные !, ~, ++, --, * (разыменование), &!flag, *p
3*, /, %a * b % c
4+, -a + b
5сдвиги &lt;&lt;, &gt;&gt;mask &lt;&lt; 2
6сравнения <, <=, >, >=x < y
7==, !=a == b
8& (побитовое И)flags & MASK
9^a ^ b
10| (побитовое ИЛИ)READ | WRITE
11&&ok && ready
12логическое ИЛИ ||a || b
13?:x > 0 ? 1 : -1
14=, +=, -=count += 1
15,редко в учебных выражениях

Пример:

// Без скобок — трудно читать
if (a == b && c != d || e > f)

// Со скобками — понятно
if ((a == b && c != d) || (e > f))

Скобки не влияют на производительность, но значительно повышают надёжность и читаемость кода.


Совместное использование управляющих конструкций и операторов

В реальных программах редко встречаются изолированные if или одиночные арифметические выражения. Гораздо чаще они комбинируются в сложные, но чёткие последовательности, которые обеспечивают корректную обработку данных при любых условиях.


Валидация пользовательского ввода

Одна из самых частых задач — получение данных от пользователя и проверка их корректности. Это требует циклов и условий.

Пример: программа запрашивает возраст до тех пор, пока не будет введено значение от 0 до 120.

Код ITЗагрузка примера кода…

Разбор:

  • Цикл do { ... } while (...) гарантирует минимум одну итерацию ввода и хорошо подходит для валидации.
  • Проверка scanf("%d", &age) != 1 отсекает нечисловой ввод и не допускает работу с мусорным значением.
  • Очистка до '\n' убирает остаток некорректного ввода из буфера, чтобы следующая итерация читала свежие данные.
  • Условие age < 0 || age > 120 задаёт допустимый диапазон и удерживает цикл до корректного значения.

Здесь цикл do-while гарантирует хотя бы один запрос. Логическое ИЛИ (||) в условии означает: "продолжать, если возраст меньше нуля или больше 120". Только когда оба условия ложны, цикл завершается.

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


Обработка массивов

Массивы — фундаментальная структура данных. Их обработка почти всегда требует циклов.

Пример: поиск максимального элемента в массиве.

Код ITЗагрузка примера кода…

Цикл for проходит по всем элементам, начиная со второго. Условие if сравнивает текущий элемент с текущим максимумом. Оператор присваивания обновляет максимум, если найден больший элемент.

Аналогично можно реализовать подсчёт суммы, среднего арифметического, количества чётных чисел и так далее.

Пример: подсчёт чётных чисел.

int countEven = 0;
for (int i = 0; i < size; i++) {
if (numbers[i] % 2 == 0) {
countEven++;
}
}
printf("Чётных чисел: %d\n", countEven);

Здесь используется оператор % для проверки делимости на 2. Результат сравнения (== 0) управляет выполнением блока if.


Реализация меню с помощью switch

Интерактивные программы часто строятся вокруг текстового меню. Конструкция switch идеально подходит для такой задачи.

Код ITЗагрузка примера кода…

Цикл do-while обеспечивает повторное отображение меню. Конструкция switch выбирает действие по номеру. Метка default обрабатывает некорректные вводы. Условие выхода — choice != 3.

Такая структура легко расширяется: достаточно добавить новый case и обновить текст меню.


Флаги и побитовые операции

В системном программировании часто возникает задача хранения нескольких логических состояний в одном числе. Например, права доступа к файлу — чтение, запись, выполнение.

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

Код ITЗагрузка примера кода…

Разбор:

  • Макросы READ, WRITE, EXEC задают флаги как отдельные биты в одном целочисленном значении.
  • permissions = READ | WRITE включает сразу два разрешения через побитовое ИЛИ.
  • Проверка permissions & WRITE оставляет только бит записи и позволяет быстро определить, установлен ли флаг.
  • Выражение permissions &= ~WRITE очищает конкретный бит, не затрагивая остальные права.

Оператор | объединяет флаги.
Оператор & проверяет наличие флага.
Оператор ~ инвертирует биты, что позволяет "выключить" конкретный флаг с помощью &=.

Этот подход экономит память и ускоряет проверки, особенно в низкоуровневых системах.


Тернарный оператор в форматировании вывода

Тернарный оператор удобен для динамического выбора значений без многострочных if.

Пример: согласование окончаний в русском языке.

int files = 5;
printf("Найдено %d файл%s.\n", files,
(files == 1) ? "" :
(files >= 2 && files <= 4) ? "а" : "ов");

Для files = 1 — "файл",
для 2–4 — "файла",
для остальных — "файлов".

Хотя это упрощённая модель (полная грамматика сложнее), пример демонстрирует, как тернарный оператор делает код компактным и выразительным.


Что дальше

ТемаСтатья
Функции, указателиФункции и указатели
ТипыТипы данных в С
Первая программаПервая программа на С
О разделеintro

Что попробовать

  1. Перепишите switch с break и без — посмотрите провал.
  2. Сделайте цикл while с условием на указатель строки.
  3. Скомпилируйте с -Wall и исправьте предупреждение о сравнении.

Практические паттерны вместо "сухих правил"

Защитный if (guard clause)

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

int process(const int *arr, size_t n) {
if (arr == NULL) {
return -1;
}
if (n == 0) {
return 0;
}
/* основная логика */
return 1;
}

Разбор:

  • Это guard-clause стиль: сначала проверяются ошибки входа, затем выполняется основная логика.
  • const int *arr фиксирует, что функция читает массив без изменения исходных данных.
  • Ранние return уменьшают вложенность и делают счастливый путь короче и легче для ревью.

Так код читается проще и меньше шансов забыть ветку с ошибкой.


Цикл с явной границей

for (size_t i = 0; i < n; i++) {
/* arr[i] безопасен, пока i < n */
}

Условие i < n лучше держать максимально простым, особенно в низкоуровневом коде.


Про switch в реальных проектах

switch часто используют как "диспетчер состояний":

switch (state) {
case STATE_INIT: /* ... */ break;
case STATE_RUNNING: /* ... */ break;
case STATE_ERROR: /* ... */ break;
default: /* аварийный путь */ break;
}

Полезные правила:

  • всегда оставляйте default,
  • если нужен осознанный "провал" в следующий case, помечайте это комментарием,
  • не перегружайте один switch десятками действий — лучше вынести часть логики в функции.

Связанные статьи: Функции и указатели, Идиомы кода и обработка ошибок, Системное программирование на С.