LEARNING · STAGE 00 · PROGRAMMING BASICS

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

В прошлом уроке появилось предупреждение: 10 / 3 в int даёт не 3.33, а просто 3. Пора разобраться, почему — и заодно пройти операторы сравнения и логики, без которых не построить ни одно условие в следующем уроке.

01Что ты изучишь

  • Арифметические операторы + - * / % и ловушку целочисленного деления
  • Операторы сравнения == != < > <= >= — их результат всегда bool
  • Логические операторы && || ! и что такое короткое замыкание
  • Частую ошибку — перепутать = (присваивание) и == (сравнение)

02Основная идея: операторы — тот же калькулятор, но с новыми кнопками

Обычный калькулятор умеет + - * /. C++ добавляет к этому набору: % (остаток от деления), кнопки сравнения (равно/больше/меньше — но вместо числа они выдают «да» или «нет»), и кнопки логики, которые комбинируют несколько ответов «да/нет» в один. Никакой новой концепции здесь нет — оператор по-прежнему часть выражения, которое вычисляется в значение, как и x + 5 из второго урока. Просто теперь типы результата бывают разные: арифметика даёт число, сравнение и логика — всегда bool.

03Арифметические операторы

Сложение и вычитание уже встречались (x + 5) — остальные три из полного набора (* / %) работают по тому же принципу: каждый оператор — часть выражения, которое вычисляется в значение, а не команда сама по себе.

Именно на этом наборе и произошла та самая ловушка, о которой предупреждал прошлый урок. На бумаге 17, делённое на 5, даёт 3.4. В C++, если обе переменные — int, 17 / 5 даёт ровно 3, без округления, дробная часть просто не появляется. Компьютер здесь не ошибается и не глючит: если результат должен быть int, а int дробей не хранит, единственный выход — отбросить дробную часть целиком. Чтобы получить точный результат, хотя бы один из операндов должен быть double ещё до деления:

main.cppC++
int total = 17;
int players = 5;

int perPlayerInt = total / players;                              // 3 — дробная часть отброшена
int remainder = total % players;                                  // 2 — остаток, то, что не поместилось
double perPlayerExact = static_cast<double>(total) / players;    // 3.4 — точный результат

% (остаток от деления) — то, что дополняет целочисленное деление: total / players даёт «сколько досталось каждому», total % players — «сколько осталось нераспределённым». static_cast<double>(total) — явное приведение типа: превращает значение total из int во временное значение типа double прямо перед делением, чтобы деление выполнилось по правилам double, а не int (подробнее приведения типов — в Stage 01). Конструкция состоит из трёх частей, у каждой своя роль:

  • static_cast — сам оператор: команда «преобразовать явно», а не «сложить» или «сравнить».
  • <double> — целевой тип в угловых скобках: во что именно превратить значение.
  • (total) — исходное выражение в круглых скобках: что именно преобразуется; результат — новое временное значение, сам total при этом остаётся int и не меняется.

Итог всей конструкции — обычное значение типа double, с которым дальше можно работать как с любым другим double (например, сразу поделить на players). static_cast — не единственный способ превратить одно значение в другое; у него есть три соседних варианта, и выбор между ними не случаен:

ВариантКогда
static_cast<T>(x)обычное контролируемое преобразование — стандартный выбор (как здесь, int → double)
неявное преобразованиекогда компилятор безопасно делает нужное преобразование сам — например, double d = 5; без всякого static_cast
C-style (T)xунаследовано из языка C; обычно избегать в современном C++ — не показывает намерение и опаснее, чем static_cast
reinterpret_castсовсем другая, гораздо более редкая и опасная задача — не для чисел; тема вне этого курса

04Операторы сравнения

«Жив ли персонаж?» — по сути вопрос «hp больше нуля?», а ответ на такой вопрос — не число, а да/нет. Ровно это и делают операторы сравнения: берут два значения и превращают их в bool — true или false, ничего третьего:

ОператорЗначитПример
==равноhp == 0
!=не равноhp != 0
<меньшеhp < 20
>большеhp > 0
<=меньше или равноhp <= 0
>=больше или равноhp >= 100

Самая частая ошибка новичков — написать hp = 0 там, где имелось в виду сравнение hp == 0. Это не опечатка, которую поймает компилятор: hp = 0 — совершенно законное присваивание (см. второй урок), просто оно не сравнивает, а меняет значение hp на 0. Подробнее — в разделе «Типичные ошибки».

