Операторы — это глаголы языка C: они берут значения и порождают новые. Их около 40, но дюжина покрывает большую часть кода, а у двух из них ловушки настолько острые, что заслуживают отдельных разделов.
Арифметические операторы
(%% в строке формата выводит сам знак процента — одиночный % начал бы спецификатор формата.)
Пять бинарных арифметических операторов — это + - * / %, плюс унарный - для смены знака и +, который практически ничего не делает.
Ловушка целочисленного деления
a / b с двумя целыми выполняет целочисленное деление и отбрасывает остаток. Никакого округления нет — результат усекается в сторону нуля.
Последний случай — это баг, который доживает до продакшена. Присваивание в double не помогает: деление total / count уже выполнено и уже является int до того, как произойдёт присваивание. Исправление — сделать один операнд числом с плавающей точкой внутри выражения, обычно с помощью приведения типа.
Два связанных правила: деление на ноль в целых числах — неопределённое поведение, которое обычно приводит к аварийному завершению программы, а INT_MIN / -1 вызывает переполнение.
Оператор остатка
% даёт остаток и работает только с целыми числами:
Проверка x % 2 == 0 на чётность и использование % n для зацикливания индекса по массиву — два повседневных применения.
С отрицательными числами % в C следует знаку делимого:
Поэтому x % 2 == 1 — сломанная проверка на нечётность, если x может быть отрицательным: -7 % 2 равно -1. Используйте вместо неё x % 2 != 0.
Для остатка от чисел с плавающей точкой % даёт ошибку компиляции; используйте fmod() из math.h.
Присваивание и составное присваивание
= сохраняет значение. Составные формы объединяют операцию с сохранением:
x += 5 означает x = x + 5, но вычисляет x только один раз — что важно, когда целью является что-то вроде arr[compute_index()].
Присваивание само по себе является выражением, возвращающим присвоенное значение, — именно поэтому работает a = b = c = 0 (присваивание идёт справа налево) и поэтому ошибка = вместо == компилируется молча.
Инкремент и декремент
++ прибавляет единицу, -- вычитает единицу. У каждого есть префиксная и постфиксная форма:
Как отдельная инструкция обе формы взаимозаменяемы, а i++ — общепринятый выбор в заголовке for без более глубокой причины, чем традиция.
Чего делать нельзя — так это использовать переменную больше одного раза в выражении, где она также изменяется:
int i = 5;
int x = i++ + i++; /* неопределённое поведение */
arr[i] = i++; /* неопределённое поведение */
printf("%d %d", i++, i);/* неопределённое поведение */
C не определяет порядок, в котором вычисляются эти подвыражения, поэтому результат не просто неуточнён — неопределённым становится поведение всей программы. Разбейте инструкцию на две, и неоднозначность исчезнет.
Операторы сравнения
Их шесть, и все они дают int: 1 для истины и 0 для лжи.
То, что результатом является обычный int, а не отдельный логический тип, — определяющая черта C; см. логический тип в C, чтобы понять, что из этого следует, включая баг = вместо ==.
Две ошибки сравнения специфичны для C. Цепочки не работают так, как подсказывает математика: if (1 < x < 10) всегда истинно, потому что 1 < x даёт 0 или 1, а затем это сравнивается с 10. Пишите if (x > 1 && x < 10). И сравнение строк через == сравнивает указатели, а не текст — используйте strcmp из string.h.
Логические операторы
&& — это И, || — ИЛИ, ! — НЕ. Все три считают истиной любое ненулевое значение.
Они вычисляются лениво: && останавливается, как только один операнд оказывается ложным, а || — как только один оказывается истинным. Остальное никогда не вычисляется, и это не просто оптимизация — это гарантия, на которую можно опереться ради безопасности:
Поменяйте эти две проверки местами — и вызов с NULL разыменует нулевой указатель и уронит программу. Сначала проверка, потом использование — порядок в цепочке && является частью логики.
Условный оператор
Единственный трёхместный оператор C выбирает между двумя значениями:
условие ? значение_если_истина : значение_если_ложь. Это выражение, поэтому оно помещается там, где инструкция if не может: внутри аргумента printf, в инициализаторе. Держите его коротким; вложенные условные операторы быстро становятся нечитаемыми.
Побитовые операторы, коротко
Они работают с отдельными битами целого числа:
Их применяют для флагов, масок и аппаратных регистров. Две предосторожности: & и | — это не && и || (они не вычисляются лениво и работают побитово), а сдвиг знакового значения или сдвиг больше ширины типа не определён. Для работы с битами используйте беззнаковые типы.
Приоритет
Когда в выражении нет скобок, эта таблица решает, что с чем связывается. Наивысший приоритет — первым:
| Уровень | Операторы | Ассоциативность |
|---|---|---|
| 1 | () [] -> . x++ x-- | слева направо |
| 2 | ! ~ ++x --x +x -x *p &x sizeof (type) | справа налево |
| 3 | * / % | слева направо |
| 4 | + - | слева направо |
| 5 | << >> | слева направо |
| 6 | < <= > >= | слева направо |
| 7 | == != | слева направо |
| 8 | & | слева направо |
| 9 | ^ | слева направо |
| 10 | побитовое ИЛИ | слева направо |
| 11 | && | слева направо |
| 12 | логическое ИЛИ | слева направо |
| 13 | ?: | справа налево |
| 14 | = += -= *= /= %= и им подобные | справа налево |
| 15 | , | слева направо |
Практические следствия:
a + b * c /* означает a + (b * c) */
a < b == c < d /* означает (a < b) == (c < d) - редко то, что вы имели в виду */
x & 1 == 0 /* означает x & (1 == 0), то есть x & 0 - классический баг */
*p++ /* означает *(p++) - разыменовать p, затем сдвинуть его */
Третью строку стоит запомнить: == связывается сильнее, чем &, поэтому проверкам битовых масок нужны скобки — (x & 1) == 0.
Никто не помнит все пятнадцать уровней, и никому это не нужно. Выучите, что * сильнее +, что сравнения сильнее &&, а && сильнее ||, а всё остальное получает скобки:
if ((flags & MASK) != 0 && (count > 0 || force)) { ... }
В этой строке есть избыточные скобки, и она от этого только лучше.
Часто задаваемые вопросы
Что делает % в C?
% — это оператор взятия остатка: он даёт остаток от целочисленного деления. 17 % 5 равно 2. Он работает только с целыми числами — применение его к float или double вызовет ошибку компиляции, а эквивалент для чисел с плавающей точкой — fmod() из math.h.
Почему 5 / 2 даёт 2 в C?
Потому что оба операнда — целые числа, поэтому C выполняет целочисленное деление и отбрасывает дробную часть. Сделайте одну сторону значением с плавающей точкой, чтобы получить 2.5: 5 / 2.0, — или приведите один операнд: (double)a / b.
В чём разница между i++ и ++i?
Оба прибавляют единицу к i. ++i (префиксная форма) сначала увеличивает и возвращает новое значение; i++ (постфиксная) возвращает старое значение, а увеличивает после. Как отдельная инструкция они одинаковы — разница важна, только когда результат используется, как в int b = a++; против int b = ++a;.
Что такое приоритет операторов в C?
Это порядок, в котором операторы связываются, когда в выражении нет скобок. *, / и % связываются сильнее, чем + и -, которые сильнее сравнений, которые сильнее &&, затем ||, затем присваивание. Если сомневаетесь — ставьте скобки: они ничего не стоят и снимают вопрос.