
Полная версия
MINI DOOM современный С++ через игру том 1 учимся держать оружие
Строка 13 делает то же самое короче: health -= 15; значит «уменьши health на 15». Такие сокращения есть для всех операций: +=, -=, *=, /=. Вывод подтверждает: после стычки у нас 85 здоровья и 49 патронов.

Хочешь посмотреть на склад своими глазами? Оператор & перед именем переменной выдаёт её адрес, а sizeof — размер в байтах. Листинг 2.2 печатает и то, и другое; буква B в выводе — байты.

Листинг 2.2 — Где живут переменные и сколько места занимают
Адреса печатаются в шестнадцатеричной системе — поэтому там буквы. У тебя они будут другими, и даже у меня при каждом запуске меняются: операционная система специально раскладывает программу по памяти случайно, чтобы злоумышленникам было сложнее. А вот размеры будут те же: int занимает 4 байта, double — 8, char и bool — по одному. Как это выглядит на складе, показано на рисунке 2.1.

Рисунок 2.1 — Переменные из листинга 2.2 на складе памяти: имя, значение, адрес и размер
Какие бывают типы
Типов в C++ много, но для игры на первых порах хватит шести. Листинг 2.3 печатает их размеры и пределы. std::numeric_limits — это справочник стандартной библиотеки: спрашиваешь у него про тип, он отвечает, какое самое большое число туда влезает и сколько знаков точности он держит.

Листинг 2.3 — Размеры и пределы основных типов
Обрати внимание на int: максимум чуть больше двух миллиардов. Для здоровья и патронов — с огромным запасом. А вот float держит всего около шести значащих цифр, double — около пятнадцати. Поэтому для дробных чисел по умолчанию бери double, а float — только когда памяти жалко или так требует графика. Что где пригодится в игре, собрано в таблице 2.1.

Таблица 2.1 — Основные типы и где они пригодятся в MINI DOOM

Арифметика и подлость деления
Сложение, вычитание и умножение в C++ работают так, как ты ожидаешь. А вот деление приготовило сюрприз. Смотри листинг 2.4.

Листинг 2.4 — Целое деление и остаток
60 / 15 = 4 — пока всё честно. Но 60 / 7 дало 8, хотя на калькуляторе будет 8,57. Дело в том, что если оба числа целые, C++ делит нацело и просто отбрасывает дробную часть. Не округляет, а именно отрезает: даже 8,99 превратилось бы в 8.
Остаток от деления даёт оператор %: 60 % 7 = 4, потому что 7 × 8 = 56, и до 60 не хватает четырёх. Звучит как школьная скукотища, но в играх остаток нужен постоянно: перевести секунды в минуты, сделать анимацию, которая крутится по кругу, проверить чётность.
Стоит хотя бы одному числу стать дробным — 60.0 вместо 60, — и деление становится обычным: 8.57143. А последняя строка — классическая ловушка: 7 / 2 * 2 даёт не 7, а 6. Сначала 7 / 2 нацело = 3, потом 3 × 2 = 6. Половинка потерялась на первом шаге и больше не вернулась.

Преобразования: когда тип меняется
Иногда значение одного типа надо положить в коробку другого. Например, средний урон 9,75 превратить в целое число патронов. Компилятор такие переходы не любит, потому что дробная часть при этом теряется. Есть два способа создать переменную: со знаком = и с фигурными скобками. Начнём со скобок — листинг 2.5.

Листинг 2.5 — Фигурные скобки запрещают сужение
Запись int ammo{average}; — это инициализация в фигурных скобках. Она строже: если значение не влезает в тип без потерь, это ошибка сразу, по правилам языка, без всяких настроек. Компилятор называет это narrowing conversion — сужающее преобразование, когда из большей коробки пытаются пересыпать в меньшую.
А что со знаком =? В листинге 2.6 то же самое, но по-старому.

Листинг 2.6 — Со знаком = потерю дроби ловит строгий режим
По правилам языка это разрешено: дробь молча отрежется, и в ammo будет 9. Поймал это только наш строгий режим с предупреждениями, превращёнными в ошибки. Visual Studio выдаст тут предупреждение C4244, а с /WX — ошибку. Без строгих настроек такой код собрался бы, и ты потом неделю искал бы, куда пропадают патроны.
Когда отрезать дробь — именно то, что нужно, скажи об этом компилятору явно через static_cast. В листинге 2.7 заодно видна самая популярная ошибка с процентами.

Листинг 2.7 — Проценты убитых монстров и явное приведение типов
Первая строка вывода — 0%, хотя мы убили троих из четырнадцати. Почему так получилось, видно на рисунке 2.2: выражение считается слева направо, и 3 / 14 нацело — это ноль. Ноль умножить на сто — ноль. Если сначала умножить на 100, а потом делить, получится 21% — уже похоже на правду. А static_cast

