Динамическая память и динамические переменные
Поможем в ✍️ написании учебной работы
Поможем с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой

Память, отводимая под данные программы, делится на статическую, автоматическую и динамическую. Статическая память выделяется до начала работы программы под глобальные переменные и константы и освобождается только при завершении программы. Автоматическая память выделяется на программном стеке под локальные переменные при вызове подпрограммы, а после завершения подпрограммы автоматически освобождается. При этом статическая память инициализируется нулевыми значениями, а автоматическая – не инициализируется (это делается для ускорения вызова подпрограммы).

Поскольку как программный стек, так и область статической памяти, выделяются заранее в момент начала работы программы, статическая и автоматическая память имеют фиксированный размер. Однако во многих задачах в разные моменты работы программы требуется существенно различное количество памяти. Отводить для этого фиксированный максимально необходимый размер памяти – расточительство. С данной проблемой мы уже сталкивались при работе с массивами: при описании массива указывается его максимально возможный размер, текущая же заполненность массива, как правило, меньше его размера.

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

Процедуры New и Delete

Для выделения динамической памяти, контролируемой типизированным указателем, используется стандартная процедура New, для освобождения – стандартная процедура Dispose. Если pt – указатель на тип T, то вызов New(pt) распределяет в динамической памяти переменную типа T и записывает в pt адрес этой переменной:

Переменная, распределенная в динамической памяти, называется динамической переменной. Она не имеет своего имени и для доступа к ней используется разыменованный указатель pt^. После работы с динамической переменной занимаемая ею память должна быть освобождена вызовом стандартной процедуры Dispose, например: Dispose(pt). Таким образом, динамическая переменная существует между вызовами New и Dispose:

var pt: ^real;

Begin

New(pt);

pt^:=2.8;

pt^:=pt^*2;

...

Dispose(pt);

end.

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

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

function NewInteger(i: integer): pinteger;

Begin

New(Result);

Result^:=i;

end;

var pi: pinteger;

Begin

pi:= NewInteger(5);

...

При своем вызове функция NewInteger возвращает указатель на динамическую переменную, которая должна быть впоследствии освобождена. Основная проблема состоит в том, что NewInteger не является стандартной функцией, и при ее вызове можно забыть, что она выделяет динамическую память. Один из способов «напомнить» об этом программисту – дать функции имя, свидетельствующее о ее «создающей» способности. Например, имя такой функции может начинаться с префикса New или Create.

Пример. Массив указателей на переменные разных типов.

В некоторых задачах возникает необходимость хранить в массиве данные различных типов. Пусть в массиве требуется хранить данные типа integer, real и shortstring.

Приведем вначале решение, не использубщее указатели.

Решение 1. Используем записи с вариантами. Опишем следующие типы:

type TVar=(tInt,tReal,tStr);

Variant = record

  case t: TVar of

   tInt: (i: integer);

   tReal: (r: real);

   tStr: (s: shortstring);

end;

Теперь опишем массив записей Variant и добавим в него несколько значений:

var A: array [1..10] of Variant;

Begin

A[1].t:=tInt; A[1].i:=5;

A[2].t:=tReal; A[2].r:=3.14;

A[3].t:=tStr; A[3].s:='Delphi';

end.

Для вывода содержимого массива, очевидно, следует воспользоваться циклом

for i:=1 to 3 do

case A[i].t of

tInt: writeln(A[i].i);

tReal: writeln(A[i].r);

tStr: writeln(A[i].s);

end;

Такое решение имеет важный недостаток: каждый элемент массива имеет размер, определяемый самым большим типом shortstring, что расточительно.

Решение 2. В вариантной части записи Variant будем хранить не значения соответствующих типов, а указатели на них:

Type

TVar=(tInt,tReal,tStr);

pinteger=^integer;

preal=^integer;

pshortstring=^shortstring;

Variant = record

  t: TVar;

  case t: TVar of

   tInt: (pi: pinteger);

   tReal: (pr: preal);

   tStr: (ps: pshortstring);

end;

Будем добавлять в такой массив указатели на переменные разных типов:

var A: array [1..10] of Variant;

Begin

A[1].t:=tInt;  New(A[1].pi); A[1].pi^:=5;

A[2].t:=tReal; New(A[2].pr); A[2].pr^:=3.14;

A[3].t:=tStr; New(A[3].ps); A[3].ps^:='Delphi';

Для вывода содержимого такого массива воспользуемся следующим циклом:

for i:=1 to 3 do

case A[i].t of

tInt: writeln(pinteger(A[i].p)^);

tReal: writeln(preal(A[i].p)^);

tStr: writeln(pstring(A[i].p)^);

end;

В данном решении суммарный объем данных определяется не размером максимального типа данных, а реальным содержимым в момент выполнения программы. По окончании работы с массивом A динамическую память, занимаемую его элементами, следует освободить. Поскольку параметр процедуры Delete имеет тип pointer, то для освобождения занимаемой памяти можно передать любое из полей-указателей, например, pi:

for i:=1 to 3 do

Delete(A[i].pi);

Процедуры 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).

Дата: 2019-02-02, просмотров: 266.