Главная » Delphi » На каждую хитрую гайку… или нетипизированные указатели, как способ организации массивов

0

Иметь в Pascal произвольный доступ к массиву байтов по указателю любого вида путем простых арифметических операций с ним, как и в языке С, все же можно, правда, несколько "кривым" путем. Мы этот способ использовали в главе 14, когда перебирали символы в массиве, считанном из дискового файла. Для этого нужно просто приводить указатель к численном виду integer, longint (в 32-разрядном Pascal это совершенно идентичные типы) или dword, а потом обратно, путем непосредственного преобразования типов:

pb:=pointer(integer(pFile)+1);

После выполнения такого оператора в нетипизированном указателе рь окажется ссылка на второй байт, считая от pFile. Разумеется, ответственность за то, чтобы не залезть при таких операциях на чужую делянку в памяти, целиком ложится на программиста, как, впрочем, и во всех подобных случаях организации индексированного доступа к элементам любого массива (строки, обычного массива, динамического массива, объекта-потомка TstringList и т. п.). Поэтому, для того чтобы ваша программа не демонстрировала пользователю синие экраны в стиле "Гибель Помпеи", размеры выделенной памяти нужно своевременно корректировать. Менеджер памяти Delphi (он тут свой, в обход менеджера памяти Windows) предполагает, что программа часто выделяет и освобождает блоки разного размера, но постепенное увеличение одного и того же блока может вызвать разные неприятные последствия, типа утечки памяти неизвестно куда (а на самом деле — вследствие фрагментации кучи). Но самым главным недостатком механизма постепенного увеличения размера однажды выделенного блока памяти является снижение производительности программы по мере увеличения размеров блока. Происхождение этого явления понятно: при выделении блока процедурами типа GetMem память выделяется линейно, т. е. весь выделенный кусок занимает непрерывную последовательность адресов (иначе бы описанная ранее манипуляция с указателями-числами была бы невозможна). Потому если вы увеличиваете размер текущего блока памяти, и при этом места для расширения не хватает, то выделяется новый блок памяти, куда переносится содержимое старого (см. также описание особенностей работы процедур delete и insert: в главе 14). Естественно, что данный процесс сильно замедляет работу. Особенно глупо увеличивать память мелкими блоками — многоступенчатый механизм выделения памяти устроен так, что выделяется (но не всегда используется) все равно больше, чем надо.

Ну, и как же физически все это делается в Delphi? Первоначально память выделяется с помощью GetMem по нетипизированному указателю типа pointer, а последовательное увеличение блока может осуществляться с помощью редко упоминаемой в учебниках процедуры ReaiiocMem (аналога reaiioc в С), которая вызывается точно так же, как и GetMem:

ReaiiocMem(var Р : Pointer; NewSize: Integer);

Здесь p — тот же самый указатель, который был уже использован при вызове GetMem, a NewSize— новый размер блока. Причем если указатель р не был инициализирован ранее, то ReaiiocMem просто сработает, как GetMem. Освобождать через FreeMem, естественно, следует количество памяти, которое соответствует последнему вызову ReaiiocMem.

Однако по причинам, указанным ранее, часто использовать ReaiiocMem нежелательно. Гораздо лучше заранее зарезервировать блок памяти побольше и в Windows в этом ничего страшного в принципе нет (в отличие от DOS, где все время приходилось следить за тем, чтобы не вылезти за пределы свободной памяти) — если вы даже вызовете GetMem с просьбой выделить 300 Мбайт при имеющемся ОЗУ в 256 Мбайт, то программа не рухнет и остальные программы не прекратят работу. Но, по мере того, как память эта будет реально заполняться, все операции, разумеется, будут совершаться через SWAP-файл, и система будет становиться все более "задумчивой". Практически такие операции следует организовывать так: следует ограничить массив в памяти размером в несколько мегабайт, а при его превышении сбрасывать избыток в дисковый файл самостоятельно, не перекладывая это на Windows. Как это лучше делать на практике, причем сохраняя доступ ко всему массиву, который к тому же непрерывно пополняется, мы рассмотрим далее.

Через механизм нетипизированных указателей можно в принципе получить массив любых типов переменных, необязательно байтов. Вот как можно решить задачу расположения в памяти массива вещественных чисел типа double и последующего чтения этого массива. Я, как и обещал ранее, не располагаю соответствующий проект на диске — он очень прост. Поместите на форму в новом проекте кнопку Button и компонент Memo. При создании формы мы будем записывать в массив по нетипизированному указателю четыре вещественных числа, представляющих собой последовательные значения я, 2я, Зтс и 4я, а потом по нажатию кнопки считывать их в Memo. Для этого нам потребуются следующие переменные и типы:

