http://sulfurzona.ru/
News
Service
Magazine
Software (Battle City Game, Wallpaper manager, Superpad, VG-NOW, Puzzle Game, Netler Internet Browser, ..)
Wing-Thunder Game (fly simulator)
Dune Game (Dune III, Dune IV, Cheats, Forum, ..)
Games free
Turbo Pascal (Assembler, Docs, Sources, Debbugers, ..)
Books (Docs for developers)
Guest book
Компьютерная диагностика двигателя автомобиля (адаптер К-линии)Компьютерная диагностика двигателя автомобиля (адаптер К-линии)
 
 
Скачать игру Крыло-Гром (Wing-Thunder) бесплатно
 
 

Паскаль для новичков (часть 23)

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

Бестиповые указатели

 
Turbo Pascal помимо типизированных указателей позволяет при помощи идентификатора pointer объявлять нетипизированные указатели, совместимые со всеми ссылочными типами. При помощи примера
 
type
       TPL = ^longint;
       TPS = ^single;
var
      PL : TPL;
      PS : TPS;
      P : pointer;
      L : longint;
      S : single;
 
begin
{допустимы присваивания}
PL := @L;
PS := @S;
P := PS;
P := PL;
{вызовет ошибку}
PL := PS;
end.
 
легко убедиться, что нетипизированному указателю P легко может быть присвоено значение любого типизированного указателя, а вот
последнее присваивание вызовет ошибку “Error 26: Type mismatch.” (базовые типы не соответствуют).
 
Таким образом, нетипизированный указатель совместим с любым ссылочным типом, но так как он не имеет базового типа, то для обращения к области памяти по такому указателю, его необходимо преобразовать в типизированный указатель:
 
TPL( P )^ := TPL( P )^ + 10;
Inc( TPL( P )^ );
 
Теперь мы разобрались со всеми ссылочными типами. Но как же тогда выделять и освобождать память процедурами New и Dispose, используя нетипизированный указатель? Ведь процедура New должна как то определить размер памяти, которую следует выделить под динамическую переменную.
 
Спокойствие, только спокойствие! В конце концов, ведь это дело житейское, и значит не стоит так переживать из-за пустяков. Тем более, что решение этой проблемы пустяшное, да и проблема тоже надумана.
 
Итак, для выделения памяти под динамические переменные, без учета типа значений, которые будут храниться в данной области, в Turbo Pascal имеются две процедуры сестрички, и зовут их почти как в том мультфильме “Лелик и Болик” соответственно GetMem(var p : pointer; size : word) и FreeMem(var p : pointer; size : word). Первая выделяет область памяти размером Size байт (не должен превышать 65520 байт) и помещает ее адрес в указатель любого ссылочного типа (типизированного и нетипизированного). При этом, если запрашиваемый размер будет больше, чем имеется свободной памяти в куче, то произойдет все та же ошибка “Error 203: Heap overflow error.” (куча переполнена, исчерпана), поэтому актуальность использования функции MaxAvail сохраняется. Процедура FreeMem предназначена для освобождения памяти, выделенной только процедурой GetMem, при этом следует освобождать столько же байт, сколько было выделено, и по той же самой ссылке, иначе могут быть “бедствия с последствиями” в масштабах программы. Пример:
 
begin
If MaxAvail < SizeOf( longint ) then begin
    Writeln( ‘Error: Not enough memory.’ );
    Halt;
    end;
GetMem( PL, SizeOf( longint ) );
PL^ := 0;
PL^ := PL^ + 5;
Writeln( ‘PL^= ’, PL^ );
FreeMem( PL, SizeOf( longint ) );
end.
 

Управление кучей

 
При запуске Pascal-программы под кучу, по умолчанию, отводится вся свободная оперативная память, доступная операционной системе. При этом не следует забывать о всевозможных загруженных драйверах, резидентах, а так же сегменте данных и стеке самой программы, на суммарный объем которых свободная область оперативной памяти станет меньше. В итоге, размер кучи при благоприятных условиях может достигать 450К, если конечно краткость является родной сестрой вашего таланта, и обеспечит минимальный размер EXE-файла исполняемой программы. При этом не всегда может понадобится такой большой объем кучи, или на ряду с использованием динамической памяти бывает необходимо иметь возможность запускать внешние программы из Pascal-программы.
 
Поэтому в Turbo Pascal имеется директива {$M StackSize, HeapMin, HeapMax}, где параметр StackSize – задает размер сегмента стека, который может варьироваться в пределах 1024..65520 байт. HeapMin – задает минимальный размер кучи, который необходим для запуска программы, диапазон может быть 0..655360. Если свободной памяти окажется меньше, чем значение данного параметра, то программа вообще не запустится, а при параметре 0 программа будет запускаться всегда. HeapMax – задает максимальный, но скорее рекомендательный объем кучи (HeapMin..655360), который будет учитываться в ситуации, когда свободной памяти оказалось больше, чем HeapMin, и тогда размер кучи не будет превышать HeapMax.
 
