Часть III. В среде Borland Pascal
Глава 21. Использование памяти
В данной главе описывается, как программы Borland Pascal ис-
пользуют память. Borland Pascal может создавать прикладные прог-
раммы для реального режима DOS, защищенного режима DOS, и
Windows; в каждом типе прикладной программы память используется
по-разному. В данной главе поясняется, как использует память каж-
дый из этих типов программ. Мы рассмотрим также внутренние форма-
ты данных, подсистему управления динамически распределяемой об-
ластью памяти и прямой доступ к памяти.
Использование памяти программами реального режима DOS
На Рис. 21.1 приведена схема распределения памяти программы
Borland Pascal, для реального режима DOS.
Префикс программного сегмента (PSP) - это область длиной 256
байт, которая строится операционной системой DOS при загрузке
файла .EXE. Адрес PSP сохраняется в предописанной переменной
Borland Pascal длиной в слово с именем PrefixSeg.
Каждой программе (которая включает в себя основную программу
и каждый модуль) соответствует сегмент ее кода. Основная програм-
ма занимает первый сегмент кода. Следующие сегменты кода заняты
модулями (в порядке, обратном тому, в котором они указаны в опе-
раторе uses). Последний сегмент кода занят библиотекой исполняю-
щей системы (модуль System). Размер отдельного сегмента не может
превышать 64К, однако общий размер кода ограничен только объемом
имеющейся памяти.
Верхняя граница памяти DOS
HeapEnd -->-----------------------------
¦ ¦
¦ свободная память ¦
¦ ¦
HeapPtr -->¦............................¦
¦ динамически распределяемая ¦
¦ область памяти ¦
¦ (растет вверх) ^ ¦
HeapOrg -->+----------------------------+<-- OvrHeapEnd
¦ оверлейный буфер ¦
+----------------------------+<-- OvrHeapOrg
¦ стек (растет вниз) v ¦
SSeg:SPtr -->¦............................¦
¦ свободный стек ¦
SSeg:0000 -->+----------------------------+
¦ глобальные переменные ¦
¦............................¦<-------
¦ типизированные константы ¦ ¦
DSeg:0000 -->+----------------------------+ ¦
¦ кодовый сегмент ¦ ¦
¦ модуля System ¦ ¦
¦............................¦ ¦
¦ кодовый сегмент ¦ ¦
¦ первого модуля ¦ ¦
¦............................¦ ¦
L----------------------------- содержимое
. кодовый сегмент . образа
. других модулей . файла .EXE
----------------------------- ¦
¦............................¦ ¦
¦ кодовый сегмент ¦ ¦
¦ последнего модуля ¦ ¦
+----------------------------+ ¦
¦ кодовый сегмент ¦ ¦
¦ главной программы ¦ ¦
+----------------------------+<--------
¦ префикс сегмента программы ¦
¦ (PSP) ¦
PrefixSeg -->L-----------------------------
Рис. 21.1 Схема памяти для программы реального режима DOS.
Сегмент данных (адресуемый через регистр DS) содержит все
типизированные константы, за которыми следуют все глобальные пе-
ременные. В процессе выполнения программы регистр DS никогда не
изменяется. Размер сегмента данных не может превышать 64К.
При входе в программу регистр сегмента стека (SS) и указа-
тель стека (SР) загружаются так, что пара регистров SS:SР указы-
вает на первый байт, следующий за сегментом стека. Регистр SS в
процессе выполнения программы никогда не изменяется, а SP может
перемещаться вниз, пока не достигнет нижней границы сегмента.
Размер сегмента стека не может превышать 64К. По умолчанию ему
назначается размер, равный 16К, но с помощью директивы компилято-
ра $М это значение можно изменить.
Оверлейный буфер используется стандартным модулем Overlay
для хранения оверлейного кода. По умолчанию размер оверлейного
буфера соответствует размеру наибольшего оверлея в программе. Ес-
ли программа не имеет оверлеев, то размер оверлейного буфера бу-
дет нулевым. Размер оверлейного буфера можно увеличить с помощью
вызова подпрограммы OvrSetBuf модуля Overlay. В этом случае раз-
мер динамически распределяемой области памяти соответственно
уменьшается, а HeapOrg перемещается вверх.
В динамически распределяемой области сохраняются динамичес-
кие переменные, то есть переменные, выделенные при обращениях к
стандартным процедурам New и GetMem. Она занимает всю свободную
память или часть свободной памяти, оставшуюся при выполнении
программы. Действительный размер динамически распределяемой об-
ласти зависит от максимального и минимального значений, которые
можно установить для динамически распределяемой области с помощью
директивы компилятора $М. Гарантированный минимальный размер ди-
намически распределяемой области не может быть меньше минимально-
го значения, установленного для этой области. По умолчанию мини-
мальные размер динамически распределяемой области равен 0 байт, а
максимальный - 640К; это означает, что по умолчанию динамически
распределяемая область занимает всю доступную память.
Подсистема динамического распределения памяти (являющаяся
частью библиотеки исполняющей системы), как можно догадаться, уп-
равляет динамически распределяемой областью. Детально она описы-
вается в следующем разделе.
Администратор динамически распределяемой области памяти DOS
Динамически распределяемая область - это похожая на стек
структура, которая увеличивается, начиная от младших адресов па-
мяти. При этом используется сегмент динамически распределяемой
области. Нижняя граница динамически распределяемой области запо-
минается в переменной HеаpOrg, а верхняя граница динамически
распределяемой области соответствует нижней границе свободной па-
мяти и сохраняется в переменной НеаpPtr. При каждом выделении ди-
намической переменной в динамически распределяемой области под-
система динамического распределения памяти (администратор динами-
чески распределяемой области) перемещает переменную HeapPtr вверх
на размер переменной, как бы организуя при этом стек динамических
переменных, в котором одна переменная размещается над другой.
Переменная НеаpPtr после каждой операции как правило норма-
лизуется, и смещение, таким образом, принимает значения в диапа-
зоне от $0000 до $000F. Так как каждая переменная должна целиком
содержаться в одном сегменте, максимальный размер отдельной пере-
менной, которая может быть размещена в динамически распределяемой
области, составляет 65521 байт (что соответствует $10000 минус
$000F).
Методы освобождения областей динамически распределяемой памяти
Динамические переменные, сохраняемые в динамически распреде-
ляемой области, освобождаются одним из двух следующих способов:
1. С помощью процедур Dispose или FrееМем.
2. С помощью процедур Маrk и Rеlеаsе.
Простейшей схемой использования процедур Маrk и Rеlеаsе,
например, является выполнение следующих операторов:
New(Ptr1);
New(Ptr2);
Mark(P);
New(Ptr3);
New(Ptr4);
New(Ptr5);
Схема динамически распределяемой области при этом будет выг-
лядеть, как показано на Рис. 21.2.
HeapEnd -->--------------------------- Верхняя граница
¦ ¦ памяти
¦ ¦
HeapPtr -->+--------------------------+
¦ содержимое Ptr5^ ¦
Ptr5 -->+--------------------------+
¦ содержимое Ptr4^ ¦
Ptr4 -->+--------------------------+
¦ содержимое Ptr3^ ¦
Ptr3 -->+--------------------------+
¦ содержимое Ptr2^ ¦
Ptr2 -->+--------------------------+
¦ содержимое Ptr1^ ¦
Ptr1 -->L--------------------------- Нижняя граница памяти
Рис. 21.2 Метод освобождения областей динамически распреде-
ляемой области помощью процедур Маrk и Rеlеаsе.
Оператор Маrk(P) отмечает состояние динамически распределяе-
мой области непосредственно перед выделением памяти для перемен-
ной Ptr3 (путем сохранения текущего значения переменной НеаpPtr в
P). Если выполняется оператор Rеleаsе(P), то схема динамически
распределяемой области становится такой, как показано на Рис.
21.3. При этом, поскольку производится обращение к процедуре
Маrk, освобождается память, выделенная под все указатели.
Примечание: Выполнение процедуры Rеleаsе(НеаpОrg) пол-
ностью освобождает динамически распределяемую область памя-
ти, поскольку переменная НеаpOrg указывает на нижнюю грани-
цу динамически распределяемой области.
HeapEnd -->--------------------------- Верхняя граница
¦ ¦ памяти
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
HeapPtr -->+--------------------------+
¦ содержимое Ptr2^ ¦
Ptr2 -->+--------------------------+
¦ содержимое Ptr1^ ¦
Ptr1 -->L--------------------------- Нижняя граница памяти
Рис. 21.3 Схема динамически распределяемой области при вы-
полнении процедуры Rеleаsе(P).
Применение процедур Маrk и Rеlеаsе для освобождения памяти,
выделенной для динамических переменных, на которые ссылаются ука-
затели, выполняемое в порядке, в точности обратном порядку выде-
ления памяти, весьма эффективно. Однако в большинстве программ
имеется тенденция в более случайному выделению и освобождению па-
мяти, отведенной для динамических переменных, на которые ссылают-
ся указатели, что влечет за собой необходимость использования бо-
лее тонких методов управления памятью, которые реализованы с по-
мощью процедур Dispose и FrееMem. Эти процедуры позволяют в любой
момент освободить память, выделенную для любой динамической пере-
менной, на которую ссылается указатель.
Когда с помощью процедур Dispose и FrееМем освобождается па-
мять, отведенная для динамической переменной, не являющаяся "са-
мой верхней" переменной в динамически распределяемой области, то
динамически распределяемая область становится фрагментированной.
Предположим, что выполнялась та же последовательности операторов,
что и в предыдущем примере. Тогда после выполнения процедуры
Dispose(Ptr3) в центре динамически распределяемой области памяти
образуется незанятое пространство ("дыра"). Это показано на Рис.
21.4.
HeapEnd -->--------------------------- Верхняя граница
¦ ¦ памяти
¦ ¦
HeapPtr -->+--------------------------+
¦ содержимое Ptr5^ ¦
Ptr5 -->+--------------------------+
¦ содержимое Ptr4^ ¦
Ptr4 -->+--------------------------+
¦--------------------------¦
¦--------------------------¦
+--------------------------+
¦ содержимое Ptr2^ ¦
Ptr2 -->+--------------------------+
¦ содержимое Ptr1^ ¦
Ptr1 -->L--------------------------- Нижняя граница памяти
Рис. 21.4 Создание незанятой области ("дыры") в динамически
распределяемой области памяти.
Если в данный момент выполняется процедура New(Ptr3), то это
опять приведет к выделению той же области памяти. С другой сторо-
ны, выполнение процедуры Dispose(Ptr4) увеличит размер свободного
блока, так как Ptr3 и Ptr4 были соседними блоками (см. Рис.
21.5).
HeapEnd -->--------------------------- Верхняя граница
¦ ¦ памяти
¦ ¦
HeapPtr -->+--------------------------+
¦ содержимое Ptr5^ ¦
Ptr5 -->+--------------------------+
¦--------------------------¦
¦--------------------------¦
¦--------------------------¦
¦--------------------------¦
+--------------------------+
¦ содержимое Ptr2^ ¦
Ptr2 -->+--------------------------+
¦ содержимое Ptr1^ ¦
Ptr1 -->L--------------------------- Нижняя граница памяти
Рис. 21.5 Увеличение размера незанятого блока памяти.
В конечном итоге выполнение процедуры Dispose(Ptr5) приведет
сначала к созданию незанятого блока большего размера, а затем
НеаpPtr переместится в более младшие адреса памяти. Поскольку
последним допустимым указателем теперь будет Ptr2 (см. Рис.
21 6), то это приведет к действительному освобождению незанятого
блока.
HeapEnd -->--------------------------- Верхняя граница
¦ ¦ памяти
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
HeapPtr -->+--------------------------+
¦ содержимое Ptr2^ ¦
Ptr2 -->+--------------------------+
¦ содержимое Ptr1^ ¦
Ptr1 -->L--------------------------- Нижняя граница памяти
Рис. 21.7 Освобождение незанятого блока памяти.
Как показано на Рис. 21.7, динамически распределяемая об-
ласть памяти теперь находится в том же самом состоянии, в каком
она находилась бы после выполнения процедуры Rеlеаsе(P). Однако
создаваемые и освобождаемые при таком процессе незанятые блоки
отслеживаются для их возможного повторного использования.
Список свободных блоков
Адреса и размеры свободных блоков, созданных при операциях
Dispose и FrееМем, хранятся в списке свободных блоков, который
увеличивается вниз, начиная со старших адресов памяти, в сегменте
динамически распределяемой области. Каждый раз перед выделением
памяти для динамической переменной, перед тем, как динамически
распределяемая область будет расширена, проверяется список сво-
бодных блоков. Если имеется блок соответствующего размера (то
есть размер которого больше или равен требуемому размеру), то он
используется.
Процедура Rеlеаsе всегда очищает список свободных блоков.
Таким образом, программа динамического распределения памяти "за-
бывает" о незанятых блоках, которые могут существовать ниже ука-
зателя динамически распределяемой области. Если вы чередуете об-
ращения к процедурам Маrk и Rеlеаsе с обращениями к процедурам
Dispose и FrееМем, то нужно обеспечить отсутствие таких свободных
блоков.
Переменная FreeList модуля System указывает на первый сво-
бодный блок динамически распределяемой области памяти. Данный
блок содержит указатель на следующий свободный блок и т.д. Пос-
ледний свободный блок содержит указатель на вершину динамически
распределяемой области (то есть адрес, заданный HeapPtr). Если
свободных блоков в списке свободных блоков нет, то FreeList будет
равно HeapPtr.
Формат первых 8 байт свободного блока задается типом
TFreeRec:
type
PFreeRec = ^TFreeRec;
TFreeRec = record
Next: PFreeRec;
Size: Pointer;
end;
Поле Next указывает на следующий свободный блок, или на ту
же ячейку, что и HeapPtr, если блок является последним свободным
блоком. В поле Size записан размер свободного блока. Значение в
поле Size представляет собой не обычное 32-битовое значение, а
"нормализованное" значение-указатель с числом свободных парагра-
фов (16-байтовых блоков) в старшем слове и счетчиком свободных
байт (от 0 до 15) в младшем слове. Следующая функция BlockSize
преобразует значение поля Size в обычное значение типа Longint:
function BlockSize(Size: Pointer): Longint;
type
PtrRec = record Lo, Hi: Word end;
begin
BlockSize := Longint(PtrRec(Size)).Hi)*16+PtrRec(Size).Lo
end;
Чтобы обеспечить, что в начале свободного блока всегда имеет-
ся место для TFreePtr, подсистема управления динамически распре-
деляемой областью памяти округляет размер каждого блока, выделен-
ного подпрограммами New и GetMem до 8-байтовой границы. Таким
образом, 8 байт выделяется для блоков размером 1..8, 16 байт -
для блоков размером 9..16 и т.д. Сначала это кажется непроизводи-
тельной тратой памяти. Это в самом деле так, если бы каждый блок
был размером 1 байт. Но обычно блоки имеют больший размер, поэто-
му относительный размер неиспользуемого пространства меньше.
8-байтовый коэффициент раздробленности обеспечивает, что
при большом числе случайного выделения и освобождения блоков от-
носительно небольшого размера (что типично для записей переменной
длины в программах обработки текста) не приведет к сильной фраг-
ментации динамически распределяемой области. В качестве примера
предположим, что занимается и освобождается блок размером 50
байт. После его освобождения запись о нем включается в список
свободных блоков. Этот блок округляется до 56 (7*8) байт. Если в
дальнейшем потребуется блок размером от 49 до 56 байт, то данный
блок будет полностью повторно использован, а не останется от 1 до
7 байт памяти (использование который маловероятно), которые будут
только фрагментировать динамически распределяемую область.
Переменная HeapError
Переменная HeapError позволяет вам реализовать функцию обра-
ботки ошибки динамически распределяемой области памяти. Эта функ-
ция вызывается каждый раз, когда программа динамического распре-
деления памяти не может выполнить запрос на выделение памяти.
НеаpЕrror является указателем, который ссылается на функцию со
следующим заголовком:
function HeapFunc(Size: Word): Integer; far;
Заметим, что директива far указывает функции обработки ошиб-
ки динамически распределяемой области не необходимость использо-
вать дальнюю модель вызова.
Функция обработки ошибки динамически распределяемой области
реализуется путем присваивания ее адреса переменной НеаpEror:
HeapError := @HeapFunc;
Функция обработки ошибки динамически распределяемой области
памяти получает управление, когда при обращении к процедурам New
или GetМем запрос не может быть выполнен. Параметр Size содержит
размер блока, для которого не оказалось области памяти соответс-
твующего размера, и функция обработки ошибки динамически распре-
деляемой области попытается освободить блок, размер которого не
меньше данного размера.
В зависимости от успеха выполнения этой попытки функция об-
работки ошибки динамически распределяемой области возвращает зна-
чения 0, 1 или 2. Возвращаемое значение 0 свидетельствует о неу-
дачной попытке, что немедленно приводит к возникновению ошибки во
время выполнения программы. Возвращаемое значение 1 также свиде-
тельствует о неудачной попытке, но вместо ошибки этапа выполнения
оно приводит к тому, что процедуры GetМем или FrееМем возвращают
указатель nil. Наконец, возвращаемое значение 2 свидетельствует
об удачной попытке и вызывает повторную попытку выделить память
(которая также может привести к вызову функции обработки ошибки
динамически распределяемой области).
Стандартная обработки функция ошибки динамически распределя-
емой области всегда возвращает значение 0, приводя, таким обра-
зом, к ошибке всякий раз, когда не могут быть выполнены процедуры
New или GetМем. Однако для многих прикладных программ более под-
ходящей является простая функция обработки ошибки динамически
распределяемой области, пример которой приведен ниже:
function HeapFunc(Size: Word): Integer; far;
begin
HeapFunc := 1;
end;
Если такая функция реализована, то вместо принудительного
завершения работы программы в ситуации, когда процедуры New или
GetМем не могут выполнить запрос, она будет возвращать пустой
указатель (указатель nil).
Вызов функции ошибки динамически распределяемой области па-
мяти со значением параметра Size, равным 0, показывает, что удов-
летворение запроса на выделение памяти привело к расширению дина-
мически распределяемой области памяти путем перемещения HeapPtr
вверх. Это происходит, когда в списке свободных блоков нет сво-
бодных блоков, или когда все свободные блоки слишком малы для
удовлетворения данного запроса. Вызов со значением Size, равным
0, не указывает на состояние ошибки, поскольку между HeapPtr и
HeapEnd достаточно пространства для расширения, однако такой вы-
зов служит предупреждением, что неиспользуемая область выше
HeapPtr сократилась, и подсистема управления динамически распре-
деляемой областью памяти игнорирует значение, возвращаемое при
вызове такого типа.
Использование памяти в программах DOS защищенного режима
В данном разделе поясняется использование память в програм-
мах Borland Pascal для защищенного режима.
Сегменты кода
Прикладная программа и каждая библиотека в прикладной прог-
рамме или DLL имеет свой собственный сегмент кода. По умолчанию
модули с аналогичными атрибутами группируются в сегментах кода.
Вы можете управлять таким группированием с помощью директив $S и
$G имя_модуля. Размер одного сегмента кода не может превышать
64К, но общий размер кода ограничен только объемом доступной па-
мяти.
Атрибуты сегмента
Каждый сегмент кода имеет набор атрибутов, определяющих по-
ведение сегмента кода при загрузке в память.
Атрибуты MOVEABLE или FIXED
Когда сегмент кода имеет атрибут MOVEABLE (перемещаемый),
администратор памяти может перемещать сегмент в физической памя-
ти, чтобы удовлетворить другие запросы распределения памяти. Ког-
да сегмент кода имеет атрибут FIXED (фиксированный), он ни при
каких обстоятельствах не перемещается в физической памяти. Пред-
почтительным атрибутом является MOVEABLE, и пока не будет абсо-
лютно необходимо хранить сегмента в одних и тех же адресах памяти
(это имеет место, например, для обработчика прерываний), следует
использовать этот атрибут. Когда вам потребуется фиксированный
сегмент кода, такой сегмент кода следует сделать по возможности
маленьким.
Атрибуты PRELOAD или DEMANDLOAD
Сегмент кода, имеющий атрибут PRELOAD (предварительно загру-
жаемый), автоматически загружается при активизации прикладной
программы или библиотеки. Атрибут DEMANDLOAD (загружаемый по зап-
росу) откладывает загрузку сегмента или программы до фактического
вызова сегмента. Хотя это требует больше времени, но позволяет
прикладной программе экономить память.
Атрибуты DISCARDABLE или PERMAMENT
Когда сегмент имеет атрибут DIASCARDABLE (выгружаемый), ад-
министратор памяти защищенного режима может освободить занимаемую
сегментом память, когда требуется дополнительная память. Когда
сегмент имеет атрибут PERMANENT (постоянный), он все время хра-
нится в памяти. Когда прикладная программа вызывает сегмент
DISCARDABLE, отсутствующий в памяти, администратор защищенного
режима сначала загружает его из файла .EXE. Это требует больше
времени, чем если бы сегмент имел атрибут PERMANENT, но позволяет
выполнять прикладную программу в меньшем объеме памяти.
Сегмент DISCARDABLE в программе DOS защищенного режима ана-
логичен оверлейному сегменту в программе DOS, в то время как сег-
мент PERMANENT в защищенном режиме DOS аналогичен сегменту прог-
раммы DOS, не являющемуся оверлейным.
Сегменты данных и стека
Каждая прикладная программа защищенного режима DOS или биб-
лиотека содержит сегмент данных, которые может иметь размер до
64К. На сегмент всегда указывает регистр сегмента данных (DS).
Этот сегмент содержит типизированные константы и глобальные пере-
менные.
Кроме сегмента данных, прикладная программа защищенного ре-
жима DOS имеет сегмент стека, который используется для хранения
локальных переменных, распределенных процедурами и функциями. На
входе в прикладную программу регистр сегмента стека (SS) и указа-
тель стека (SP) загружены таким образом, что пара регистров SS:SP
указывает на первый байт после сегмента стека. Когда вызываются
процедуры и функции, SP для выделения пространства для парамет-
ров, адреса возврата и локальных переменных перемещается вниз.
Когда подпрограмма возвращает управление, процесс изменяется на
обратный: указатель стека увеличивается до значения, которое он
имел перед вызовом. По умолчанию размер сегмента стека равен 16К,
но с помощью директивы компилятора $M его можно изменить.
В отличие от прикладной программы, DDL DOS защищенного режи-
ма не имеет сегмента стека. Когда в DLL вызывается процедура или
функция, регистр DS изменяется, чтобы указывать на сегмент данных
DLL, но пара регистров SS:SP не модифицируется. Таким образом,
DLL всегда использует сегмент стека вызывающей прикладной прог-
раммы.
Изменение атрибутов
Используемые по умолчанию атрибуты сегмента кода - это атри-
буты MOVEABLE, DEMANDLOAD и DISCARDABLE. Но с помощью директивы
компилятора $C вы можете задать другие используемые по умолчанию
атрибуты, например:
{$C MOVEABLE PRELOAD PERMANENT}
В прикладной программе защищенного режима DOS нет необходи-
мости в администраторе оверлеев. Администратор памяти DOS защи-
щенного режима включает в себя полный набор средств управления
оверлеями, управлять которыми можно через атрибуты сегмента кода.
Описываемые ниже средства доступны для любой программы защищенно-
го режима DOS.
Примечание: Подробности о директиве компилятора $C
можно найти в Главе 2 ("Директивы компилятора") в "Справоч-
ном руководстве программиста".
Администратор динамически распределяемой области памяти DOS
Расширения Borland защищенного режима DOS включают в себя
полный администратор памяти защищенного режима. При выполнении
программы защищенного режима DOS вся доступная память превращает-
ся в глобальную динамически распределяемую область памяти, кото-
рая управляется администратором памяти (подсистемой управления
памятью) защищенного режима. Прикладная программа может получить
доступ к глобальной динамически распределяемой области памяти че-
рез подпрограммы GlobalXXXX модуля WinAPI. Хотя можно распреде-
лять блоки глобальной памяти любого размера, глобальная динами-
чески распределяемая область памяти предназначена только для
больших блоков (1024 байт или более). Для каждого блока глобаль-
ной памяти требуется дополнительно 32 байта (это непроизводитель-
ные затраты), а общее число блоков глобальной памяти не может
превышать 8192.
Примечание: Подробнее расширения Borland защищенного
режима DOS описываются в Главе 17 "Программирование в защи-
щенном режиме DOS".
Borland Pascal включает в себя администратор памяти (который
называют также подсистемой управления памятью), реализующий стан-
дартные процедуры New, Dispose, GetMem и FreeMem. Администратор
памяти использует для всех распределений памяти глобальную дина-
мически распределяемую область. Поскольку глобальная динамически
распределяемая область памяти ограничена 8192 блоками (что оче-
видно меньше, чем может потребоваться для некоторых прикладных
программ), администратор памяти Borland Pascal реализует алгоритм
вторичного распределения сегментов, который улучшает производи-
тельность и допускает распределение существенно большего коли-
чества блоков.
Примечание: Borland Pascal для расширенного режима DOS
не поддерживает схему распределения с помощью процедур MArk
и Release, предусмотренную для реального режима DOS.
Алгоритм вторичного распределения блоков работает следующим
образом. При выделении большого блока администратор памяти просто
выделяет глобальную память с помощью подпрограммы GlobalAlloc.
При выделении маленького блока администратор выделяет более круп-
ный блок памяти, а затем разбивает его (вторично распределяет) по
требованию на более мелкие блоки. Перед тем, как администратор
памяти выделяет новый блок глобальной памяти, который, в свою
очередь, будет распределяться повторно, при распределении малень-
ких блоков повторно используется все доступное пространство вто-
ричного распределения.
Примечание: Об использовании администратора памяти в
DLL подробнее рассказывается в Главе 11 "Динамически компо-
нуемые библиотеки".
Переменная HeapLimit определяет порог между маленькими и
большими блоками динамически распределяемой памяти. По умолчанию
ее значение равно 1024 байтам. Переменная HeapBlock определяет
размер, используемый администратором памяти при распределении
блоков, выделенных для вторичного распределения. По умолчанию
значение HeapBlock равно 8192 байтам. Значения этих переменных
изменять не следует, но если вы это сделаете, убедитесь, что зна-
чение HeapBlock составляет не меньше четырехкратного размера
HeapLimit.
Переменная HeapAllocFpals определяет значение флагов атрибу-
тов, передаваемых GlobalAlloc, когда администратор памяти распре-
деляет блоки глобальной памяти. По умолчанию ее значение равно
gmem_Moveable.
Переменная HeapError
Переменная HeapError позволяет вам реализовать функцию обра-
ботки ошибки динамически распределяемой области памяти. Эта функ-
ция вызывается каждый раз, когда программа динамического распре-
деления памяти не может выполнить запрос на выделение памяти.
НеаpError является указателем, который ссылается на функцию со
следующим заголовком:
function HeapFunc(Size: word): integer; far;
Заметим, что директива far указывает функции обработки ошиб-
ки динамически распределяемой области не необходимость использо-
вать дальнюю модель вызова.
Функция обработки ошибки динамически распределяемой области
реализуется путем присваивания ее адреса переменной НеаpEror:
HeapError := @HeapFunc;
Функция обработки ошибки динамически распределяемой области
памяти получает управление, когда при обращении к процедурам New
или GetМем запрос не может быть выполнен. Параметр Size содержит
размер блока, для которого не оказалось области памяти соответс-
твующего размера, и функция обработки ошибки динамически распре-
деляемой области произведет попытку освобождения блока, размер
которого не меньше данного размера.
Перед вызовом функции обработки ошибки динамически распреде-
ляемой области памяти администратор динамически распределяемой
памяти пытается выделить свободный блок из блоков вторичного раз-
биения, а также использовать непосредственный вызов функции
GlobalAlloc.
В зависимости от успеха выполнения этой попытки функция об-
работки ошибки динамически распределяемой области возвращает зна-
чения 0, 1 или 2. Возвращаемое значение 0 свидетельствует о неу-
дачной попытке, что немедленно приводит к возникновению ошибки во
время выполнения программы. Возвращаемое значение 1 также свиде-
тельствует о неудачной попытке, но вместо ошибки этапа выполнения
оно приводит к тому, что процедуры New или GetМем возвращают ука-
затель nil. Наконец, возвращаемое значение 2 свидетельствует об
удачной попытке и вызывает повторную попытку выделить память (ко-
торая также может привести к вызову функции обработки ошибки ди-
намически распределяемой области).
Стандартная обработки функция ошибки динамически распределя-
емой области всегда возвращает значение 0, приводя, таким обра-
зом, к ошибке всякий раз, когда не могут быть выполнены процедуры
New или GetМем. Однако для многих прикладных задач более подходя-
щей является простая функция обработки ошибки динамически распре-
деляемой области, пример которой приведен ниже:
function HeapFunc(Size: Word): Integer; far;
begin
HeapFunc := 1;
end;
Если такая функция реализована, то вместо принудительного
завершения работы программы в ситуации, когда процедуры New или
GetМем не могут выполнить запрос, она будет возвращать пустой
указатель (указатель nil).
Использование памяти в программах Windows
В данном разделе поясняется использование памяти в програм-
мах Borland Pascal для Windows.
Атрибуты сегментов
Каждый сегмент кода имеет набор атрибутов, определяющих его
поведение при загрузке в память.
Атрибуты MOVEABLE или FIXED
Когда сегмент является перемещаемым (MOVEABLE), Windows,
чтобы удовлетворить потребности в распределяемой памяти, может
перемещать сегмент в физической памяти. Когда сегмент кода фикси-
рованный (FIXED), он не перемещается в физической памяти. Более
предпочтителен атрибут MOVEABLE, и если нет абсолютной необходи-
мости хранить сегмент кода по одному и тому же адресу в физичес-
кой памяти (как бывает в том случае, если он содержит драйвер
прерываний), следует использовать атрибут MOVEABLE.
Атрибуты PRELOAD или DEMANDLOAD
Сегмент кода, имеющий атрибут PRELOAD, при активизации прик-
ладной программы или библиотеки загружается автоматически. Атри-
бут DEMANDLOAD откладывает загрузку сегмента до тех пор, пока
подпрограмма в сегменте действительно не будет вызвана.
Атрибуты DISCARDABLE или PERMANENT
Когда сегмент имеет атрибут DISCARDABLE, Windows при необхо-
димости выделения дополнительной памяти может освобождать память,
занимаемую данным сегментом. Когда прикладная программа обращает-
ся к выгружаемому сегменту (DISCARDABLE), которого нет в памяти,
Windows загружает его сначала из файла .EXE. Это занимает большее
время, чем если бы сегмент был постоянным (PERMANENT), но позво-
ляет прикладной программе при выполнении занимать меньше места.
Грубо говоря, сегмент DISCARDABLE в прикладной программе
Windows очень напоминает оверлейный сегмент в программе DOS.
Изменение атрибутов
По умолчанию сегменту кода назначаются атрибуты MOVEABLE,
PRELOAD и PERMANEMT, но с помощью директивы компилятора $C вы мо-
жете их изменить. Например:
{$C MOVEABLE DEMANDLOAD DISCARDABLE}
Примечание: Более подробно о директиве $C рассказыва-
ется в Главе 2 ("Директивы компилятора") "Справочного руко-
водства программиста".
В прикладной программе Windows нет необходимости выделять
подсистему управления оверлеями. Администратор памяти Windows
включает в себя полный набор обслуживающих средств, управляемых
атрибутами сегмента кода. Эти средства доступны любой прикладной
программе Windows.
Сегмент локальных динамических данных
Каждая прикладная программа или библиотека имеет один сег-
мент данных, который называется сегментом локальных динамических
данных и может занимать до 64К. На сегмент локальных динамических
данных всегда указывает регистр сегмента данных DS. Он разделен
на четыре части:
Сегмент локальных динамических данных
------------------------------------
¦ ¦
¦ Локальная динамически распределя- ¦
¦ емая область памяти ¦
¦ ¦
+-----------------------------------+
¦ ¦
¦ Стек ¦
¦ ¦
+-----------------------------------+
¦ ¦
¦ Статические данные ¦
¦ ¦
+-----------------------------------+
¦ ¦
¦ Заголовок задачи ¦
¦ ¦
L------------------------------------
Рис. 21.7 Сегмент локальных динамических данных.
Первый 16 байт сегмента локальных динамических данных всегда
содержат заголовок задачи, в котором Windows сохраняет различную
системную информацию.
Область статических данных содержит все глобальные перемен-
ные и типизированные константы, описанные в прикладной программе
или библиотеке.
Сегмент стека используется для хранения локальных перемен-
ных, распределяемых процедурами и функциями. На входе в приклад-
ную программу регистр сегмента стека SS и указатель стека SP заг-
ружаются таким образом, что SS:SP указывает на первый байт после
области стека в сегменте локальных динамических данных. При вызо-
ве процедур и функций SP перемещается вниз, выделяя память для
параметров, адреса возврата и локальных переменных. Когда подп-
рограмма возвращает управление, процесс изменяется на обратный:
SP увеличивается и принимает то значение, которое было перед вы-
зовом. Используемый по умолчанию размер области стека в автомати-
ческом сегменте данных равен 8К, но с помощью директивы компиля-
тора $M это значение можно изменить.
В отличие от прикладной программы библиотека в сегменте ло-
кальных динамических данных не имеет области стека. При вызове в
динамически компонуемой библиотеке DLL процедуры или функции ре-
гистр DS указывает на сегмент локальных динамических данных биб-
лиотеки, но пара регистров SS:SP не изменяется. Таким образом,
библиотека всегда использует стек вызывающей прикладной програм-
мы.
Последняя часть в сегменте локальных динамических данных -
локальная динамически распределяемая область. Она содержит все
локальные динамические данные, которые распределялись с помощью
функции LocalAlloc в Windows. По умолчанию локальная динамически
распределяемая область имеет размер 8К, но это значение можно из-
менить с помощью директивы компилятора $M.
Windows допускает, чтобы сегмент локальных динамических дан-
ных был перемещаемым, но Borland Pascal этого не поддерживает.
Сегмент локальных динамических данных прикладной программы или
библиотеки Borland Pascal всегда блокируется, этим обеспечивает-
ся, что селектор (адрес сегмента) сегмента локальных динамических
данных никогда не изменяется. При работе в стандартном или расши-
ренном режиме это не приводит ни к какому ухудшению, поскольку
сегмент сохраняет тот же селектор даже при перемещении в физичес-
кой памяти. Однако в реальном режиме, если требуется расширение
локальной динамически распределяемой области, Windows, возможно,
не сможет этого сделать, поскольку сегмент локальных динамических
данных перемещаться не может. Если ваша прикладная программа ис-
пользует локальную динамически распределяемую область памяти и
должна выполняться в реальном режиме, то следует обеспечить, что-
бы начальный размер локальной динамически распределяемой области
был таким, чтобы он удовлетворял всем потребностям в распределе-
нии локальной динамической области (для этого используется дирек-
тива компилятора $M).
Администратор динамически распределяемой области памяти
Windows поддерживает динамическое распределение памяти в
двух различных динамически распределяемых областях: глобальной
динамически распределяемой области и локальной динамически расп-
ределяемой области.
Примечание: Более подробно о локальной и глобальной
динамически распределяемой области рассказывается в "Руко-
водстве программиста по Windows".
Глобальная динамически распределяемая область - это пул па-
мяти, доступный для всех прикладных программ. Хотя могут выде-
ляться блоки глобальной памяти любого размера, глобальная динами-
чески распределяемая область памяти предназначена только для
"больших" областей памяти (256 байт или более). Каждый блок гло-
бальной памяти имеет избыточный размер 20 байт, и при работе в
стандартной среде Windows в улучшенном режиме 386 существует ог-
раничение в 8192 блока памяти, только некоторые из которых дос-
тупны для отдельной прикладной программы.
Локальная динамически распределяемая область памяти - это
пул памяти, доступной только для вашей прикладной программы или
библиотеки. Она расположена в верхней части сегмента данных прик-
ладной программы или библиотеки. Общий размер блоков локальной
памяти, которые могут выделяться в локальной динамически распре-
деляемой области, равен 64К, минус размер стека прикладной прог-
раммы и статических данных. По этой причине локальная динамически
распределяемая область памяти лучше подходит для "небольших" бло-
ков памяти (26 байт или менее). По умолчанию размер локальной ди-
намически распределяемой области равен 8К, но с помощью директивы
компилятора $M это значение можно изменить.
Примечание: Borland Pascal не поддерживает механизм
распределения памяти с помощью процедур Mark и Release, ко-
торые предусмотрены в версии для DOS.
Borland Pascal включает в себя подсистему управления динами-
чески распределяемой памятью (администратор памяти), которая реа-
лизует стандартные процедуры New, Dispose, GetMem и FreeMem. Для
всех выделений памяти подсистема динамически управления распреде-
ляемой областью памяти использует глобальную динамически распре-
деляемую область. Поскольку глобальная динамически распределяемая
область памяти имеет системное ограничение в 8192 блока (что оп-
ределенно меньше, чем может потребоваться в некоторых прикладных
задачах), подсистема управления динамически распределяемой об-
ластью памяти Borland Pascal для улучшения производительности и
обеспечения выделения существенно большего числа блоков включает
в себя алгоритм вторичного распределения сегмента.
Примечание: Более подробно об этом рассказывается в
Главе 11 "Динамически компонуемые библиотеки".
Алгоритм вторичного выделения сегмента работает следующим
образом: при распределении большого блока администратор динами-
чески распределяемой области памяти просто выделяет глобальный
блок памяти, используя подпрограмму Windows ClobalAlloc. При вы-
делении маленького блока администратор динамически распределяемой
области памяти выделяет больший блок памяти, а затем делит его на
более мелкие блоки (как требуется). При выделении "маленьких"
блоков перед тем, как администратор динамически распределяемой
области памяти выделит блок глобальной динамически распределяемой
памяти (который будет в свою очередь разбит на блоки), повторно
используются все доступные мелкие блоки.
Границу между маленькими и большими блоками определяется пе-
ременной HeapLimit. По умолчанию она имеет значение 1024 байта.
Переменная HeapBlock определяет размер, который использует под-
система управления динамически распределяемой областью памяти при
выделении блоков для вторичного разбиения. По умолчанию она имеет
значение 8192 байта. Изменять эти значения вам незачем, но если
вы решите это сделать, убедитесь что HeapBlock имеет значение по
крайней мере в четыре раза превышающее HeapLimit.
Переменная HeapAllocFlags определяет значение флагов атрибу-
тов, передаваемых GlobalAlloc, когда администратор памяти распре-
деляет глобальные блоки. В программе по умолчанию используется
значение gmem_Moveable, а в библиотеке - gmem_Moveable +
gmem_SSEShure.
Блоки глобальной памяти, выделяемые администратором динами-
чески распределяемой области памяти, всегда блокируются непос-
редственно после своего выделения (с помощью GlobalLock) немед-
ленно после своего выделения и не разблокируются, пока не будут
освобождены. Этим обеспечивается, что селекторы (адреса сегмен-
тов) блоков не изменяются. В стандартной среде Windows и улучшен-
ных режимах процессора 386 фиксированные блоки могут, тем не ме-
нее, перемещаться в физической памяти, освобождая место для дру-
гих запросов по выделению памяти, поэтому это не ухудшает произ-
водительности администратора динамически распределяемой области
памяти Borland Pascal. Однако в реальном режиме, если от Windows
требуется расширение локальной динамически распределяемой облас-
ти, администратор памяти Windows, возможно, не сможет переместить
их, чтобы выделить другие блоки. Если ваша прикладная программа
использует локальную динамически распределяемую область и должна
выполняться в реальном режиме, можно рассмотреть при выделении
блоков динамической памяти возможность использования средств
распределения памяти, предоставляемых Windows.
Переменная HeapError
Переменная HeapError позволяет вам реализовать функцию обра-
ботки ошибки динамически распределяемой области памяти. Эта функ-
ция вызывается каждый раз, когда программа динамического распре-
деления памяти не может выполнить запрос на выделение памяти.
НеаpError является указателем, который ссылается на функцию со
следующим заголовком:
function HeapFunc(Size: word): integer; far;
Заметим, что директива far указывает функции обработки ошиб-
ки динамически распределяемой области не необходимость использо-
вать дальнюю модель вызова.
Функция обработки ошибки динамически распределяемой области
реализуется путем присваивания ее адреса переменной НеаpEror:
HeapError := @HeapFunc;
Функция обработки ошибки динамически распределяемой области
памяти получает управление, когда при обращении к процедурам New
или GetМем запрос не может быть выполнен. Параметр Size содержит
размер блока, для которого не оказалось области памяти соответс-
твующего размера, и функция обработки ошибки динамически распре-
деляемой области попытается освободить блок, размер которого не
меньше данного размера.
Перед вызовом функции обработки ошибки динамически распреде-
ляемой области памяти подсистема динамического распределения па-
мяти пытается выделить свободный блок из блоков вторичного разби-
ения, а также использовать непосредственный вызов функции
GlobalAlloc.
В зависимости от успеха выполнения этой попытки функция об-
работки ошибки динамически распределяемой области возвращает зна-
чения 0, 1 или 2. Возвращаемое значение 0 свидетельствует о неу-
дачной попытке, что немедленно приводит к возникновению ошибки во
время выполнения программы. Возвращаемое значение 1 также свиде-
тельствует о неудачной попытке, но вместо ошибки этапа выполнения
оно приводит к тому, что процедуры New или GetМем возвращают ука-
затель nil. Наконец, возвращаемое значение 2 свидетельствует об
удачной попытке и вызывает повторную попытку выделить память (ко-
торая также может привести к вызову функции обработки ошибки ди-
намически распределяемой области).
Стандартная обработки функция ошибки динамически распределя-
емой области всегда возвращает значение 0, приводя, таким обра-
зом, к ошибке всякий раз, когда не могут быть выполнены процедуры
New или GetМем. Однако для многих прикладных задач более подходя-
щей является простая функция обработки ошибки динамически распре-
деляемой области, пример которой приведен ниже:
function HeapFunc(Size: word) integer; far;
begin
HeapFunc := 1;
end;
Если такая функция реализована, то вместо принудительного
завершения работы программы в ситуации, когда процедуры New или
GetМем не могут выполнить запрос, она будет возвращать пустой
указатель (указатель nil).
Форматы внутреннего представления данных
Далее описываются форматы внутреннего представления данных
Borland Pascal.
Целочисленные типы
Формат, выбираемый для представления переменной целого типа,
зависит от ее минимальной и максимальной границ:
1. Если обе границы находятся в диапазоне -128..127
(Shotrint - короткое целое), то переменная хранится, как
байт со знаком.
2. Если обе границы находятся в диапазоне 0..255 (Byte -
байтовая переменная), то переменная хранится, как байт
без знака.
3. Если обе границы находятся в диапазоне -32768..32767
(Integer - целое), то переменная хранится, как слово со
знаком.
4. Если обе границы находятся в диапазоне 0..65535 (Word -
переменная длиной в слово), то переменная хранится, как
слово.
5. В противном случае переменная хранится, как двойное сло-
во со знаком (Longint - длинное целое).
Символьный тип
Символьный тип или поддиапазон (отрезок) символьного типа
(Char) хранится, как байт без знака.
Булевский тип
Значения и переменные булевского типа Boolean хранятся как
байт, WordBool - как слово, а LongBool - как значение Longint.
При этом подразумеваются, что они могут принимать значения 0
(Falsе) или 1 (Тruе).
Перечислимый тип
Значения перечислимого типа хранятся, как байт без знака,
если нумерация не превышает 256. В противном случае они хранятся,
как слово без знака.
Типы с плавающей точкой
Типы значений с плавающей точкой Real, Single, Double,
Extended и Comp (вещественный, с одинарной точностью, с двойной
точностью, с повышенной точностью и сложный) хранятся в виде дво-
ичного представления знака (+ или -), показателя степени и знача-
щей части числа. Представляемое число имеет значение:
+/- значащая_часть Х 2^показатель_степени
где значащая часть числа представляет собой отдельный бит слева
от двоичной десятичной точки (то есть 0 <= значащая часть <= 2).
В следующей далее схеме слева расположены старшие значащие
биты, а справа - младшие значащие биты. Самое левое значение хра-
нится в самых старших адресах. Например, для значения веществен-
ного типа e сохраняется в первом байте, f - в следующих пяти бай-
тах, а s - в старшем значащем бите последнего байта.
Вещественный тип
Шестибайтовое (48-битовое) вещественное число (Real) подраз-
деляется на три поля:
1 39 8
----T------..-------T--------
¦ s ¦ f ¦ e ¦
L---+------..-------+---------
msb lsb msb lsb
Значение v числа определяется с помощью выражений:
if 0 < e <= 255, then v = (-1)^s * 2^(e-129)*(l.f).
if e = 0, then v = 0.
Вещественный тип не может использоваться для хранения ненор-
мализованных чисел, значений, не являющихся числом (NaN), а также
бесконечно малых и бесконечно больших значений. Ненормализованное
число при сохранении его в виде вещественного принимает нулевое
значение, а не числа, бесконечно малые и бесконечно большие зна-
чения при попытке использовать для их записи формат вещественного
числа приводят к ошибке переполнения.
Здесь и далее msb означает более значащий бит (старшие раз-
ряды), lsb - менее значащий (младшие разряды).
Тип числа с одинарной точностью
Четырехбайтовое (32-битовое) число типа Single подразделяет-
ся на три поля:
1 8 23
----T------T-------..---------
¦ s ¦ e ¦ f ¦
L---+------+-------..----------
msb lsb msb lsb
Значение v этого числа определяется с помощью выражений:
if 0 < e < 255, then v = (-1)^s * 2^(e-12) * (l.f).
if e = 0 and f <> 0, then v = (-1)^s * 2^(126) * (o.f).
if e = 0 and f = 0, then v = (-1)^s * O.
if e = 255 and f = 0, then v = (-1)^s * Inf.
if e = 255 and f <> 0, then v = NaN.
Тип числа с двойной точностью
Восьмибайтовое (64-битовое) число типа Double подразделяется
на три поля:
1 11 52
----T------T-------..--------
¦ s ¦ e ¦ f ¦
L---+------+-------..---------
msb lsb msb lsb
Значение v этого числа определяется с помощью выражений:
if 0 < e < 2047, then v = (-1)^s * 2^(e-1023) * (l.f).
if e = 0 and f <> 0, then v = (-1)^s * 2^(1022) * (o.f).
if e = 0 and f = 0, then v = (-1)^s * O.
if e = 2047 and f = 0, then v = (-1)^s * Inf.
if e = 2047 and f <> 0, then v = NaN.
Тип числа с повышенной точностью
Десятибайтовое (80-битовое) число типа Extended подразделя-
ется на четыре поля:
1 15 1 63
----T--------T---T--------..-------
¦ s ¦ e ¦ i ¦ f ¦
L---+--------+---+--------..--------
msb lsb msb lsb
Значение v этого числа определяется с помощью выражений:
if 0 < e < 32767, then v = (-1)^s * 2^(e-1023) * (l.f).
if e = 32767 and f = 0, then v = (-1)^s * Inf.
if e = 32767 and f <> 0, then v = NaN.
Сложный тип
Восьмибайтовое (64-битовое) число сложного типа (Comp) под-
разделяется на два поля:
1 63
----T-----------..--------------
¦ s ¦ d ¦
L---+-----------..---------------
msb lsb
Значение v этого числа определяется с помощью выражений:
if s = 1 and d = 0, then v = NaN.
в противном случае v представляет собой 64-битовое значение, яв-
ляющееся дополнением до двух.
Значения типа указатель
Значение типа указатель хранится в виде двойного слова, при
этом смещение хранится в младшем слове, а адрес сегмента - в
старшем слове. Значение указателя nil хранится в виде двойного
слова, заполненного 0.
Значения строкового типа
Строка занимает столько байт, какова максимальная длина
строки, плюс один байт. Первый байт содержит текущую динамическую
длину строки, а последующие байты содержат символы строки. Бит
длины и символы рассматриваются, как значения без знака. Макси-
мальная длина строки - 255 символов, плюс байт длины
(string[255]).
Значения множественного типа
Множество - это массив бит, в котором каждый бит указывает,
является элемент принадлежащим множеству или нет. Максимальное
число элементов множества - 256, так что множество никогда не мо-
жет занимать более 32 байт. Число байт, занятых отдельным мно-
жеством, вычисляется, как:
ByteSize = (Max div 8) - (Min div 8) + 1
где Мin и Мах - нижняя и верхняя граница базового типа этого мно-
жества. Номер байта для конкретного элемента E вычисляется по
формуле:
ByteNumber = (E div 8) - (Min div 8)
а номер бита внутри этого байта по формуле:
BitNumber = E mod 8
где E обозначает порядковое значение элемента.
Значения типа массив
Массив хранится в виде непрерывной последовательности пере-
менных, каждая из которых имеет тип массива. Элементы с наимень-
шими индексами хранятся в младших адресах памяти. Многомерный
массив хранится таким образом, что правый индекс возрастает быст-
рее.
Значения типа запись
Поля записи хранятся, как непрерывная последовательность пе-
ременных. Первое поле хранится в младших адресах памяти. Если в
записи содержатся различные части, то каждая часть начинается с
одного и того же адреса памяти.
Объектные типы
Внутренний формат данных объекта имеет сходство с внутренним
форматом записи. Поля объекта записываются в порядке их описаний
как непрерывная последовательность переменных. Любое поле, унас-
ледованное от родительского (порождающего) типа, записывается пе-
ред новыми полями, определенными в дочернем (порожденном) типе.
Если объектный тип определяет виртуальные методы, конструк-
тор или деструктор, то компилятор размещает в объектном типе до-
полнительное поле данных. Это 16-битовое поле, называемое полем
таблицы виртуальных методов (VMP), используется для запоминания
смещения таблицы виртуальных методов в сегменте данных. Поле таб-
лицы виртуальных методов следует непосредственно после обычных
полей объектного типа. Если объектный тип наследует виртуальные
методы, конструкторы или деструкторы (сборщики мусора), то он
также наследует и поле таблицы виртуальных методов, благодаря че-
му дополнительное поле таблицы виртуальных методов не выделяется.
Инициализация поля таблицы виртуальных методов экземпляра
объекта осуществляется конструктором (или конструкторами) объект-
ного типа. Программа никогда не инициализирует поле таблицы вир-
туальных методов явно и не имеет к нему доступа.
Следующие примеры иллюстрируют внутренние форматы данных
объектных типов.
type
PLocation = ^TLocation;
TLocation = object
X,Y: integer;
procedure Init(PX, PY: Integer);
function GetX: Integer;
function GetY: Integer;
end;
PPoint = ^TPoint;
TPoint = object(TLocation)
Color: Integer;
constructor Init(PX, PY, PColor: Integer);
destructor Done; virtual;
procedure Show; virtual;
procedure Hide; virtual;
procedure MoveTo(PX, PY: I+nteger); virtual;
end;
PCircle = ^TCircle;
TCircle = object(TPoint)
Radius: Integer;
constructor Init(PX, PY, PColor, PRadius: Integer);
procedure Show; virtual;
procedure Hide; virtual;
procedure Fill; virtual;
end;
Рисунок 21.8 показывает размещение экземпляров типов
TLocation, TPoint и TCircle: каждый прямоугольник соответствует
одному слову памяти.
TLocation TPoint TCircle
----------- ------------ ------------
¦ X ¦ ¦ X ¦ ¦ X ¦
+----------+ +-----------+ +-----------+
¦ Y ¦ ¦ Y ¦ ¦ Y ¦
L----------- +-----------+ +-----------+
¦ Color ¦ ¦ Color ¦
+-----------+ +-----------+
¦ VMT ¦ ¦ VMT ¦
L------------ +-----------+
¦ Radius ¦
L------------
Рис. 21.8 Схема экземпляров типов TLocation, TPoint и
TCircle.
Так как TPoint является первым типом в иерархии, который
вводит виртуальные методы, то поле таблицы виртуальных методов
размещается сразу после поля Color.
Таблица виртуальных методов
Каждый объектный тип, содержащий или наследующий виртуальные
методы, конструкторы или деструкторы, имеет связанную с ним таб-
лицу виртуальных методов, в которой запоминается инициализируемая
часть сегмента данных программы. Для каждого объектного типа (но
не для каждого экземпляра) имеется только одна таблица виртуаль-
ных методов, однако два различных объектных типа никогда не раз-
деляют одну таблицу виртуальных методов, независимо от того, нас-
колько эти типы идентичны. Таблицы виртуальных методов создаются
автоматически компилятором, и программа никогда не манипулирует
ими непосредственно. Аналогично, указатели на таблицы виртуальных
методов автоматически запоминаются в реализациях объектных типов
с помощью конструкторов программа никогда не работает с этими
указателями непосредственно.
Первое слово таблицы виртуальных методов содержит размер
экземпляров соответствующего объектного типа. Эта информация ис-
пользуется конструкторами и деструкторами для определения того,
сколько байт выделяется или освобождается при использовании рас-
ширенного синтаксиса стандартных процедур New и Dispose.
Второе слово таблицы виртуальных методов содержит отрица-
тельный размер экземпляров соответствующего объектного типа эта
информация используется ратификационным (т.е. подтверждающим
действительность) механизмом вызова виртуального метода для выяв-
ления инициализируемых объектов (экземпляров, для которых должен
выполняться конструктор) и для проверки согласованности таблицы
виртуальных методов. Когда разрешена ратификация виртуального вы-
зова (с помощью директивы {$R+} компилятора, которая расширена и
включает в себя проверку виртуальных методов), компилятор генери-
рует вызов программы ратификации таблицы виртуальных методов пе-
ред каждым вызовом виртуального метода. Программа ратификации
таблицы виртуальных методов проверяет, что первое слово таблицы
виртуальных методов не равно нулю и что сумма первого и второго
слов равна нулю. Если любая из проверок неудачна, то генерируется
ошибка 210 исполняющей системы Borland Pascal.
Разрешение проверок границ диапазонов и проверок вызовов
виртуальных методов замедляет выполнение программы и делает ее
несколько больше, поэтому используйте {$R+} только во время от-
ладки и переключите эту директиву в состояние {$R-} в окончатель-
ной версии программы.
Наконец, начиная со смещения 4 таблицы виртуальных методов
следует список 32-разрядных указателей методов, один указатель на
каждый виртуальный метод в порядке их описаний. Каждая позиция
содержит адрес точки входа соответствующего виртуального метода.
На Рис. 21.9 показано размещение таблиц виртуальных методов
типов Point и Circle (тип Location не имеет таблицы виртуальных
методов, т.к. не содержит в себе виртуальных методов, конструкто-
ров и деструкторов): каждый маленький прямоугольник соответствует
одному слову памяти, а каждый большой прямоугольник - двум словам
памяти.
Point VMT Circle VMT
---------------- -----------------
¦ 8 ¦ ¦ 8 ¦
+---------------+ +----------------+
¦ -8 ¦ ¦ -8 ¦
+---------------+ +----------------+
¦ 0 ¦ ¦ 0 ¦
+---------------+ +----------------+
¦ 0 ¦ ¦ 0 ¦
+---------------+ +----------------+
¦ ¦ ¦ ¦
¦ @TPoint.Done ¦ ¦ @TPoint.Done ¦
¦ ¦ ¦ ¦
+---------------+ +----------------+
¦ ¦ ¦ ¦
¦ @TPoint.Show ¦ ¦ @TCircle.Show ¦
¦ ¦ ¦ ¦
+---------------+ +----------------+
¦ ¦ ¦ ¦
¦ @TPoint.Hide ¦ ¦ @TCircle.Hide ¦
¦ ¦ ¦ ¦
+---------------+ +----------------+
¦ ¦ ¦ ¦
¦ @TPoint.MoveTo¦ ¦ @TPoint.MoveTo ¦
¦ ¦ ¦ ¦
L---------------- +----------------+
¦ ¦
¦ @TCircle.Fill ¦
¦ ¦
L-----------------
Рис. 21.9 Схемы таблиц виртуальных методов для TPoint и
TCircle.
Обратите внимание на то, как TCircle наследует методы Done и
MoveTo типа TPoint и как он переопределяет Show и Hide.
Как уже упоминалось, конструкторы объектных типов содержат
специальный код, который запоминает смещение таблицы виртуальных
методов объектного типа в инициализируемых экземплярах. Например,
если имеется экземпляр P типа TPoint и экземпляр C типа TCircle,
то вызов P.Init будет автоматически записывать смещение таблицы
виртуальных методов типа TPoint в поле таблицы виртуальных мето-
дов экземпляра P, а вызов C.Init точно так же запишет смещение
таблицы виртуальных методов типа TCircle в поле таблицы виртуаль-
ных методов экземпляра C. Эта автоматическая инициализация явля-
ется частью кода входа конструктора, поэтому если управление пе-
редается в начало операторной секции, то поле Self таблицы вирту-
альных методов также будет установлено. Таким образом, при воз-
никновении необходимости, конструктор может выполнить вызов вир-
туального метода.
Таблица динамических методов
Таблица виртуальных методов объектного типа содержит для
каждого описанного в объектном типе виртуального метода и его
предков четырехбайтовую запись. В тех случаях, когда в порождаю-
щих типах (предках) определяется большее число виртуальных мето-
дов, в процессе создания производных типов может использоваться
достаточно большой объем памяти, особенно если создается много
производных типов. Хотя в производных типах могут переопределять-
ся только некоторые из наследуемых методов, таблица виртуальных
методов каждого производного типа содержит указатели метода для
всех наследуемых виртуальных методов, даже если они не изменя-
лись.
Динамические методы обеспечивают в таких ситуациях альтерна-
тиву. В Borland Pascal имеется формат таблицы методов и новый
способ диспетчеризации методов с поздним связыванием. Вместо ко-
дирования для всех методов объектного типа с поздним связыванием,
в таблице динамических методов кодируются только те методы, кото-
рые были в объектном типе переопределены. Если в наследующих ти-
пах переопределяются только некоторые из большого числа методов с
поздним связыванием, формат таблицы динамических методов исполь-
зует меньшее пространство, чем формат таблицы виртуальных мето-
дов.
Формат таблицы динамических методов иллюстрируют следующие
два объектных типа:
type
TBase = object
X: Integer;
constructor Init;
destructor Done; virtual;
procedure P10; virtual 10;
procedure P20; virtual 20;
procedure P30; virtual 30;
procedure P30; virtual 30;
end;
type
TDerived = object(TBase)
Y: Integer;
constructor Init;
destructor Done; virtual;
procedure P10; virtual 10;
procedure P30; virtual 30;
procedure P50; virtual 50;
end;
На Рис. 21.10 и 21.11 показаны схемы таблицы виртуальных ме-
тодов и таблицы динамических методов для TBase и TDerived. Каждая
ячейка соответствует слову памяти, а каждая большая ячейка - двум
словам памяти.
ТВМ TBase ТДМ TBase
------------------- -------------------
¦ 4 ¦ ¦ 0 ¦
+------------------+ +------------------+
¦ -4 ¦ ¦ индекс в кэш ¦
+------------------+ +------------------+
¦ Смещ. ТДМ TBase ¦ ¦ смещение записи ¦
+------------------+ +------------------+
¦ 0 ¦ ¦ 4 ¦
+------------------+ +------------------+
¦ ¦ ¦ 10 ¦
¦ @TBase.Done ¦ +------------------+
¦ ¦ ¦ 20 ¦
L------------------- +------------------+
¦ 30 ¦
+------------------+
¦ 40 ¦
+------------------+
¦ ¦
¦ @TBase.P10 ¦
¦ ¦
+------------------+
¦ ¦
¦ @TBase.P20 ¦
¦ ¦
+------------------+
¦ ¦
¦ @TBase.P30 ¦
¦ ¦
+------------------+
¦ ¦
¦ @TBase.P40 ¦
¦ ¦
L-------------------
Рис. 21.10 Схемы таблицы виртуальных методов и таблицы дина-
мических методов для TBase.
Объектный тип имеет таблицу динамических методов только в
том случае, если в нем вводятся или переопределяются динамические
методы. Если объектный тип наследует динамические методы, но они
не переопределяются, и новые динамические методы не вводятся, то
он просто наследует таблицу динамических методов своего предка.
Как и в случае таблицы виртуальных методов, таблица динами-
ческих методов записывается в инициализированную часть сегмента
данных прикладной программы.
ТВМ TDerived ТДМ TDerived
-------------------- -------------------
¦ 6 ¦ ¦ Смещ. ТДМ TBase ¦
+-------------------+ +------------------+
¦ -6 ¦ ¦ индекс в кеше ¦
+-------------------+ +------------------+
¦ Смещ. ТДМ TDerived¦ ¦ смещение записи ¦
+-------------------+ +------------------+
¦ 0 ¦ ¦ 3 ¦
+-------------------+ +------------------+
¦ ¦ ¦ 10 ¦
¦ @TBase.Done ¦ +------------------+
¦ ¦ ¦ 30 ¦
L-------------------- +------------------+
¦ 50 ¦
+------------------+
¦ ¦
¦ @TDerived.P10 ¦
¦ ¦
+------------------+
¦ ¦
¦ @TDerived.P30 ¦
¦ ¦
+------------------+
¦ ¦
¦ @TDerived.T50 ¦
¦ ¦
L-------------------
Рис. 21.11. Схемы таблицы виртуальных методов и таблицы ди-
намических методов для TDerived.
Первое слово таблицы динамических методов содержит смещение
сегмента данных родительской таблицы динамических методов, или 0,
если родительская таблица динамических методов отсутствует.
Второе и третье слово таблицы динамических методов использу-
ется в кеш-буфере просмотра динамических методов (см. далее).
Четвертое слово таблицы динамических методов содержит счет-
чик записи таблицы динамических методов. Непосредственно за ним
следует список слов, каждое из которых содержит индекс динамичес-
кого метода, а затем список соответствующих указателей методов.
Длина каждого списка задается счетчиком записи таблицы динамичес-
ких методов.
Значения файлового типа
Значения файлового типа представляются в виде записей. Типи-
зированные и нетипизированные файлы занимают 128 байт, которые
располагаются по следующей схеме:
type
TFileRec = record
Handle : word; { описатель }
Mode : word; { режим }
RecSize : word; { размер записи }
Private : array[1..26] of byte;
UserData : array[1..16] of byte;
Name : array[0..79] of char;
end;
Текстовые файлы занимают 256 байт со следующей схемой распо-
ложения:
type
TTextBuf = array[0..127] of char;
TTextRec = record
Handle : word;
Mode : word;
BufSize : word;
Private : word;
BufPos : word;
BufEnd : word;
BufPtr : ^TTextBuf;
OpenFunc : pointer;
InOutFunc : pointer;
FlushFunc : pointer;
CloseFunc : pointer;
UserData : array[1..16] of Byte;
Name : array[0..79] of Char;
Buffer : TTextBuf;
end;
В переменной Наndlе содержится номер описателя файла (когда
файл открыт). Это значение возвращается DOS.
Поле Моdе считается равным одному из следующих значений:
const
fmClosed = $D7B0;
fmInput = $D7B1;
fmOutput = $D7B2;
fmInOut = $D7B3;
Значение fmClosed показывает, что файл закрыт. Значения
fmInput и fmOutput показывают, что файл является текстовым файлом
и что для него была выполнена процедура Reset (fmInput) или
Rewrite (fmOutput). Значение fmOutput показывает, что переменная
файлового типа является типизированным или нетипизированным фай-
лом, для которого была выполнена процедура Reset или Rewrite. Лю-
бое другое значение говорит о том, что для файловой переменной
присваивание не было выполнено (и она, таким образом, не инициа-
лизирована).
Поле UserData в Borland Pascal недоступно, и пользователь-
ские программы могут сохранять в нем данные.
Поле Nаме содержит имя файла, которое представляет собой
последовательность символов, оканчивающуюся нулевым символом
(#0).
Для типизированных и нетипизированных полей RесSizе содержит
длину записи в байтах, а поле Рrivate зарезервировано, но являет-
ся свободным.
Для текстовых файлов BufPtr является указателем на буфер
размером BufSize, BufPоs представляет собой индекс следующего
символа в буфере, который должен быть записан или прочитан, а
BufEnd - счетчик допустимых символов в буфере. Указатели
OpenFunc, InOutFunc, FlushFunc и CloseFunc служат для ссылки на
программы ввода-вывода и используются для управления файлом. В
Главе 14 в разделе под заглавием "Драйверы устройств для тексто-
вых файлов" приводится дополнительная информация по этому вопро-
су.
Процедурные типы
Процедурные типы хранятся в виде двойного слова. При этом в
младшем слове содержится смещение процедуры, а в старшем - базо-
вый сегмент.
Прямой доступ к памяти
В Borland Pascal реализованы три предопределенных массива
Mem, MemW и MemL, которые используются для прямого доступа к па-
мяти. Каждый компонент массива Mem представляет собой байт, каж-
дый компонент массива MemW - слово, а каждый компонент MemL -
значение длинного целого типа (Longint).
Для индексирования массива Mem используется специальный син-
таксис. Два выражения целочисленного типа Word, разделенные запя-
тыми, используются для задания базового сегмента и смещения ячей-
ки памяти, к которой производится доступ. Например:
Mem[$0040:$0049] := 7;
Data := MemW[Seg(V):Ofs(V)];
MemLong := MemL[64:3*4];
Первый оператор записывает значение 7 в байт по адресу
$0040:$0049. Второй оператор помещает значение типа Word, запи-
санное в первые 2 байта переменной V, в переменную Data. Третий
оператор помещает значение типа Longint, записанное по адресу
$0040:$000C, в переменную MemLong.
Прямой доступ к портам
Для доступа к портам данных процессора 80х86 Borland Pascal
реализует два предопределенных массива - Port и PortW. Оба эти
массива являются одномерными массивами, где каждый элемент предс-
тавляет порт данных, адрес которого соответствует индексу. Индекс
имеет тип Word. Элементы массива Port имеют типа Byte, а элементы
массива PortW - Word.
Когда элементами массива Port или PortW присваивается значе-
ние, оно выводится в выбранный порт. Когда на элементы этих мас-
сивов имеются ссылки в выражениях, то значение вводится из задан-
ного порта.
Использование массивов Port и PortW ограничено только прис-
ваиванием и ссылками в выражениях, то есть элементы этих массивов
не могут использоваться в качестве параметров-переменных. Кроме
того, ссылки на весь массив Port или PortW (без индекса) не до-
пускаются.
Назад | Содержание | Вперед