Найти тему
ZDG

Пишем Сапёра на C + SDL #3: Какую память выделить?

Предыдущие части: Структура программы, Пишем Сапёра на С + 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:

-2

Исчезли инструкции .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. Выделять память под нужный размер, затем освобождать её

Если с пунктом 1 всё понятно, то пункт 2 можно опять же сделать двумя способами: на стеке и в куче. Чтобы создавать поле на стеке, мы могли бы запускать игровую партию внутри ещё одной функции, передавая туда размеры поля. Функция создавала бы поле заданного размера на стеке – при условии, что поле относительно небольшое. А при возврате (т.е. когда партия закончилась или игрок отказался продолжать) мы бы возвращались из функции и стек освобождался бы.

Мы также могли бы использовать malloc() для выделения памяти в куче и free() для освобождения, но это самый громоздкий метод.

Исходя из того, что игровое поле это основной объект игры, который не нужно без необходимости пересоздавать, и максимального размера, скажем, 100 на 100 клеток (с большим запасом), целесообразно будет выделить память статически (minesweeper.c):

-3

Итак, около 10 килобайт памяти зарезервировано статически. Ранее я говорил про двумерный массив, но память это просто память, поэтому двумерность будем обеспечивать самостоятельно.

В следующей части уделим больше внимания собственно игре, научимся заполнять поле и выведем его на экран.

Читайте дальше:

Читайте также:

Программирование: Что такое стек?
ZDG19 июня 2020

С подпиской рекламы не будет

Подключите Дзен Про за 159 ₽ в месяц