Операторы в C — это знаки, которыми программа считает, сравнивает и меняет значения: +, %, ==, &&, <<, ?: и ещё три десятка. Оператор вместе с операндами образует выражение, например a % b == 0. Ниже разбираем все группы операторов языка Си с примерами и таблицей приоритета, а заодно три места, где новички ошибаются чаще всего: целочисленное деление, остаток от деления отрицательных чисел и выражения без скобок.
Каждая программа в статье скомпилирована дважды, в GCC 13.3 на Ubuntu 24.04 и в Apple clang 17 на macOS, вывод скопирован из терминала. Компилятора ещё нет? Начните с инструкции как выбрать и установить компилятор C. Если вы пришли из Python, сравните с разбором операторов Python: знаки похожи, а поведение местами отличается.
Какие операторы есть в языке C

Сводная таблица групп. Подробности по каждой ниже.
| Группа | Операторы | Пример |
|---|---|---|
| Арифметические | + - * / % |
total = price * count; |
| Присваивание | = += -= *= /= %= <<= >>= &= ^= |= |
sum += x; |
| Инкремент и декремент | ++ -- |
i++; |
| Сравнение | == != < > <= >= |
if (age >= 18) |
| Логические | && || ! |
if (ok && !error) |
| Побитовые | & | ^ ~ << >> |
flags |= FLAG_READ; |
| Условный (тернарный) | ?: |
max = a > b ? a : b; |
| Прочие | sizeof, (тип), ,, & и * для адресов, . и -> |
n = sizeof arr / sizeof arr[0]; |
Одни и те же знаки иногда означают разное. Звёздочка между двумя числами умножает, а перед указателем достаёт значение по адресу. Амперсанд между числами даёт побитовое И, а перед переменной возвращает её адрес. Компилятор различает их по числу операндов: унарный оператор берёт один, бинарный два, тернарный ?: три.
Почти весь этот набор без изменений перешёл в C++, Java, C# и JavaScript, поэтому таблица пригодится и после C. Как связаны эти языки, рассказывали в обзоре языков семейства C.
Арифметические операторы и деление в C
Сложение, вычитание и умножение работают как в школе. Сюрпризы начинаются на делении.
#include <stdio.h>
int main(void) {
int a = 17, b = 5;
printf("%d + %d = %d\n", a, b, a + b);
printf("%d - %d = %d\n", a, b, a - b);
printf("%d * %d = %d\n", a, b, a * b);
printf("%d / %d = %d\n", a, b, a / b);
printf("%d %% %d = %d\n", a, b, a % b);
printf("%d / 5.0 = %.1f\n", a, a / 5.0);
return 0;
}
Вывод:
17 + 5 = 22
17 - 5 = 12
17 * 5 = 85
17 / 5 = 3
17 % 5 = 2
17 / 5.0 = 3.4
Целочисленное деление
Если оба операнда целые, деление в C отбрасывает дробную часть: 17 / 5 даёт 3, а не 3,4. Округления нет, остаток просто теряется. Чтобы получить дробь, хотя бы один операнд должен быть вещественным: 17 / 5.0 или (double)a / b.
С отрицательными числами деление отбрасывает дробную часть в сторону нуля, поэтому -7 / 2 даёт −3, а не −4. Так требует стандарт начиная с C99, раньше направление выбирал компилятор (описание на cppreference).
Остаток от деления
Оператор % возвращает остаток от деления целых чисел. Стандарт связывает его с делением тождеством (a / b) * b + a % b == a, отсюда правило: знак остатка совпадает со знаком делимого.
#include <stdio.h>
int main(void) {
printf("-7 / 2 = %d\n", -7 / 2);
printf("-7 %% 2 = %d\n", -7 % 2);
printf("7 %% -2 = %d\n", 7 % -2);
int n = -7;
int r = ((n % 3) + 3) % 3; /* остаток всегда от 0 до 2 */
printf("математический остаток -7 по 3 = %d\n", r);
return 0;
}
Вывод:
-7 / 2 = -3
-7 % 2 = -1
7 % -2 = 1
математический остаток -7 по 3 = 2
Из-за этого проверка нечётности через n % 2 == 1 ломается на отрицательных числах. Для −3 остаток равен −1, и условие ложно:
#include <stdio.h>
int main(void) {
int n = -3;
printf("n %% 2 == 1 даёт %d\n", n % 2 == 1);
printf("n %% 2 != 0 даёт %d\n", n % 2 != 0);
return 0;
}
Вывод:
n % 2 == 1 даёт 0
n % 2 != 0 даёт 1
Решение: сравнивайте с нулём (n % 2 != 0), а если нужен остаток строго от 0 до b − 1, пишите ((n % b) + b) % b, как во втором примере.
Для типичных задач остаток незаменим. Так переводят секунды в часы и минуты:
#include <stdio.h>
int main(void) {
for (int n = -3; n <= 3; n++) {
printf("%2d: %s\n", n, (n % 2 == 0) ? "чётное" : "нечётное");
}
int seconds = 3725;
printf("%d с = %d ч %d мин %d с\n", seconds,
seconds / 3600, seconds % 3600 / 60, seconds % 60);
return 0;
}
Вывод:
-3: нечётное
-2: чётное
-1: нечётное
0: чётное
1: нечётное
2: чётное
3: нечётное
3725 с = 1 ч 2 мин 5 с
Оператор % работает только с целыми типами. Попытка написать 7.5 % 2.0 не скомпилируется, остаток вещественных чисел считает функция fmod() из <math.h>. На Linux программу с ней собирают с флагом -lm:
#include <stdio.h>
#include <math.h>
int main(void) {
double x = 7.5, y = 2.0;
printf("fmod(7.5, 2.0) = %.1f\n", fmod(x, y));
return 0;
}
Вывод:
fmod(7.5, 2.0) = 1.5
Деление на ноль и переполнение
- Целое деление на ноль и
% 0— неопределённое поведение. Обычно программа падает, но гарантий нет. INT_MIN / -1тоже неопределённое поведение: результат не помещается вint.- Переполнение знакового целого (
INT_MAX + 1) не определено стандартом. Беззнаковые типы, наоборот, переполняются по модулю, и это гарантировано. В Python целые растут без предела, поэтому эта ловушка обычно неожиданна для тех, кто пришёл из операторов Python.
#include <stdio.h>
#include <limits.h>
int main(void) {
unsigned char c = 250;
c += 10;
printf("unsigned char 250 + 10 = %d\n", c);
unsigned int u = UINT_MAX;
u = u + 1;
printf("UINT_MAX + 1 = %u\n", u);
printf("INT_MAX = %d\n", INT_MAX);
return 0;
}
Вывод:
unsigned char 250 + 10 = 4
UINT_MAX + 1 = 0
INT_MAX = 2147483647
В первой строке 260 не помещается в unsigned char (максимум 255), поэтому значение «прокрутилось» до 260 − 256 = 4.
Присваивание, инкремент и декремент
Простое присваивание = кладёт значение справа в переменную слева. Составные операторы сокращают запись: x += 3 значит x = x + 3. Такие пары есть для всех арифметических и побитовых операторов.
Присваивание в C само является выражением и возвращает записанное значение. Поэтому работает цепочка a = b = 0: она разбирается справа налево как a = (b = 0).
Префиксный и постфиксный инкремент
Оператор ++ увеличивает переменную на единицу, -- уменьшает. Разница между формами видна, только когда результат куда-то идёт:
i++(постфиксный) возвращает старое значение, потом увеличивает;++i(префиксный) сначала увеличивает, потом возвращает новое.
#include <stdio.h>
int main(void) {
int i = 5;
int a = i++; /* a получает 5, потом i становится 6 */
int b = ++i; /* i становится 7, b получает 7 */
printf("a = %d, b = %d, i = %d\n", a, b, i);
int x = 10;
x += 3; /* 13 */
x *= 2; /* 26 */
x %= 7; /* 5 */
x <<= 2; /* 20 */
printf("x = %d\n", x);
return 0;
}
Вывод:
a = 5, b = 7, i = 7
x = 20
Отдельной строкой i++; и ++i; ничем не отличаются, пишите как привыкли. Чаще всего инкремент встречается в заголовке цикла for, где счётчик растёт на каждом шаге; что такое шаг цикла, объясняли в статье что такое итерация.
Дважды изменять переменную в одном выражении нельзя
Выражения вроде i = i++ + 1 или a[i] = i++ меняют переменную и ещё раз меняют или читают её без точки следования между этими действиями. Стандарт называет это неопределённым поведением (C11, раздел 6.5, пункт 2, черновик стандарта N1570). Где именно в выражении стоят точки следования, перечислено в справке Microsoft о точках следования C.
#include <stdio.h>
int main(void) {
int i = 1;
i = i++ + 1;
printf("%d\n", i);
return 0;
}
Оба наших компилятора напечатали 2, но полагаться на это нельзя: другой компилятор или другой уровень оптимизации вправе выдать иное. Clang предупреждает о такой строке даже без флагов, GCC с флагом -Wall:
05_ub.c:5:7: warning: operation on 'i' may be undefined [-Wsequence-point]
Операторы сравнения и логические операторы
Операторы сравнения ==, !=, <, >, <=, >= возвращают int: 1, если условие верно, и 0, если нет. Отдельного логического типа в старом C не было, поэтому любое ненулевое значение считается истиной. Тип bool со значениями true и false подключается заголовком <stdbool.h> (с C99), а в C23 bool, true и false стали ключевыми словами и заголовок не нужен.
Логические операторы:
| Оператор | Название | Результат 1, когда |
|---|---|---|
&& |
логическое И | оба операнда ненулевые |
|| |
логическое ИЛИ | хотя бы один операнд ненулевой |
! |
логическое НЕ | операнд равен нулю |
Короткое вычисление
Операторы && и || вычисляют правую часть, только если без неё ответ неизвестен. У 0 && … результат уже ноль, у 1 || … уже единица, и вторая функция не вызывается:
#include <stdio.h>
int check(const char *name, int value) {
printf(" вызвана %s\n", name);
return value;
}
int main(void) {
printf("5 > 3 даёт %d, 5 == 3 даёт %d\n", 5 > 3, 5 == 3);
printf("!0 = %d, !42 = %d\n", !0, !42);
printf("0 && ...:\n");
if (check("левая", 0) && check("правая", 1)) { }
printf("1 || ...:\n");
if (check("левая", 1) || check("правая", 0)) { }
return 0;
}
Вывод:
5 > 3 даёт 1, 5 == 3 даёт 0
!0 = 1, !42 = 0
0 && ...:
вызвана левая
1 || ...:
вызвана левая
Так защищаются от деления на ноль в одном условии: сначала проверка делителя, потом деление.
#include <stdio.h>
int main(void) {
int total = 100, count = 0;
if (count != 0 && total / count > 10) {
printf("среднее больше 10\n");
} else {
printf("делить не стали: count = %d\n", count);
}
return 0;
}
Вывод:
делить не стали: count = 0
Сравнение дробных чисел
Числа double хранятся в двоичном виде с округлением, поэтому 0.1 + 0.2 == 0.3 ложно. Дробные значения сравнивают с допуском:
#include <stdio.h>
#include <math.h>
int main(void) {
double s = 0.1 + 0.2;
printf("0.1 + 0.2 == 0.3 даёт %d\n", s == 0.3);
printf("разница меньше 1e-9: %d\n", fabs(s - 0.3) < 1e-9);
return 0;
}
Вывод:
0.1 + 0.2 == 0.3 даёт 0
разница меньше 1e-9: 1
Побитовые операторы в C

