#17 Выделение и освобождение памяти.

Вступление

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

Теория

Прочитать что-нибудь в памяти это конечно хорошо, но гораздо лучше размещать в ней свои переменные. До этого мы пользовались статическими переменными - память для них выделялась как бы "по умолчанию". Однако можно создавать и динамические переменные - память для них мы должны выделять и освобождать самостоятельно. Вообще, честно говоря, память можно не освобождать, т.к. ОС после завершения работы программы затирает её сама. Но иногда случаются глюки и ..... вообщем мы будем всегда освобождать неиспользуемую память. Для выделения/освобождения память существует 2 пары процедур: New/Dispose и GetMem/FreeMem. Начнём с

procedure New(var P: Pointer)
Вызвав эту процедуру, передав ей в качестве параметра имя указателя (можно так же имя типизированного указателя, т.к. pointer вмещает его в себя). Так вот, вызвав эту процедуру, вы соответственно отведёте кусок памяти нужного размера (т.е. размера той переменной, на которую указывает указатель p). После вызова процедуры получаем следующее: p - адрес, p^ - переменная, того типа на который указывает p.

Например:

var
   pInt : ^integer;
   i : integer;
   
begin
   i := 1; 
   New (pInt);
   pInt^ := i;  
Вызвав New (pInt); мы выделяем два байта (именно столько занимает переменная типа integer). Теперь у нас как бы появляется новая целочисленная переменная pInt^.

Освободить, выделенную в ходе работы программы память можно процедурой:

procedure Dispose(var P: Pointer);
Завершим предыдущий пример таким образом:
   i := pInt^ * 2;
   Dispose (pInt)
end.
теперь посмотрим на это дело в отладчике. Добавим только ещё пару строк. Теперь наша программа будет выглядеть так:
type
   pinteger = ^integer
var
  pInt, pInt1 : pinteger ;
  i : integer;
begin
  i := 1;
  New (pInt);
  pInt^ := i;
  i := pInt^ * 2;
  Dispose (pInt);
  New (pInt1);
  pInt^ := i;
  Dispose (pInt1)
end.
Давайте добавим просмотр значений указателей pInt и pInt1 и значений по этим адресам (вместо имени переменной надо ввести pInt^ и pInt1^ соответственно). Входим в пошаговое исполнение, видим как pInt присваивается адрес (New (pInt)), потом по этому адресу меняется значение (pInt^ := i;), потом происходит вызов Dispose (pInt).... вроде бы ничего не поменялось... ладно смотрим дальше...После New (pInt1) pInt1 ссылается на тот же адрес, что и pInt !!! Поэтому pInt^ := i; изменит значение pInt1^ !!! Почему так происходит ?

Памятью заведует операционная система, именно она по нашему запросу решает дать или не дать. Когда мы вызываем New (pInt) ОС выделяет 2 байта и возвращает нам адрес этих байт. Вызывая же Dispose (pInt); мы "говорим" ОС, что эти 2 байта нам больше не нужны и она считает их свободными. Когда мы в следующий раз просим 2 байта (New (pInt1)), то она опять выделяет нам те же самые байты, поэтому происходит такая фишка, что pInt и pint1 указывают на один и тот же адрес! Поэтому, что бы не возникло логической ошибки (такую ошибку будет трудно поймать), можно после освобождения памяти присваивать неиспользуемым указателям значение nil и так же перед вызовом Dispose заполнить эту память 0.

Так же есть возможность использовать New, как функцию. В таком случае она возвращает указатель типа, имя которого является единственным параметром такой функции. Т.е. в предыдущем примере мы могли написать следующее:

pInt := New (pinteger);
  Это особенно  полезно  в случаях,  когда может потребоваться   присваивать указателю элементы различных типов.

Теперь обратим наш взгляд ко второй паре процедур.

procedure GetMem(var P: Pointer; Size: Word);
Как видите эта процедура отличается от New вторым параметром. Size - это точное число выделяемых байт. Т.е. под указатель на integer Size = 2, на byte Size = 1 и так далее. Естесственно, что для определения размера структур можно использовать функцию sizeof. Для тех, кто забыл я повторюсь:
function SizeOf(X): Integer;
возвращает размер аргумента Х в байтах.

Соответственно вторая процедура

procedure FreeMem(var P: Pointer; Size: Word);
Параметр Size теперь определяет число байт, которые надо освободить. Параметр Size у GetMem / FreeMem должен совпадать, иначе в памяти останутся байты, которые не используются, но и не считаются свободными. Т.е. происходит простое захламленее памяти. Или же если вы освободите больше байт, чем "заказали", то тогда вы можете освободить память другой переменной!

Кажется зачем такие трудности, когда используя New/Dispose можно не заботится об размере выделяемой памяти ? Однако давайте рассмотрим такой пример: считать из файла 1000 строк и сохранить их в массиве:

program read;

type
 pstring = ^string;

var
  buffer : string;
  data   : array [1 .. 1000] of pstring;
  f : text;
  i : integer;

begin
  assign (f, 'C:\autoexec.bat');
  reset (f);

  for i := 1 to 1000 do
  begin
    readLn (f, buffer);
    getMem (data[i], length (buffer));
    data[i]^ := buffer
  end;
  .....
Итак мы создаём массив указателей data. Каждый элемент такого массива - указатель на строку. Потом мы в цикле читаем из файла строки, при этом заносим строку во временную переменную buffer.
Теперь обратите внимание, как мы выделяем память getMem (data[i], length (buffer)); В качестве размера выделяемой памяти мы используем длинну считанной строки. Используя New, мы бы всегда выделяли бы 256 байт (размер string), однако если размер считанной строки меньше чем 256 байт, зачем нам тратить лишнюю память ?

А теперь маленький вопрос :) как вы видите в конце программы я поставил троеточие. Так вот вопрос: как в данном случае правильно освободить память ? Ответы как всегда шлите на мыло, внизу.

Ещё один момент, может быть он и очевиден, но всё же: для выделения/освобождения память надо использовать только парные функции. Т.е. вы не можете вызвать с New - FreeMem, а с GetMem нельзя использовать Dispose.


[Назад] [Содержание] [Дальше]
Hosted by uCoz