Рисунок 2.2 — Порядок действий решает: деление нацело в начале убивает результат, в конце — только хвостик
Последние строки листинга показывают обратное превращение: static_cast
Константы и auto
Здоровье импа в Doom — 60 единиц, и оно не меняется посреди игры. Если в коде в двадцати местах написано просто 60, то, во-первых, непонятно, что это за число, а во-вторых, когда захочешь сделать импов покрепче, придётся найти и исправить все двадцать мест — и пропустить хотя бы одно. Такие числа программисты зовут магическими и не любят. Лекарство — в листинге 2.8.

Листинг 2.8 — Константы constexpr, const и автоматический тип auto
Строки 3–5. constexpr int kImpHealth = 60; создаёт константу, которая известна уже во время компиляции. Её нельзя изменить, а компилятор подставит значение прямо в машинный код. Букву k в начале имени я ставлю по традиции — сразу видно, что это константа.
Строка 8. auto ammo = 50; — компилятор сам выводит тип из значения. Пятьдесят — целое, значит, ammo будет int. auto экономит время, когда тип длинный и очевидный, но злоупотреблять не стоит: в коде должно быть понятно, что где лежит.
Строка 9. const тоже запрещает изменение, но значение может быть известно только при запуске программы. Половину патронов мы посчитали из переменной ammo, поэтому тут const, а не constexpr.
Строка 11. Попытка изменить константу. Сейчас строка закомментирована; раскомментируй — и компилятор откажется собирать программу, потому что менять константы нельзя. Это и есть их смысл: компилятор сторожит то, что ты сам объявил неизменным.

Когда числа ломаются
У каждой коробки есть дно и крышка. Что будет, если в int, где и так лежит самое большое возможное число, добавить единицу? Проверим в листинге 2.9. Этот пример я собрал с санитайзером — специальным сторожем, который следит за программой во время работы.

Листинг 2.9 — Переполнение int: санитайзер ловит его на месте
Первая строка напечаталась нормально. А на строке 7 санитайзер схватил программу за руку: знаковое целое переполнилось, 2147483647 + 1 не помещается в int. Программа остановилась прямо на месте преступления, и вторая строка так и не напечаталась.
А что было бы без сторожа? Соберём тот же код без санитайзера и запустим — листинг 2.10.

Листинг 2.10 — Без санитайзера переполнение проходит молча
Число перевалило через край и превратилось в самое маленькое отрицательное — как старый автомобильный одометр, который после 99999 показывает 00000. Только хуже: по стандарту C++ переполнение знакового целого — это неопределённое поведение. Компилятор вправе считать, что его никогда не бывает, и на этом основании выкидывать из программы проверки. Сегодня у тебя «заворачивается», завтра с другими настройками оптимизации происходит что-то совсем странное.

С дробными числами своя беда — они не точные. Листинг 2.11 проверяет, равно ли 0,1 + 0,2 трём десятым.

Листинг 2.11 — Дробные числа хранятся приблизительно
Ответ — false, не равно. Компьютер хранит дробные числа в двоичной системе, а там 0,1 — бесконечная дробь, как 1/3 в десятичной. Она обрезается, и при печати с семнадцатью знаками видно, что 0,1 + 0,2 чуть больше 0,3, а само 0,3 чуть меньше. Разница крохотная, но для == её достаточно. Здесь я печатал через std::format — о нём чуть ниже.


Красивый вывод: std::format
Помнишь задачу из главы 1, где точки для выравнивания пришлось считать руками? В C++20 для этого есть std::format из заголовка

Таблица 2.2 — Самые полезные шаблоны std::format
Строка формата проверяется ещё при компиляции: если в шаблоне три пары скобок, а значений два, программа не соберётся. Это одна из причин, почему в новом коде std::format лучше, чем старые способы.
Контрольная точка: калькулятор выстрелов
Соберём всё вместе. Пистолет в Doom наносит от 5 до 15 урона за выстрел, у импа 60 здоровья. Сколько выстрелов понадобится при большом везении, при полном невезении и в среднем? А сколько урона в среднем наносит дробовик, если в его патроне семь дробинок? Ответы — в листинге 2.12.

Листинг 2.12 — Калькулятор: сколько выстрелов нужно на импа
Строки 5–8 — константы с параметрами оружия и монстра. Никаких магических чисел в расчётах.
Строки 12–13 — трюк, который стоит запомнить. Нам нужно деление с округлением вверх: если до смерти импа не хватает хотя бы единицы урона, нужен ещё один выстрел. Целое деление округляет вниз, поэтому перед делением прибавляем делитель без единицы. При везении: (60 + 14) / 15 = 74 / 15 = 4 выстрела. При невезении: (60 + 4) / 5 = 64 / 5 = 12. Проверь на бумаге: если бы урона было 61, вышло бы (61 + 14) / 15 = 5 — как и должно быть.
Конец ознакомительного фрагмента.
Текст предоставлен ООО «Литрес».
Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.
Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.