Побитовые операторы работают с отдельными битами целого числа. Без них не обходится программирование микроконтроллеров, драйверов и сетевых протоколов: там один байт хранит восемь флагов, а регистр устройства настраивают масками. Чем занят программист микроконтроллеров, рассказывали отдельно.
| Оператор | Что делает | 12 и 10 (1100 и 1010) |
|---|---|---|
& |
И: бит 1, если 1 в обоих | 8 (1000) |
| |
ИЛИ: бит 1, если 1 хотя бы в одном | 14 (1110) |
^ |
исключающее ИЛИ: бит 1, если биты разные | 6 (0110) |
~ |
инверсия всех битов | ~0u = 4294967295 |
<< |
сдвиг влево, умножение на степень двойки | 12 << 1 = 24 |
>> |
сдвиг вправо, деление на степень двойки | 12 >> 2 = 3 |
Типичный приём с флагами: |= включает бит, &= ~ выключает, ^= переключает, & проверяет.
#include <stdio.h>
#define FLAG_READ (1u << 0) /* 0001 */
#define FLAG_WRITE (1u << 1) /* 0010 */
#define FLAG_EXEC (1u << 2) /* 0100 */
int main(void) {
unsigned a = 12, b = 10; /* 1100 и 1010 */
printf("a & b = %u\n", a & b);
printf("a | b = %u\n", a | b);
printf("a ^ b = %u\n", a ^ b);
printf("a << 1 = %u\n", a << 1);
printf("a >> 2 = %u\n", a >> 2);
printf("~0u = %u\n", ~0u);
unsigned mode = 0;
mode |= FLAG_READ | FLAG_WRITE; /* включить два флага */
mode &= ~FLAG_WRITE; /* выключить запись */
mode ^= FLAG_EXEC; /* переключить исполнение */
printf("mode = %u, чтение %s\n", mode, (mode & FLAG_READ) ? "есть" : "нет");
return 0;
}
Вывод:
a & b = 8
a | b = 14
a ^ b = 6
a << 1 = 24
a >> 2 = 3
~0u = 4294967295
mode = 5, чтение есть
Для битовых операций берите беззнаковые типы. Сдвиг отрицательного числа влево — неопределённое поведение, а сдвиг вправо зависит от компилятора. Сдвиг на число бит, равное ширине типа или больше (1u << 32 для 32-битного unsigned), тоже не определён.
Не путайте & с &&. Выражение 2 & 1 даёт 0 (общих битов нет), а 2 && 1 даёт 1 (оба числа ненулевые).
Тернарный оператор, sizeof, приведение и запятая
Тернарный оператор условие ? a : b возвращает a, если условие истинно, иначе b. Удобен для коротких присваиваний, а ветвления с несколькими действиями читаются легче через if. В Python та же идея записывается словами, сравните с разбором тернарного оператора в Python.
sizeof возвращает размер типа или объекта в байтах, тип результата size_t, печатается через %zu. Классика: число элементов массива как sizeof arr / sizeof arr[0]. Внутри функции, куда массив передан параметром, этот приём не работает: там arr уже указатель.
Приведение типа (double)a меняет тип значения на время вычисления. Место скобок решает исход: (double)a / b делит дробно, а (double)(a / b) сначала делит нацело и только потом превращает 3 в 3.0.
Оператор «запятая» вычисляет выражения слева направо и возвращает последнее. Чаще всего он встречается в заголовке цикла с двумя счётчиками.
#include <stdio.h>
int main(void) {
int age = 20;
const char *status = (age >= 18) ? "взрослый" : "несовершеннолетний";
printf("%s\n", status);
int arr[10];
printf("sizeof(int) = %zu, sizeof arr = %zu, элементов = %zu\n",
sizeof(int), sizeof arr, sizeof arr / sizeof arr[0]);
int a = 7, b = 2;
printf("(double)a / b = %.1f\n", (double)a / b);
printf("(double)(a / b) = %.1f\n", (double)(a / b));
int i, j;
for (i = 0, j = 10; i < j; i += 3, j -= 3) { }
printf("i = %d, j = %d\n", i, j);
return 0;
}
Вывод:
взрослый
sizeof(int) = 4, sizeof arr = 40, элементов = 10
(double)a / b = 3.5
(double)(a / b) = 3.0
i = 6, j = 4
Приоритет операций в C
Приоритет операций определяет, как компилятор расставит невидимые скобки. Чем выше строка, тем сильнее оператор связывает операнды. Таблица по справочнику cppreference:
| Уровень | Операторы | Порядок |
|---|---|---|
| 1 | a++ a-- () [] . -> |
слева направо |
| 2 | ++a --a +a -a ! ~ (тип) *p &x sizeof _Alignof |
справа налево |
| 3 | * / % |
слева направо |
| 4 | + - |
слева направо |
| 5 | << >> |
слева направо |
| 6 | < <= > >= |
слева направо |
| 7 | == != |
слева направо |
| 8 | & |
слева направо |
| 9 | ^ |
слева направо |
| 10 | | |
слева направо |
| 11 | && |
слева направо |
| 12 | || |
слева направо |
| 13 | ?: |
справа налево |
| 14 | = += -= *= /= %= <<= >>= &= ^= |= |
справа налево |
| 15 | , |
слева направо |
Порядок (ассоциативность) решает спор равных: a - b - c считается как (a - b) - c, а a = b = c как a = (b = c).
Три ловушки приоритета
- Побитовое И слабее сравнения.
flags & 4 == 4разбирается какflags & (4 == 4), то естьflags & 1. - Сдвиг слабее сложения.
1 << 2 + 1равно1 << 3, то есть 8, а не 5. - Постфиксный
++сильнее разыменования.*p++читает значение и сдвигает указатель. Чтобы увеличить само значение, пишут(*p)++.
#include <stdio.h>
int main(void) {
unsigned flags = 6; /* 0110 */
if (flags & 4 == 4) printf("без скобок: флаг найден\n");
else printf("без скобок: флага нет\n");
if ((flags & 4) == 4) printf("со скобками: флаг найден\n");
int x = 1 << 2 + 1; /* сначала 2 + 1, потом сдвиг */
printf("1 << 2 + 1 = %d\n", x);
int arr[] = {10, 20, 30};
int *p = arr;
int v = *p++; /* берёт arr[0], потом сдвигает указатель */
printf("v = %d, теперь *p = %d\n", v, *p);
return 0;
}
Вывод:
без скобок: флага нет
со скобками: флаг найден
1 << 2 + 1 = 8
v = 10, теперь *p = 20
Оба компилятора с -Wall предупреждают о первых двух строках. GCC пишет так:
09_prec.c:5:15: warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]
09_prec.c:9:15: warning: suggest parentheses around '+' inside '<<' [-Wparentheses]
Правило на практике простое: таблицу помнить не обязательно, достаточно ставить скобки везде, где смешиваются побитовые, логические и арифметические операторы.
И ещё одно: приоритет говорит, как сгруппировать выражение, но не в каком порядке вычислять операнды. В f() + g() компилятор вправе вызвать g() первой. Порядок гарантирован только у &&, ||, ?: и запятой, так это описано и в справке Microsoft по приоритету и порядку вычисления. Запятая между аргументами функции служит простым разделителем и порядок вычисления не задаёт.
Неявные преобразования типов в выражениях
Если в выражении встречаются разные типы, C приводит их к общему по «обычным арифметическим преобразованиям»: int с double даёт double, char и short перед вычислением расширяются до int. Опаснее всего смесь знакового и беззнакового целого: знаковое превращается в беззнаковое, и −1 становится огромным числом.
#include <stdio.h>
int main(void) {
int n = -1;
unsigned int u = 1;
if (n < u) printf("-1 < 1\n");
else printf("-1 >= 1u: n превратилось в %u\n", (unsigned)n);
return 0;
}
Вывод:
-1 >= 1u: n превратилось в 4294967295
Такая ошибка часто прячется в циклах, где счётчик int сравнивают с результатом strlen() или sizeof (оба size_t). В C оба компилятора предупреждают об этом только с флагом -Wextra, одного -Wall мало (проверили на GCC 13.3 и Clang 17):
11_unsigned.c:6:11: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
Частые ошибки с операторами и что с ними делать

