Методические указания для студентов 1 курса факультета математики, механики и компьютерных наук

Вид материалаМетодические указания

Содержание


1.6Процедуры GetMem и FreeMem
1.7Ошибки при работе с динамической памятью
Подобный материал:
1   2   3   4   5   6   7   8

1.6Процедуры GetMem и FreeMem


Для выделения/освобождения динамической памяти, контролируемой бестиповым указателем, используется другая пара процедур: GetMem и FreeMem. Если p – указатель любого типа (в частности, типа pointer), то вызов GetMem(p,nb) выделяет в динамической памяти участок размера nb байтов и записывает адрес его начала в указатель p. Вызов FreeMem(p) освобождает динамическую память, контролируемую указателем p. Следует обратить внимание, что при вызове FreeMem не указывается размер освобождаемой памяти, поскольку в каждом выделенном блоке хранится его размер, и FreeMem пользуется этой информацией.

В большинстве ситуаций использования типизированных указателей и процедур New и Dispose оказывается достаточно. Процедуры GetMem и FreeMem применяются там, где требуется более гибкое управление памятью.

Пример. Динамический массив.

Динамическим будем называть массив, размер которого задается в процессе работы программы. В Delphi (начиная с версии 4) динамические массивы реализованы средствами языка:

var dyn: array of integer;

n: integer;

begin

read(n);

Assert(n>0);

SetLength(dyn,n);

dyn[0]:=5;

...

Однако, динамические массивы нетрудно создать и с помощью обычных массивов с помощью процедур GetMem и FreeMem:

const sz=MaxInt div sizeof(integer);

type Arr: array [0..sz-1] of integer;

var dyn: Arr;

n: integer;

begin

read(n);

Assert(n>0);

GetMem(dyn,n*sizeof(integer));

dyn[0]:=5; // можно dyn[0]:=5

...

Идея подобной реализации динамического массива состоит в следующем. Описывается тип массива с большим количеством элементов и переменная dyn, являющаяся указателем на этот тип. С помощью GetMem выделяется нужное количество памяти, определяемое в процессе работы программы; адрес выделенной памяти записывается в переменную dyn. С этого момента можно обращаться к элементам массива, используя запись вида dyn[0]. Операцию разыменования в Delphi можно опускать, поэтому с dyn можно обращаться как с обычным массивом: dyn[0]. В конце работы с таким массивом следует вызвать FreeMem(dyn).

Отметим, что при включенном режиме проверки выхода за границы диапазона {$R+} нельзя выделять под массив память, превосходящую его размер, то есть должно выполняться условие n<=sz. Поэтому следует задавать размер массива sz максимально возможным. В Delphi память, занимаемая переменной любого типа, не должна превосходить 2 Гб, т.е. MaxInt байт. Поскольку элементы массива имеют тип integer, то в качестве sz выбрано максимально возможное значение MaxInt div sizeof(integer).

1.7Ошибки при работе с динамической памятью


Как было отмечено, при работе с динамической памятью можно совершить большое количество ошибок, которые имеют различные последствия и различную степень тяжести. Большинство этих ошибок проявляется не сразу, а через некоторое время в процессе выполнения программы. Следовательно, такие ошибки труднонаходимы и потому особенно опасны. Используя принцип «предупрежден – значит, вооружен», перечислим наиболее часто встречающиеся варианты ошибок при работе с динамической памятью.

1. Попытка воспользоваться неинициализированным указателем.

var pi: integer;

    i: integer;

begin

  pi:=5;

Если pi – глобальная переменная, то она автоматически инициализируется нулевым значением, т.е. имеет значение nil. Разыменование нулевого указателя приводит к ошибке времени выполнения. Если pi – локальная переменная, то она по умолчанию не инициализируется, а поэтому содержит непредсказуемое значение. Это значение трактуется как адрес целой переменной, к которой осуществляется доступ. Как правило, в этой ситуации возникает исключение Access Violation (нарушение защиты доступа), но по чистой случайности может оказаться, что указатель pi содержит истинный адрес переменной программы, тогда переменная будет изменена, выполнение программы продолжится дальше, а факт изменения переменной непредсказуемым образом повлияет на дальнейшее выполнение программы.


2. «Висячие» указатели.

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

var pi: integer;

begin

  New(pi);

  pi:=5;

  Dispose(pi); // указатель становится "висячим"

  ...

  pi:=6; // ошибка!

Если после Dispose(pi) сразу написать pi:=nil, то в дальнейшем при попытке разыменовать нулевой указатель pi возникнет исключение, что является более предпочтительным, чем скрытая ошибка изменения другой переменной. Данный прием следует взять на вооружение и после освобождения динамической переменной обнулять указатель:

Dispose(pi);

pi:=nil;

3. «Утечка» памяти.

Данная ошибка возникает, когда память не освобождается, но перестает контролироваться указателем. Подобную ошибку называют «утечкой» памяти, поскольку такую память невозможно освободить. Такая ошибка труднонаходима, поскольку практически не сказывается на работе приложения. Однако при систематических утечках программа требует все больше памяти у операционной системы, замедляя работу других приложений. Далее приводятся две распространенные ситуации, в которых возникает утечка памяти.

Пример 1. Повторное выделение памяти.

Если выделить память повторно для того же указателя, то ранее выделенная память «утечет»:

var pi: integer;

begin

  New(pi);

  pi:=5;

  New(pi);


Пример 2. Выделение памяти под локальную переменную без освобождения.

procedure pp;

var pi: integer;

begin

  New(pi);

end;

Данная процедура составлена ошибочно: локальный указатель pi уничтожается после завершения работы процедуры, поэтому контролируемая им динамическая память «утекает». Особенно опасна подобная утечка при вызове такой процедуры в цикле:

for i:=1 to MaxInt do

  pp;

4. Память, выделенная динамически для глобальных переменных-указателей, не возвращается явно в конце программы.

Поскольку динамическая память автоматически освобождается в конце работы программы, отсутствие явного вызова Dispose или FreeMem для глобальных переменных-указателей на динамическую память не может считаться ошибкой и свидетельствует просто о неаккуратности программирования. Однако при переносе текста основной программы в процедуру мы получим ошибку утечки памяти, описанную в предыдущем пункте.

5. Попытка освободить динамическую память, не выделенную ранее.

var pi: integer;

begin

  Dispose(pi);

Подобная ошибка приводит к немедленной генерации исключения, поэтому не принадлежит к числу опасных. Заметим, что в Delphi вызов процедуры Dispose для нулевого указателя просто игнорируется, не приводя к генерации исключения.

6. Попытка дважды освободить занимаемую память.

var pi: integer;

begin

  New(pi);

  ...

  Dispose(pi);

  Dispose(pi);

При повторном вызове процедуры Dispose будет сгенерировано исключение.

7. Попытка освободить нединамическую память.

var pi: integer;

    i: integer;

begin

  pi:=@i;

  Dispose(pi);

При вызове Dispose для нединамической переменной будет сгенерировано исключение.

8. Выход за память, выделенную процедурой GetMem.

Поскольку GetMem выделяет количество памяти, не зависящее от размера объекта, с которым связан указатель, она менее безопасна, чем New. Например, при реализации с помощью GetMem динамического массива можно предпринять попытку обратиться за границы выделенной памяти:

GetMem(dyn,n*sizeof(integer));

dyn[n+1]:=5;

Обычно такая ошибка приводит к исключению Access Violation.