Операторы: арифметика, сравнение, логика
В прошлом уроке появилось предупреждение: 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 ещё до деления:
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 (есть ли ключ) — новые переменные для новой механики, не связанной напрямую с типами из прошлого урока, но той же самой программы. Тот же принцип, что и раньше: одна программа, к которой на каждом шаге добавляется ещё один вид оператора — сначала арифметика, потом сравнение, потом логика.
int hp = 40;
int damage = 25;
int hpAfterHit = hp - damage;
std::cout << "HP after hit: " << hpAfterHit << "\n";
int hp = 40;
int damage = 25;
int hpAfterHit = hp - damage;
bool isAlive = hpAfterHit > 0;
std::cout << "HP after hit: " << hpAfterHit << ", Alive: " << isAlive << "\n";
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";
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, и учесть ещё наличие ключа. Один символ здесь меняет поведение полностью, а компилятор об этом не предупредит.
int hp = 0;
bool isAlive = (hp = 5); // опечатка: присваивание, а не сравнение
std::cout << isAlive << " " << hp;
Компилируется без единого предупреждения на многих настройках — hp = 5 синтаксически совершенно законное выражение (инструкция присваивания из второго урока), просто здесь оно оказалось не в том месте.
Программа не упала и не пожаловалась — она просто сделала не то, что задумывалось: hp перезаписан на 5, а isAlive стал true, потому что результат присваивания (само значение 5) при преобразовании в bool — любое ненулевое число становится true. Это опаснее ошибки компиляции: здесь нечего чинить «по ошибке компилятора», баг нужно заметить самому.
int hp = 0;
bool isAlive = (hp == 5); // сравнение
std::cout << isAlive << " " << hp;
Теперь hp остаётся равным 0 (сравнение не меняет значения), а isAlive честно равен false, потому что 0 ≠ 5.
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;
Дробная часть теряется в момент самого деления, а не при сохранении в double — переменная average лишь хранит уже округлённый результат 3 как 3.0. Приводить к double нужно до деления: static_cast<double>(total) / players.
int total = 17;
int divisor = 0;
int x = total % divisor; // деление/остаток на 0 для int — неопределённое поведение
На практике это чаще всего немедленный крах программы (сообщение выше — типичный вид на 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 (с приведением типа). Выведи все три значения.
Три отдельные переменные — целочисленное деление, остаток и приведённое к double деление (§03) — и все три должны реально попасть в std::cout, не только быть вычислены.
Решение: int perPlayerInt = total / players; int remainder = total % players; double perPlayerExact = static_cast<double>(total) / players; — затем выведи все три через std::cout.
★★ Есть hp и hasKey. Вычисли и выведи через std::cout результат сравнения (hp > 0 или hp == 0) и результат логического И/ИЛИ, объединяющего это сравнение с hasKey.
Сначала сравнение hp с 0 — результат bool, сохрани его в переменную. Потом объедини эту переменную с hasKey через && или || — тоже в переменную. Обе переменные должны попасть в std::cout.
Решение: bool isAlive = hp > 0; bool canProceed = isAlive && hasKey; — затем std::cout << isAlive << canProceed;
★★★ Дано int hp, int damage и bool hasShield. Вычисли hp после урона (арифметика), проверь, что он больше 0 (сравнение), и посчитай canProceed — можно продолжать, если персонаж жив ИЛИ у него есть щит (логика). Используй хотя бы по одному оператору из каждой группы: арифметика, сравнение, логика. Выведи результат через std::cout.
Три шага подряд, как в разделе «Собираем условие» выше: сначала арифметика (hp - damage), потом сравнение результата с 0, потом логика — сравнение объединяется с hasShield.
Каждый из трёх результатов — арифметика, сравнение, логика — должен реально попасть в std::cout, а не просто быть вычислен где-то в теле main().
Решение: int hpAfterHit = hp - damage; bool isAlive = hpAfterHit > 0; bool canProceed = isAlive || hasShield; — затем std::cout << hpAfterHit << isAlive << canProceed; (можно и в одно выражение, как в шаге 4 раздела «Собираем условие» выше).
13Термины
| Термин | Значит |
|---|---|
| Оператор (operator) | символ вроде +, ==, &&, вычисляющий новое значение из одного или нескольких операндов |
| Целочисленное деление | деление int на int, при котором дробная часть результата отбрасывается |
| Приоритет операторов | порядок, в котором вычисляются операторы в одном выражении без скобок |
| Короткое замыкание (short-circuit) | пропуск вычисления второго операнда &&/||, когда результат уже ясен по первому |
14Что ты теперь умеешь
До этого урока переменные могли только храниться и выводиться. Теперь с ними можно вычислять:
- Ты можешь предсказать результат целочисленного деления
int / intи объяснить, почему для точного результата нужно привести один из операндов кdoubleдо деления, а не после. - Ты можешь превратить любое условие вида «A больше/равно/меньше B» в выражение типа
boolс помощью операторов сравнения. - Ты можешь объединить несколько условий
boolв одно через&&/||/!и предсказать, когда сработает короткое замыкание. - Ты можешь заметить опечатку
=вместо==в чужом или своём коде и объяснить, почему компилятор её не ловит. - Ты можешь применить таблицу приоритета операторов, чтобы предсказать порядок вычисления смешанного выражения из арифметики, сравнения и логики — или расставить скобки, если сомневаешься.
15Кратко
+ - * / %— арифметика;/для двухintотбрасывает дробную часть, для точного результата приводи кdoubleдо деления.%— остаток от целочисленного деления.== != < > <= >=— сравнение, результат всегдаbool.&& || !— логические операторы надbool;&&/||вычисляются с коротким замыканием.=(присваивание) и==(сравнение) — разные операторы, путаница между ними не всегда ловится компилятором.
Теперь можно вычислить bool-условие вроде canProceed — но пока с ним ничего нельзя сделать в зависимости от результата. Следующий урок вводит if/else: то, ради чего вообще нужен был весь этот урок про сравнение и логику.
Все эти операторы — часть самого языка C++, работают в Unreal-коде без изменений. Для более сложной математики (степени, корни, минимум/максимум, ограничение диапазона) Unreal добавляет поверх стандартных операторов библиотеку FMath:
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, который их перегружает:
FVector velocity = FVector(1.0f, 0.0f, 0.0f) * speed; // тот же *, но для вектора
bool reachedGoal = FVector::Dist(currentPos, goalPos) < 50.0f; // тот же <
Базовые + - * / % == && || и приоритет между ними остаются ровно теми же, что в этом уроке — Unreal лишь добавляет типы и функции поверх них. Подробности FMath и FVector — в Stage 07+.