Оператор описывает действие, выражение задаёт вычисление, операция действует на операнды. В русской терминологии эти слова легко перепутать, поэтому стоит развести их один раз и дальше опираться на различие.
Три разных слова
return 2 + 2 * 2;- вся строка — оператор (инструкция, statement), здесь оператор возврата;
2 + 2 * 2— выражение (expression);+и*— знаки операций, числа служат операндами.
Литерал 2 сам по себе тоже выражение: знак операции для этого не нужен.
Выражение вместе с точкой с запятой образует оператор-выражение:
int main() {
2 + 2; // допустимо, но бесполезно
return 0;
}Код компилируется, результат сложения нигде не используется, ничего не происходит. Позже появятся выражения с побочными эффектами — например меняющие переменную — и их смысл не сведётся к получению численного результата.
Арность
Арность — количество операндов операции. Унарный минус в -x имеет один
операнд, вычитание в x - y — два, условная операция (встретится позже) — три.
Одна и та же запись бывает разной арности: - в -x и в x - y это разные
операции.
Приоритет
Приоритет определяет группировку частей выражения:
2 + 2 * 2 // 2 + (2 * 2) == 6
(2 + 2) * 2 // 8Запоминать всю таблицу приоритетов не нужно — там несколько десятков уровней.
Полезнее помнить, что у арифметики приоритет выше, чем у сравнений, а у
сравнений выше, чем у && и ||, и в сомнительных местах ставить скобки.
Ассоциативность
При одинаковом приоритете важно направление группировки. Вычитание группируется слева направо:
20 - 5 - 3 // (20 - 5) - 3 == 12Присваивание — справа налево:
int x = 0;
int y = 0;
x = y = 7; // x = (y = 7)Сначала 7 присваивается y, а результат внутреннего присваивания используется
для присваивания x. Присваивание здесь — операция со значением, а не
отдельный оператор.
Группировка — не порядок вычисления
Это разные вещи, и путаница между ними — источник тонких ошибок.
(a + b) * (c + d)Скобки говорят, что произведение берётся от двух сумм. Они не требуют
вычислить сначала именно левую сумму: компилятор вправе посчитать c + d
первым.
Пока выражения только читают значения, различие незаметно. Как только появятся побочные эффекты — изменение переменной, вызов функции, которая что-то печатает — порядок станет существенным, а язык его в общем случае не фиксирует.
Заметное исключение — && и ||: у них порядок вычисления операндов
гарантирован, см. Логические операции и короткое замыкание.
Тонкости
- Тип выражения определяется типами операндов, а не тем, куда записывается результат — см. Арифметические преобразования типов.
sizeofисследует тип выражения, не вычисляя его.- Точка с запятой завершает конструкцию, а не строку в редакторе. После
return 0она нужна, после#include <iostream>— нет, а лишняя;после условия молча ломает if. - Структуру блоков задают фигурные скобки, а не отступы. Отступы нужны человеку, и писать без них — способ обмануть самого себя.
Частые ошибки
- Ожидать конкретный порядок вычисления операндов там, где язык его не задаёт.
- Полагаться на память в вопросе приоритетов вместо скобок.
- Писать
0 < x < 10, считая это двойным неравенством. - Ставить
;послеif (...)или после)в других конструкциях.
Углубление — L02
Составной оператор, или блок (compound statement), — последовательность операторов в фигурных скобках:
{
<statement>
<statement>
...
}Блок сам является оператором, поэтому его ставят там, где синтаксис ожидает
ровно один: после if, while, for. Блоки бывают вложенными и могут
содержать любое число операторов, в том числе ноль — {} это пустой блок.
Кроме синтаксиса, у блока есть смысл: он ограничивает время жизни объявленных внутри переменных — см. Область действия.
Углубление — L03
Выражение в L03 определено так: последовательность операций и их операндов, задающая некоторое вычисление. Результат выражения проявляется в двух местах:
- в возвращаемом значении;
- в побочных эффектах (side effects) — изменениях состояния программы.
Поэтому операцию полезно рассматривать с двух сторон: что она возвращает и что меняет по пути. Кроме типа у выражения есть категория значения — материален ли результат, см. Категории значения.
Присваивание возвращает переменную
Присваивающие операции — = и составные +=, %=, |= и т. п. — не только
меняют значение, но и возвращают результат. Причём результат — не число, а
сама переменная слева (lvalue). Отсюда цепочки и повторное присваивание:
std::cout << (x = y * y); // печатает новое значение x
z = x = y *= y; // z = (x = (y *= y))
(x = y) = 5; // x == 5
(x = y) *= 0; // x == 0Результат неприсваивающей операции — просто значение, которого нет в памяти,
поэтому 5 = x; и x * y = 10; дают lvalue required as left operand of assignment.
Операция запятая
Запятая объединяет несколько выражений в одно:
++x, y--, std::cout << x + y, 5;Гарантируется, что левый операнд вычисляется раньше правого, а результат
операции — результат правого операнда. Это ещё одно исключение из правила
«группировка — не порядок вычисления», наряду с && и ||.
Не из лекции
Запятые в объявлении
int a = 1, b = 2;и в вызове функцииf(a, b)— разделители, а не эта операция; порядок вычисления аргументов функции не гарантирован.