=вместо==в условии.if (errors = 5)присваивает пятёрку и всегда идёт в ветку. Решение: собирайте с-Wall, GCC ответитsuggest parentheses around assignment used as truth value.- Целочисленное деление там, где нужна дробь.
double avg = sum / count;при целыхsumиcountтеряет дробную часть до присваивания. Решение:(double)sum / count. - Проверка нечётности через
% 2 == 1. На отрицательных числах остаток равен −1. Решение:n % 2 != 0. - Скобки при смешении
&,|,<<со сравнением и арифметикой. Решение: ставить скобки всегда, даже если вы уверены в приоритете. - Изменение и повторное использование одной переменной в выражении.
i = i++ + 1,a[i] = i++и подобное. Решение: разнести на две строки. - Сравнение
intсunsignedиsize_t. Решение: счётчик цикла делатьsize_tи включать-Wextra.
Четыре ошибки из шести (первую, четвёртую, пятую и шестую) ловит компилятор, если включить предупреждения: gcc -Wall -Wextra main.c. Целочисленное деление и проверку % 2 == 1 он пропускает, их находят тестами. Список того, что входит в каждый флаг, есть в документации GCC.
Где освоить язык 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 (язык Си)
План входа в профессию по месяцам есть в статье как стать программистом на Си с нуля, а где такие специалисты работают, рассказывает разбор кто такой разработчик на Си.