05Логические операторы

Одного сравнения часто мало: «можно продолжать» зависит не только от того, жив ли персонаж, но и от того, есть ли у него ключ — два отдельных bool-значения нужно как-то свести в один ответ. Для этого — логические операторы, которые берут одно или несколько значений bool и комбинируют их в новое значение bool:

ОператорЗначитПримерИстина, когда
&&И (and)hp > 0 && hasKeyоба условия истинны
||ИЛИ (or)hp > 0 || hasShieldхотя бы одно условие истинно
!НЕ (not)!isAliveпереворачивает значение: было true — стало false

&& и || вычисляются с коротким замыканием (short-circuit): в a && b, если a уже false, C++ даже не вычисляет b — результат в любом случае будет false. Аналогично для a || b: если a уже true, b не вычисляется. Сейчас это не критично, но станет важным позже, когда b будет не просто переменной, а вызовом функции с побочным эффектом — короткое замыкание означает, что такой вызов может не произойти вовсе.

06Приоритет операторов

Как и в математике (где умножение выполняется раньше сложения), у операторов C++ есть порядок вычисления. От высшего приоритета к низшему:

ГруппаОператорыПример
1. Скобки()(a + b) * c — сначала скобки
2. Унарное НЕ!!isAlive
3. Умножение, деление, остаток* / %a + b * c — сначала b * c
4. Сложение, вычитание+ -hp - damage > 0 — сначала вычитание
5. Сравнение< > <= >=hp > 0 && hasKey — сначала сравнение
6. Равенство== !=hp == 0 || hasShield — сначала равенство
7. Логическое И&&a && b || c — сначала a && b
8. Логическое ИЛИ||вычисляется после &&
9. Присваивание=result = a && b — вычисляется последним

Держать всю таблицу в голове не обязательно: при малейшем сомнении добавь скобки явно — (hp - damage) > 0 читается сразу, без необходимости вспоминать приоритет.

07Собираем условие «можно продолжать» шаг за шагом

Программа-персонаж из прошлых двух уроков растёт дальше: к hp добавляются damage (урон от удара) и hasKey (есть ли ключ) — новые переменные для новой механики, не связанной напрямую с типами из прошлого урока, но той же самой программы. Тот же принцип, что и раньше: одна программа, к которой на каждом шаге добавляется ещё один вид оператора — сначала арифметика, потом сравнение, потом логика.

Шаг 1 — только арифметикаC++
int hp = 40;
int damage = 25;
int hpAfterHit = hp - damage;
std::cout << "HP after hit: " << hpAfterHit << "\n";
Шаг 2 — добавили сравнениеC++
int hp = 40;
int damage = 25;
int hpAfterHit = hp - damage;
bool isAlive = hpAfterHit > 0;
std::cout << "HP after hit: " << hpAfterHit << ", Alive: " << isAlive << "\n";
Шаг 3 — добавили логикуC++
int hp = 40;
int damage = 25;
bool hasKey = true;
int hpAfterHit = hp - damage;
bool isAlive = hpAfterHit > 0;
bool canProceed = isAlive && hasKey;
std::cout << "Alive: " << isAlive << ", Can proceed: " << canProceed << "\n";
Шаг 4 — всё в одном выражении, по таблице приоритетаC++
int hp = 40;
int damage = 25;
bool hasKey = true;
bool canProceed = (hp - damage) > 0 && hasKey;   // - раньше >, > раньше &&
std::cout << "Can proceed: " << canProceed << "\n";

Скобки вокруг (hp - damage) здесь не обязательны — по таблице выше вычитание всё равно выполнится раньше сравнения, — но делают выражение читаемым сразу, без сверки с таблицей приоритетов.

08Хуже → лучше: = вместо == — тихий баг

Задача — проверить, что hp равен 0, и учесть ещё наличие ключа. Один символ здесь меняет поведение полностью, а компилятор об этом не предупредит.

Хуже — опечатка = вместо ==C++
int hp = 0;
bool isAlive = (hp = 5);   // опечатка: присваивание, а не сравнение
std::cout << isAlive << " " << hp;

Компилируется без единого предупреждения на многих настройках — hp = 5 синтаксически совершенно законное выражение (инструкция присваивания из второго урока), просто здесь оно оказалось не в том месте.