type

dubp=Adouble; (указатель на число типа double} var

Forml: TForml;

pp:pointer; (нетипизированный указательI pd:dubp; (указатель на число типа double) pint:integer; (указатель-число}

db:double; (число типа double, занимает 8 байт в памяти} i:integer; {счетчик}

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

procedure TForml.FormCreate(Sender: TObject); begin

getmem(pp,32); pint:=longint(pp) ; for i:=l to 4 do

begin (сохраним в памяти четыре значения типа double: пи, 2пи, Зпи и 4пи} pd:=dubp(pint+(i-l)*8); db:=3.14159*i; pdA:~db; end; end;

А процедура чтения при нажатии на кнопку Buttoni такова:

procedure TForml.ButtonlClick(Sender: TObject); var st:string;

begin

Memol.Lines.Clear; for i:=l to 4 do

begin (извлекаем четыре значения double) pd: =ciubp(pirit-t (i-l)*3); db:=pdA;

str(db:10:5,st); Memol.Lines.Add(st); end; end;

Me забудем также освободить память:

procedure TForml.FormDestroy(Sender: TObject); begin

freememfpp,32);

end;

При нажатии на кнопку Buttonl в Memol будет выведено следующее:

3.14159 6.2831Й 9.42477 12.56636

Поиграв с этим механизмом, вы можете убедиться, что доступ к организованному массиву осуществляется побайтно, гак что интерпретировать размещенные в нем байты можно по-любому. Это очень удобно, если мы получаем, например, через СОМ-порт большие числа, которые при этом неизбежно оказываются разбиты на отдельные байты (аналогичная задача возник&ш у нас в главе 19 при чтении заголовка BMP-файла). Альтернативой побайтной посылке через последовательный порт является преобразование различных форматов чисел в символьную форму непосредственно во внешнем устройстве (наподобие того, как это делает С!PS-навигатор, разбираемый в главе 20). Это несложно: достаточно их преобразовать в десятичный неупакованный формат (см. приложение 1), а прибавлять ли еще к каждому разряду число 48 (код символа "О") для перевода в текстовую форму, или нет — дело уже десятое. Но всегда ли целесообразно этим заниматься, нагружая микроконтроллер, и зная, что в компьютере все равно это будет переводиться обратно в число?

Получаемые байты, конечно, можно накапливать в массиве-приемнике, а потом перемножать в соответствии с разрядностью. Например, четырехбайтное число типа integer при этом придется вычислять так (пусть байты поступают по "остроконечной" модели, т. е. сначала младший, и заполняют некий массив, начиная с младшего индекса): var

х:integer;

ab: array [1..4] of byte;

x:=ab[1]+256*ab[2]+ 256*256*ab(3)+ 256*256*256*ab[4];

Некрасиво? He то слово, но главное, что для вычисления подобного выражения программе, очевидно, придется привлекать арифметический сопроцессор. На ассемблере я бы такую операцию сделал более быстрым, чем умножение, способом сдвига разрядов (используя тот факт, что множители кратны степени двойки, а сдвиг на один разряд влево эквивалентен умножению на два), но не думаю, что выполнение операций умножения дифференцируется процессором в зависимости от конкретной величины операндов. Потому куда изящнее и быстрее использовать механизм приведения указателей, как мы и делали это в главе 19. Заменим в предыдущем примере объявления переменных на следующие:

type

longp=~integer; var

Forml: TForml;

pp:pointer; {нетипизированный указатель] pint:integer; (указатель-число} x,i:integer; 14-байтное число и счетчик) xb:byte; (число типа byte) px:longp; (указатель на integer)

В процедуре по созданию формы будем записывать по указателю последовательные степени числа 64:

procedure TForml.FormCreate(Sender: TObject); begin

getmem(pp, 16); pint:=longint(pp); x:=64;

for i:=l to 4 do begin

px;=longp(pint+(i-1)*4); рхл:=х; x:=x*64; end; end;

A в процедуре по нажатию кнопки мы их прочитаем:

procedure TForml.ButtonlClick(Sender: TObject); var st:string;

begin

Memol.Lines.Clear; for i:=l to 4 do

begin tизвлекаем четыре значения integer) px:=longp(pint+(i-1)*4); x:=рхл; str(x,st);

Memol.Lines.Add(st); and; end;

B Memol по выполнению этой программы мы получим:

64

4096

262144

16777216

Не забудем также потом освободить память через Freemem!

Источник: Ревнч Ю. В.  Нестандартные приемы программирования на Delphi. — СПб.: БХВ-Петербург, 2005. — 560 е.: ил.

По теме:

  • Комментарии