Директива $M должна располагаться в самом начале программы, хотя если разместить еще парочку таких директив в тексте программы, то во внимание будет принята самая последняя.
 
Стандартная процедура Mark(var p : pointer) позволяет сохранить в указатель вершину кучи, на которую указывает системная переменная HeapPtr. Данная процедура может пригодиться, например, в следующей ситуации. Допустим, в программе начиная с некоторого момента происходит выделение памяти под несколько буферов (динамических переменных) при помощи вызовов New или GetMem, и адрес каждого заносится в соответствующий указатель. Когда же приходит время освободить память, отведенную под эти буферы, придется составить немало строк с вызовом процедуры Dispose или FreeMem для каждого указателя этих буферов. Если таких указателей пара тройка, то еще ладно, но когда их десятки, то проще освободить их разом вызвав стандартную процедуру Release(var p : pointer). Таким образом вершина кучи будет восстановлена. Пример:
 
var LastHeapPtr, P1, P2, P3 : pointer;
begin
Writeln( MemAvail );
GetMem( P1, 65520 );
Mark( LastHeapPtr );
Writeln( MemAvail );
GetMem( P2, 65520 );
GetMem( P3, 65520 );
Writeln( MemAvail );
Release( LastHeapPtr );
Writeln( MemAvail );
end.
 
содержит строку Release(LastHeapPtr), которая эквивалентна строкам:
 
FreeMem(P2,65520);
FreeMem(P3,65520);
 
Как я уже сказал, указатель по своей структуре напоминает запись с двумя полями смещение и сегмент, но обращаться к этим частям как к полям нельзя. Зато в системе Turbo Pascal предусмотрены стандартные операции извлечения (чтения) значения номера сегмента Seg( X ) : word и величины смещения Ofs( X ) : word  из указателя.
 
Если к предыдущим примерам добавить следующее описание
 
type
       TRec = record
         Lo, Hi : word;
       end;
var
       Rec : TRec absolute PL;
 
то при выполнении данной конструкции
 
Writeln( 'PL = ', Seg( PL^ ), ':', Ofs( PL^ ) );
Writeln( 'Rec = ', Rec.Hi, ':', Rec.Lo );
Writeln( '@PL = ', Seg( PL ),':', Ofs( PL ));
 
легко убедиться, что использовать функции Seg и Ofs так же легко, как интерпретировать ссылочную переменную как запись с двумя полями.
  
Исходя из того, что данные функции возвращают сегмент и смещение любой переменной, указанной в качестве параметра, то следует заметить, что в конструкциях Seg(PL^) и Ofs(PL^) указан разыменованный указатель PL, чтобы получить сегмент и смещение адреса области памяти, на которую указывает PL. Если же применить соответственно конструкции Seg(PL) и Ofs(PL), то будут получены сегмент и смещение адреса самой переменной PL, а не области памяти, на которую она указывает.
 
Извлекать сегмент и смещение из указателя мы научились. А что если необходимо осуществить обратное действие, то есть сформировать указатель на некоторую область в памяти? Для этого существует стандартная функция Ptr( Seg, Ofs : word ) : pointer, которая объединяет оба параметра сегмент и смещение в единое значение нетипизированного указателя. Если к выше указанным примерам добавить
 
var
     hh, mm, ss : word;
 
то следующий код сформирует адрес поля хранения счетчика системных часов, находящийся в области данных BIOS, и поместит указатель в ссылочную переменную PL. Затем, до тех пор, пока не будет нажата клавиша “Any key” ;o) , в теле цикла будет происходить считывание системных часов и преобразование их в формат HH:MM:SS,MS, то бишь “ЧАСЫ:МИНУТЫ:СЕКУНДЫ.МИЛЛИСЕКУНДЫ”:
 
Uses CRT;
PL := Ptr($40, $6C);
repeat
 {количество миллисекунд}
 s := PL^ * ( 1000 / 18.2 );
 {количество часов}
 hh := trunc( ( s / 1000 ) / 3600 );
 s := s – longint( hh ) * 3600000;
 {количество минут}
 mm := trunc( ( s / 1000) / 60 );
 s := s – longint( mm ) * 60000;
 {количество секунд}
 ss := trunc( s / 1000 );
 {остаток миллисекунд}
 s := s – ss * 1000;
 Writeln('Time: ',hh, ':',mm, ':',ss, '.',s:3:0);
until keypressed;
end.
 
Ну, а у кого клавиши “Any key” на клавиатуре не нашлось, нам осташтся тому лишь посочувствовать ;o)
 
Ранее, в моих статьях уже упоминалась операция взятия адреса, где символ ‘@’ ставился перед идентификатором переменной или подпрограммы и такая конструкция возвращала указатель (адрес) на соответствующую переменную или подпрограмму. В Turbo Pascal имеется эквивалентная функция Addr( X ) : pointer. Следующий пример демонстрирует это
 