консоль (иллюстрация)
./main 1 5 hp тихо стал равен 5 — задумывалась только проверка.

Программа не упала и не пожаловалась — она просто сделала не то, что задумывалось: hp перезаписан на 5, а isAlive стал true, потому что результат присваивания (само значение 5) при преобразовании в bool — любое ненулевое число становится true. Это опаснее ошибки компиляции: здесь нечего чинить «по ошибке компилятора», баг нужно заметить самому.

Лучше — сравнение вместо присваиванияC++
int hp = 0;
bool isAlive = (hp == 5);   // сравнение
std::cout << isAlive << " " << hp;

Теперь hp остаётся равным 0 (сравнение не меняет значения), а isAlive честно равен false, потому что 0 ≠ 5.

Лучше всего — сравнение и логика вместе, с подписанным выводомC++
int hp = 0;
bool hasKey = true;
bool canProceed = (hp == 0) && hasKey;
std::cout << "hp: " << hp << ", canProceed: " << canProceed << "\n";

Скобки вокруг hp == 0 здесь не обязательны (сравнение по приоритету всё равно выполнится раньше &&), но явно показывают, что именно сравнивается, прежде чем результат участвует в логическом И — это ровно та привычка, которая экономит на отладке подобных опечаток в будущем.

09Попробуй изменить

  • В примере из раздела «Арифметические операторы» замени total на 20 — perPlayerInt станет 4, remainder станет 0, perPlayerExact останется 4.0.
  • Выведи hp > 0 напрямую через std::cout — увидишь 1 или 0, а не слово true/false (как и с обычным bool в прошлом уроке).
  • Замени && на || в условии с двумя переменными — результат может измениться, если хотя бы одна из частей true.

10Живая отладка: типичные ошибки

Важное отличие от прошлых уроков: все три ошибки ниже — не ошибки компиляции. Код успешно соберётся и запустится в каждом случае; проблема появится только во время выполнения — либо как неверный результат, либо как крах программы. Именно такие баги труднее всего находить, потому что компилятор не подсказывает, где искать.

= вместо ==
if (hp = 0)   // опечатка: присваивание вместо сравнения (нужно ==)

Условие с этим выражением всегда окажется ложным, а hp будет молча обнулён. Полный разбор с примером вывода — в разделе «Хуже → лучше» выше; синтаксис if подробно разбирается в следующем уроке.

Целочисленное деление вместо точного
int total = 17;
int players = 5;
double average = total / players;   // делится как int/int ДО присваивания в double
std::cout << average;
консоль (иллюстрация)
./main 3 Ожидалось 3.4 — получено 3.

Дробная часть теряется в момент самого деления, а не при сохранении в double — переменная average лишь хранит уже округлённый результат 3 как 3.0. Приводить к double нужно до деления: static_cast<double>(total) / players.

Деление на ноль в остатке
int total = 17;
int divisor = 0;
int x = total % divisor;   // деление/остаток на 0 для int — неопределённое поведение
консоль (иллюстрация)
./main Floating point exception (core dumped)

На практике это чаще всего немедленный крах программы (сообщение выше — типичный вид на Linux, несмотря на название «floating point» — историческая деталь именования этого сигнала, само деление здесь целочисленное). Перед делением на переменную, которая теоретически может быть 0, стоит убедиться, что это не так — эта проверка станет естественной, когда появится if в следующем уроке.

11Проверь себя

Три вопроса: предсказание результата целочисленного деления, обнаружение классической опечатки, и разбор приоритета операторов.

Предскажи результат
Чему равно a — и почему не 3.4?
int total = 17;
int players = 5;
int a = total / players;
Показать объяснение

a = 3 — целочисленное деление int / int отбрасывает дробную часть целиком, без округления к ближайшему целому. Чтобы получить 3.4, нужно привести хотя бы один операнд к double ДО деления (static_cast<double>(total) / players).

Найди ошибку
Программист хотел проверить, что hp равен нулю. Компилятор пропустил код без единого предупреждения. Что реально выведет программа?
int hp = 10;
bool isAlive = (hp = 0);
std::cout << hp << " " << isAlive;
Показать объяснение

Выведет 0 0. hp = 0 — не сравнение, а присваивание (один = вместо ==): hp становится равен 0, а результатом всего выражения присваивания оказывается само записанное значение (0), которое при преобразовании в bool даёт false — ноль единственное число, дающее false. Компилятор не обязан на это указывать, это законный синтаксис (см. §08).

