logo
Языки программирования

8.3. Распределение памяти

При выполнении программы память используется для хранения как программ (кода), так и различных структур данных, например стека. Хотя рас­пределение и освобождение памяти правильнее обсуждать в контексте ком­пиляторов и операционных систем, вполне уместно сделать обзор этой темы здесь, потому что реализация может существенно повлиять на выбор конст­рукций языка и стиля программирования.

Существует пять типов памяти, которые должны быть выделены.

Код. Машинные команды, которые являются результатом компиляции программы.

Константы. Небольшие константы, такие как 2 и 'х', часто могут содер­жаться внутри команды, но для больших констант память должна выде­ляться особо, в частности для констант с плавающей точкой и строк.

Стек. Стековая память используется в основном для записей активации, которые содержат параметры, переменные и ссылки. Она также исполь­зуется для временных переменных при вычислении выражений.

Статические данные. Это переменные, объявленные в главной программе и в других местах: в Ada — данные, объявленные непосредственно внутри библиотечных пакетов; в С — данные, объявленные непосредственно внутри файла или объявленные как статические (static) в блоке.

Динамическая область. Динамическая область (куча — heap) — термин, ис­пользуемый для области данных, из которой данные динамически выде­ляются командой malloc в С и new в Ada и C++.

Код и константы похожи тем, что они определяются во время компиляции и уже не изменяются. Поэтому в дальнейшем обсуждении мы объединим эти два типа памяти вместе. Обратите внимание, что, если система это поддержи­вает, код и константы могут храниться в памяти, доступной только для чтения (ROM). Стек обсуждался подробно в разделе 7.6.

Мы упомянули, что статические (глобальные) данные можно считать рас­пределенными в начале стека. Однако статические данные обычно распреде­ляются независимо. Например, в Intel 8086 каждая область данных (назы­ваемая сегментом) ограничена 64 Кбайтами. Поэтому есть смысл выделять от­дельный сегмент для стека помимо одного или нескольких сегментов для ста­тических данных.

И наконец, мы должны выделить память для кучи. Динамическая область отличается от стека тем, что выделение и освобождение памяти может быть очень хаотичным. Исполняющая система должна применять сложные алго­ритмы, чтобы гарантировать оптимальное использование динамической об­ласти.

Программа обычно помещается в отдельную, непрерывную область. Па­мять должна быть разделена так, чтобы разместить требуемые области памя­ти. На рисунке 8.5 показано, как это реализуется. Поскольку области кода, констант и статических данных имеют фиксированные размеры, они распределяются в начале памяти. Две области переменной длины, куча и стек поме­щаются в противоположные концы остающейся памяти.

При таком способе, если программа использует большой стек во время од­ной фазы вычисления и большую кучу во время другой фазы, то меньше шан­сов, что памяти окажется недостаточно.

Важно понять, что каждое выделение памяти в стеке или в куче (то есть каждый вызов процедуры и каждое выполнение программы выделения памя­ти) может закончиться неудачей из-за недостатка памяти. Тщательно разра­ботанная программа должна уметь восстанавливаться при недостатке памяти, но такую ситуацию нелегко обработать, потому что процедуре, которая выполняет восстановление, может понадобиться еще больший объем памяти! Поэтому желательно получать сигнал о недостатке памяти, когда еще остает­ся значительный резерв.

Запрос и освобождение памяти

В процедурных языках программирования есть явные выражения или опера­торы запроса и освобождения памяти. Язык С использует malloc, функцию весьма опасную, поскольку в ней никак не проверяется соответствие выде­ленного объема памяти размеру указуемого объекта. Следует использовать функцию sizeof, даже когда это явно не требуется:

C

int*p = (int*)malloc(1); /* Ошибка */

int *p = (int *) malloc(sizeof(int)); /* Этот вариант лучше */

Обратите внимание, что malloc возвращает нетипизированный указатель, ко­торый должен быть явно преобразован к требуемому типу.

При освобождении памяти задавать размер блока не нужно:

free(p);

Выделенный блок памяти включает несколько дополнительных слов, кото­рые используются для хранения размера блока." Этот размер используется в алгоритмах управления динамической областью, как описано ниже.

Языки C++ и Ada используют нотацию, из которой ясно видно, что созда­ется указуемый объект конкретного типа. При этом нет опасности несовме­стимости типа и размера объекта:

typedef Node *Node_Ptr;

Node_Ptr *p = new Node; // C++

type Node_Ptr is access Node;

P: Node_Ptr := new Node; --Ada

Оператор delete освобождает память в C++. Ada предпочитает, чтобы вы не освобождали память, выделенную в куче, потому что освобождение памяти опасно по существу (см. ниже). Конечно, на практике без освобождения не обойтись, поэтому применяемый метод назван освобождением без контроля (unchecked deallocation), и назван он так для напоминания, что его использова­ние опасно. Обратите внимание, что освобождаемая память — это область хранения указуемого объекта (на который ссылается указатель), а не самого указателя.

Повисшие ссылки

Серьезная опасность, связанная с указателями, — это возможность создания повисших ссылок (danglingpointers) при освобождении блока памяти:

C++

int *ptr1 = new int; int *ptr2;

ptr2 = ptrl; // Оба указывают на один и тот же блок

result = delete ptrl; // ptr2 теперь указывает на освобожденный блок

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

Повисшие ссылки могут возникать также в С и C++ без какого-либо явно­го участия программиста в освобождении памяти:

C

char *proc(int i) /* Возвращает указатель на тип char */

{

char с; /* Локальная переменная */

return &c; /* Указатель на локальную переменную типа

char */

}

Память для с неявно выделяется в стеке при вызыве процедуры и неявно ос­вобождается после возврата из процедуры, поэтому возвращенное значение указателя больше не ссылается на допустимый объект. Это легко увидеть в процедуре из двух строк, но, возможно, не так легко заметить в большой про­грамме.

Ada пытается избежать повисших ссылок.

• Указатели на объекты (именованные переменные, константы и парамет­ры) запрещены в Ada 83; в Ada 95 они вводятся специальной конструк­цией alias, правила которой предотвращают возникновение повисших ссылок.

• Явного выделения памяти избежать нельзя, поэтому применяемый метод назван Unchecked Deallocation (освобождение без контроля) с целью предупредить программиста об опасности.

Yandex.RTB R-A-252273-3
Yandex.RTB R-A-252273-4