Menu

Операторы в C: арифметика, логика и приоритет

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Операторы — это глаголы языка 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?

Это порядок, в котором операторы связываются, когда в выражении нет скобок. *, / и % связываются сильнее, чем + и -, которые сильнее сравнений, которые сильнее &&, затем ||, затем присваивание. Если сомневаетесь — ставьте скобки: они ничего не стоят и снимают вопрос.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