P := Addr( L );
Writeln('P = ', seg(TPL(P)^), ':', ofs(TPL(P)^));
P := @L;
Writeln('P = ', seg(TPL(P)^), ':', ofs(TPL(P)^));
Writeln('@L = ', seg( L ), ':', ofs( L ) );
 
и все три оператора Writeln дадут одинаковый результат.
 

Ошибки в работе с указателями

 
При работе с указателями следует быть очень внимательным, так как ошибки в работе с указателями могут приводить к трудно объяснимому поведению программы, а то и просто зависанию машины. Например:
 
type
        TArr = array [0..10000] of byte;
        PArr = ^TArr;
 
procedure CalcArr;
var Arr : PArr;
      j : integer;
begin
GetMem( Arr, SizeOf( TArr ) );
for j := 0 to 10000 do begin
     Arr^[ j ] := random( 256 );
     end;
end;
 
begin
writeln( MemAvail );
CalcArr;
writeln( MemAvail );
end.
 
в процедуре CalcArr выделяется память под массив и адрес заносится в локальную переменную-указатель Arr, затем с массивом выполняются некоторые действия и процедура завершается. Но перед завершением процедуры не выполнена команда FreeMem(Arr,SizeOf(TArr)), и в итоге на экране будет выведена информация, красноречиво подтверждающая, что объем свободной памяти уменьшился ровно на размер массива, то есть память, отведенная под массив осталась не освобожденной, да и к тому же ссылка на не освобожденную область безвозвратно потеряна вместе с содержимым локальной переменной Arr.
 
Хотя, если в блок описания глобальных переменных добавить
 
var LastHeapPtr : pointer;
 
а основной блок программы заменить на следующие строки:
 
begin
writeln( MemAvail );
Mark( LastHeapPtr );
CalcArr;
writeln( MemAvail );
Release( LastHeapPtr );
writeln( MemAvail );
end.
 
то тогда память будет корректно освобождена даже при потерянной ссылке.
  
Следует отметить, что память нужно освобождать исходя из того, что в дальнейшей работе программы она вновь может понадобиться. Но освобождать всю выделенную память перед самым завершением программы не имеет смысла, так как системный модуль SYSTEM, который обеспечил существование кучи при запуске Pascal-программы, так же корректно освободит кучу перед завершением оной, и DOS память будет освобождена. Но хороший стиль программирования всегда приветствуется.
 
Выше была рассмотрена проблема потерянной ссылки. Теперь ситуация другого сорта. Следующий пример демонстрирует проблему устаревшей ссылки, когда ссылка указывает на освобожденную область памяти:
 
{$N+}
type TPL = ^longint;
var   PL : TPL;
 
procedure Proc1;
var A : longint;
begin
A := 2222;
PL := Addr( A );
end;
 
procedure Proc2;
var B : longint;
begin
B := 5555;
end;
 
procedure Proc3;
var C : single;
begin
C := 1.0;
end;
 
begin
Proc1;
writeln( PL^ );
Proc2;
writeln( PL^ );
Proc3;
writeln( PL^ );
end.
 
В процедуре Proc1 в типизированный указатель PL заносится адрес локальной переменной типа longint, то есть адрес фрагмента памяти стека. Затем вызывается процедура Proc2, в которой по роковой случайности совершенно одинаково описана локальная переменная типа longint. В итоге разыменованный указатель PL^ уже возвращает другой результат. И на последок выполняется процедура Proc3, в которой описана локальная переменная типа single, и размещенная на том самом месте в стеке, куда указывает ссылка PL. Но значение этого фрагмента памяти интерпретируется как значение типа longint, поэтому будет получен третий, еще более умопомрачительный результат.
 
И на последок хочу предостеречь от очень распространенной ошибки среди новичков. При вызове процедур, которые принимают фактический параметр как бестиповый, новички часто указывают ссылку на динамическую переменную, забывая разыменовать ее. Поэтому при использовании таких процедур как Move, BlockRead, BlockWrite следует четко осознавать свои действия.
 
Продолжение следует…
 
© Владислав Демьянишин
 
На нашем сайте можно не только бесплатно скачать игры, но и документацию и книги по программированию на MIDLetPascal, Turbo Pascal 6, Turbo Pascal 7, Borland Pascal, по программированию устройств Sound Blaster, Adlib, VESA BIOS, справочник Norton Guide и много другой полезной информации для программистов, включая примеры решения реальных задач по созданию резидентных программ.
 

Журнал > Программирование > Паскаль для новичков (Turbo Pascal, Assembler) > Паскаль для новичков (часть 23): Бестиповые указатели. Управление кучей. Ошибки в работе с указателями
 
 
 
 
 
 
На главную страницу На предыдущую страницу На начало страницы