Приоритет операторов
В каком порядке вычислится это выражение?
bool result = hp - damage > 0 && hasKey;
Показать объяснение

Сначала hp - damage (арифметика, группа 4 в таблице §06), затем результат сравнивается с 0 (сравнение, группа 5), и только потом это сравнение объединяется с hasKey через && (группа 7) — арифметика и сравнение всегда выполняются раньше логических операторов. Явные скобки — (hp - damage) > 0 && hasKey — дали бы тот же результат, но читаются быстрее, без сверки с таблицей.

12Практика

★★ На основе total и players вычисли: целочисленное деление (/), остаток (%) и точное деление в double (с приведением типа). Выведи все три значения.

★★ Есть hp и hasKey. Вычисли и выведи через std::cout результат сравнения (hp > 0 или hp == 0) и результат логического И/ИЛИ, объединяющего это сравнение с hasKey.

★★★ Дано int hp, int damage и bool hasShield. Вычисли hp после урона (арифметика), проверь, что он больше 0 (сравнение), и посчитай canProceed — можно продолжать, если персонаж жив ИЛИ у него есть щит (логика). Используй хотя бы по одному оператору из каждой группы: арифметика, сравнение, логика. Выведи результат через std::cout.

13Термины

ТерминЗначит
Оператор (operator)символ вроде +, ==, &&, вычисляющий новое значение из одного или нескольких операндов
Целочисленное делениеделение int на int, при котором дробная часть результата отбрасывается
Приоритет операторовпорядок, в котором вычисляются операторы в одном выражении без скобок
Короткое замыкание (short-circuit)пропуск вычисления второго операнда &&/||, когда результат уже ясен по первому

14Что ты теперь умеешь

До этого урока переменные могли только храниться и выводиться. Теперь с ними можно вычислять:

  • Ты можешь предсказать результат целочисленного деления int / int и объяснить, почему для точного результата нужно привести один из операндов к double до деления, а не после.
  • Ты можешь превратить любое условие вида «A больше/равно/меньше B» в выражение типа bool с помощью операторов сравнения.
  • Ты можешь объединить несколько условий bool в одно через &&/||/! и предсказать, когда сработает короткое замыкание.
  • Ты можешь заметить опечатку = вместо == в чужом или своём коде и объяснить, почему компилятор её не ловит.
  • Ты можешь применить таблицу приоритета операторов, чтобы предсказать порядок вычисления смешанного выражения из арифметики, сравнения и логики — или расставить скобки, если сомневаешься.

15Кратко

  • + - * / % — арифметика; / для двух int отбрасывает дробную часть, для точного результата приводи к double до деления.
  • % — остаток от целочисленного деления.
  • == != < > <= >= — сравнение, результат всегда bool.
  • && || ! — логические операторы над bool; &&/|| вычисляются с коротким замыканием.
  • = (присваивание) и == (сравнение) — разные операторы, путаница между ними не всегда ловится компилятором.

Теперь можно вычислить bool-условие вроде canProceed — но пока с ним ничего нельзя сделать в зависимости от результата. Следующий урок вводит if/else: то, ради чего вообще нужен был весь этот урок про сравнение и логику.

🎮 А как это связано с Unreal Engine?

Все эти операторы — часть самого языка C++, работают в Unreal-коде без изменений. Для более сложной математики (степени, корни, минимум/максимум, ограничение диапазона) Unreal добавляет поверх стандартных операторов библиотеку FMath:

Пример (Unreal C++)C++
float clampedHp = FMath::Clamp(hpAfterHit, 0.0f, 100.0f);   // не даёт HP выйти за 0..100
float dist = FMath::Sqrt(FMath::Square(dx) + FMath::Square(dy));  // расстояние между точками

А для векторных величин (позиция, скорость, направление — тройки {X, Y, Z}) те же арифметические и логические операторы работают через тип FVector, который их перегружает:

Пример (Unreal C++)C++
FVector velocity = FVector(1.0f, 0.0f, 0.0f) * speed;   // тот же *, но для вектора
bool reachedGoal = FVector::Dist(currentPos, goalPos) < 50.0f;   // тот же <

Базовые + - * / % == && || и приоритет между ними остаются ровно теми же, что в этом уроке — Unreal лишь добавляет типы и функции поверх них. Подробности FMath и FVector — в Stage 07+.