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

Три разных слова

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 определено так: последовательность операций и их операндов, задающая некоторое вычисление. Результат выражения проявляется в двух местах:

  1. в возвращаемом значении;
  2. в побочных эффектах (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) — разделители, а не эта операция; порядок вычисления аргументов функции не гарантирован.