Предыдущие части: Структура программы, Пишем Сапёра на С + SDL
Код для этой части находится в ветке memory на github. Вы можете смотреть там все файлы онлайн и также скачать зип-архив всей ветки.
Итак, нам нужно разместить в памяти двумерный массив размером W*H для хранения ячеек поля, на котором сапёр ищет мины.
Есть три способа это сделать:
1. Статическое выделение
Не путать со статическими переменными и статическими методами в ООП. У них то же название, но другой смысл.
Данное выделение памяти называется статическим, потому что память резервируется при компиляции и становится частью самой программы. То есть когда скомпилированная программа загружается в память, в ней уже есть область, которая зарезервирована. Естественно, что такую память уже невозможно освободить, она статична и существует в течение всего времени жизни программы.
Наглядный пример:
Слева – код на C, где в глобальном пространстве (т.е. вне функций) объявлены две переменные: символьный массив "a" длиной 1000 элементов и символьный массив "b" длиной 5000 элементов. Справа – фрагмент ассемблерного кода, который получился после трансляции. Вам не нужно разбираться в ассемблере, чтобы его понять. В данный момент вы смотрите непосредственно на структуру памяти в компьютере. Можно видеть метки "main", "a" и "b". Инструкции функции main занимают какое-то количество памяти. Сразу после них расположена метка "b", где с помощью мета-инструкции .space зарезервировано 5000 байт, и после неё идёт метка "a", где таким же образом зарезервировано 1000 байт. Эта память уже принадлежит программе. (Да, "a" и "b" идут в обратном порядке, но это не наше дело, а транслятора.)
2. Автоматическое выделение (на стеке)
Теперь перенесём переменные "a" и "b" в функцию main:
Исчезли инструкции .space, память больше не резервируется статически. Теперь она выделяется в самой функции main, то есть только тогда, когда функция начнёт работать, то есть это произойдёт автоматически. Мы объявили три массива: "a", "b", "c", длиной 100, 200 и 300 байт.
Откуда же берётся память? Из стека. Стек – это уже заранее выделенная память (не нами, а операционной системой для программы). Он используется для вызовов функций и передачи параметров, а также для создания переменных. В нашем случае массивы "a", "b", "c" суммарно занимают 600 байт. Инструкция sub rsp, 656 вычитает из регистра-указателя стека (rsp) число 656. Таким образом, указатель стека сдвинулся на 656 байт, тем самым зарезервировав нужные нам 600 байт на стеке.
А почему именно 656, а не 600? Размеры 100, 200 и 300 округляются до границы 16 байт. Получаем общий размер 112 + 208 + 304 = 624. И ещё плюс 32 байта на что-то, я это не проверял, так как разбираться в логике транслятора не входит в текущие задачи. Итого 656.
Далее, через присваивания типа a[0] = 1 мы можем видеть, что начало массива "a" расположено по адресу -96[rbp], это смещение в -96 байт относительно адреса в регистре rbp, который сейчас также указывает на стек. Начало массива "b" расположено по адресу 16[rbp], а массива "c" по адресу 224[rbp]. Если вычесть соседние значения друг из друга, мы получим как раз округлённые длины массивов 112 и 208.
Самое главное:
- Стек имеет ограниченный размер, поэтому на нём нельзя выделить много памяти. В принципе, стек может автоматически наращиваться системой при нехватке памяти, но тогда создание переменных в нём будет генерировать большое количество лишних инструкций. В Windows cледует ориентироваться на 4 килобайта – это максимальный общий объём стека (включая и вложенные вызовы функций, и их параметры), который можно использовать без проблем.
- Стек предназначен для временного использования. Когда мы вызываем функцию, она размещает в стеке свои переменные, но когда мы возвращаемся из функции, указатель стека восстанавливается. Таким образом, стек используется повторно разными функциями, и переменные функции живут в нём только пока работает функция.
3. Куча
Кучей (heap) называется динамическая память, которая не принадлежит программе и стеку, а существует, скажем так, где-то в системе. Чтобы выделить в ней кусок, нужно воспользоваться специальной функцией:
malloc(100500)
Мы сейчас выделили 100500 байт в куче. Функция malloc() вернёт нам указатель на эту область памяти, и с указателем мы уже можем делать что хотим (записывать или читать данные любого размера с любым смещением относительно указателя).
Основное отличие от предыдущих двух типов памяти – возможность утечек.
- Статически зарезервированная память освобождается, когда завершается программа.
- Автоматически выделенная память на стеке автоматически же и освобождается, как только заканчивается функция.
- Динамически выделенная из кучи память должна быть освобождена с помощью соответствующей функции free(). Если этого не сделать, она так и останется выделенной, даже если вы перестали её использовать.
Представим такую ситуацию: функция вызывается много раз и каждый раз запрашивает память с помощью malloc(), не освобождая её. Это приведёт к тому, что работающая программа начнёт "жрать" всё больше и больше памяти и в конце концов с ней случится что-то нехорошее. Это называется утечкой памяти.
Теперь перейдём к проектированию игры.
Требования к игровому полю
Игрок может выбирать разный размер поля – маленький, средний, большой. Значит, мы можем пойти двумя путями:
- Заранее выделить память для самого большого размера, и использовать её для всех размеров, так как они все туда поместятся
- Выделять память под нужный размер, затем освобождать её
Если с пунктом 1 всё понятно, то пункт 2 можно опять же сделать двумя способами: на стеке и в куче. Чтобы создавать поле на стеке, мы могли бы запускать игровую партию внутри ещё одной функции, передавая туда размеры поля. Функция создавала бы поле заданного размера на стеке – при условии, что поле относительно небольшое. А при возврате (т.е. когда партия закончилась или игрок отказался продолжать) мы бы возвращались из функции и стек освобождался бы.
Мы также могли бы использовать malloc() для выделения памяти в куче и free() для освобождения, но это самый громоздкий метод.
Исходя из того, что игровое поле это основной объект игры, который не нужно без необходимости пересоздавать, и максимального размера, скажем, 100 на 100 клеток (с большим запасом), целесообразно будет выделить память статически (minesweeper.c):
Итак, около 10 килобайт памяти зарезервировано статически. Ранее я говорил про двумерный массив, но память это просто память, поэтому двумерность будем обеспечивать самостоятельно.
В следующей части уделим больше внимания собственно игре, научимся заполнять поле и выведем его на экран.
Читайте дальше:
Читайте также: