|
Часть 19
ГЛАВА 17.
ВНУТРЕННИЙ ФОРМАТ ОБЪЕКТОВ.
Внутренний формат объекта похож на запись. Поля объекта
запоминаются в порядке объявления, как непрерывная
последовательность переменных. Любые поля, унаследованные от типа
предка запоминаются до новых полей, определенных в порожденном
типе.
Если объектный тип определяет виртуальные методы, констракторы
или дестракторы, компилятор распределяет дополнительное поле в
объектном типе. Это 16-ти битовое поле, называемое полем таблицы
виртуального метода (VMT) и используется для запоминания смещения
VMT этого объектного типа в сегменте данных. Поле VMT следует
непосредственно после обычных полей в объектном типе. Когда
объектный тип наследует виртуальные методы, констракторы или
дестракторы, он так же наследует поле VMT, при этом дополнительное
поле не распределяется.
Инициализация поля VMT экземпляра производится констрактором
объектного типа. Программа никогда явно не инициализирует и не
обращается к полю VMT.
Следующие примеры иллюстрируют внутренний формат объектных
типов.
type
LocationPtr = ^Location;
Location = object
X, Y: Integer;
procedure Init(PX, PY: Integer);
function GetX: Integer;
function GetY: Integer;
end;
PointPtr = ^Point;
Point = object(Location)
Color: Integer;
constructor Init(PX, PY, PColor: Integer);
destructor Done; virtual;
procedure Show; virtual;
procedure Hide; virtual;
procedure MoveTo(PX, PY: Integer);
end;
CirclePtr = ^Circle;
Circle = object(Point)
Radius: Integer;
constructor Init(PX, PY: Integer;
PColor, PRadius: Integer);
procedure Show; virtual;
procedure Hide; virtual;
procedure Fill; virtual;
end;
Рис. 17.1 показывает слои экземпляров Location, Point и
Circle; каждый прямоугольник соответствует одному слову памяти.
Рис. 17.1 Слои экземпляров Location, Point и Circle.
Location Point Circle
ЪДДДДДДДДДДї ЪДДДДДДДДДДДї ЪДДДДДДДДДДДї
і X і і X і і X і
ГДДДДДДДДДДґ ГДДДДДДДДДДДґ ГДДДДДДДДДДДґ
і Y і і Y і і Y і
АДДДДДДДДДДЩ ГДДДДДДДДДДДґ ГДДДДДДДДДДДґ
і Color і і Color і
ГДДДДДДДДДДДґ ГДДДДДДДДДДДґ
і VMT і і VMT і
АДДДДДДДДДДДЩ ГДДДДДДДДДДДґ
і Radius і
АДДДДДДДДДДДЩ
Поскольку Point является первым типом в иерархии, который
вводит виртуальные методы, поле VMT распределяется после поля
Color.
Таблицы виртуальных методов.
Каждый объектный тип, который содержит или наследует
виртуальные методы, констракторы или дестракторы, имеет
соответствующую ему VMT, которая хранится в инициализированной
части сегмента данных программы. Существует только одна VMT для
объектного типа (а не для экземпляра), но два различных объектных
типа никогда не разделяют VMT, как бы похожи они не были. VMT
строится компилятором автоматически и никогда прямо не
обрабатывается программой. Кроме того, указатели на VMT
автоматически запоминаются в экземпляре объектного типа
констрактором этого объектного типа и никогда не обрабатываются
программой.
Первое слово VMT содержит размер экземпляров, ассоциированных
с объектным типом; эта информация используется констракторами и
дестракторами для определения сколько байт нужно распределить или
освободить, используя расширенный синтаксис New и Dispose.
Второе слово VMT содержит отрицательный размер экземпляров,
связанных с объектным типом; эта информация используется механизмом
верификации вызовов виртуальных методов для определения
неинициализированных объектов (экземпляров, для которых не было
вызова констрактора) и для проверки целостности VMT. Когда механизм
верификации виртуальных вызовов включен (используя директиву
компилятора {$R+}, которая расширена для включения проверки
виртуальных методов), компилятор генерирует вызов программы
верификации VMT перед каждым виртуальным вызовом. Программа
верификации VMT проверяет, что первое слово VMT не равно 0 и что
сумма первого и второго слов = 0. Если какая-то из проверок не
проходит, генерируется ошибка времени выполнения 210.
Примечание: Включение проверки на диапазон и верификации
вызовов виртуальных методов замедляет Вашу программу и делает ее
значительно больше, поэтому используйте состояние {$R+} только во
время отладки и переходите в состояние {$R-} для окончательной
версии программы.
И наконец, начиная со смещения 4 в VMT находится список 32-х
битных указателей на метод, по одному на виртуальный метод в
объектном типе, в порядке объявления. Каждое поле содержит адрес
точки входа соответствующего виртуального метода.
Рис.17.2 показывает слои VMT для типов Point и Circle (Тип
Location не имеет VMT так как не содержит виртуальных методов,
констракторов и дестракторов); каждый маленький прямоугольник
соответствует одному слову памяти, а каждый большой - двум.
Рис.17.2 Слои VMT для Point и Circle.
Point VMT Circle VMT
ЪДДДДДДДДДДДДДДДї ЪДДДДДДДДДДДДДДДДї
і $0008 і і $000A і
ГДДДДДДДДДДДДДДДґ ГДДДДДДДДДДДДДДДДґ
і $FFF8 і і $FFF6 і
ГДДДДДДДДДДДДДДДґ ГДДДДДДДДДДДДДДДДґ
і @Point.Done і і @Point.Done і
ГДДДДДДДДДДДДДДДґ ГДДДДДДДДДДДДДДДДґ
і @Point.Show і і @Circle.Show і
ГДДДДДДДДДДДДДДДґ ГДДДДДДДДДДДДДДДДґ
і @Point.Hide і і @Circle.Hide і
ГДДДДДДДДДДДДДДДґ ГДДДДДДДДДДДДДДДДґ
і @Point.MoveTo і і @Point.MoveTo і
АДДДДДДДДДДДДДДДЩ ГДДДДДДДДДДДДДДДДґ
і @Circle.Fill і
АДДДДДДДДДДДДДДДДЩ
Заметьте, как Circle наследует методы MoveTo и Down из Point и
как он перекрывает методы Show и Hide.
Как упомянуто ранее, констрактор объектного типа содержит
специальный код, который сохраняет смещение VMT объектного типа в
инициализируемом экземпляре. Например: пусть дан экземпляр Р типа
Point и экземпляр C типа Circle, тогда вызов P.Init будет
автоматически сохранять смещение VMT для Point в поле VMT
экземпляра P и вызов C.Init будет сохранять смещение VMT для Circle
в поле VMT экземпляра C. Эта автоматическая инициализация является
частью входного кода констрактора так, что в тот момент, когда
управление достигает оператора begin констрактора, поле Self VMT
уже установлено. Поэтому, если возникла необходимость, констрактор
может сделать вызов виртуального метода.
Стандартная функция SizeOf.
Когда стандартная функция SizeOf применяется к экземпляру
объектного типа, имеющего VMT, она возвращает размер, хранящийся в
этой VMT. Поэтому, для объектных типов, имеющих VMT, SizeOf всегда
возвращает не объявленный, а действительный размер экземпляра.
Стандартная функция TypeOf.
Turbo Pascal добавляет новую стандартную функцию TypeOf,
которая возвращает указатель на VMT объектного типа. TypeOf имеет
единственный параметр, который может быть либо идентификатором
объектного типа, либо экземпляром объектного типа. В обоих случаях
результат типа Pointer является указателем на VMT объектного типа.
TypeOf может быть применен только к объектным типам, имеющим VMT -
для всех других типов это будет ошибкой.
Функция TypeOf может быть использована для проверки
действительного типа экземпляра. Например:
if TypeOf(Self) = ТypeOf(Point) then ...
Вызовы виртуальных методов.
Чтобы вызвать виртуальный метод, компилятор генерирует код,
который указывает на адрес VMT из поля VMT в объекте, и затем
вызывает через поле, связанное с этим методом. Например: для
переменной PP типа PointPtr вызов PP^.Show генерирует следующий
код:
les di,PP ; Загрузить PP в ES:DI
push es ; передать как Self параметр
push di
mov di,es:[di + 6] ; отметить смещение VMT из поля VMT
call DWORD PTR [di+8] ; вызвать точку входа в VMT для Show
Правило совместимости для объектных типов позволяет PP
указывать на Point или Circle, или на любой другой тип, порожденный
от Point. И если Вы проверите VMT, показанную здесь, Вы увидите,
что для Point точка в VMT со смещением 8 указывает на Point.Show, в
то время, как для Circle она указывает на Circle.Show. Таким
образом, в зависимости от актуального (времени выполнения) типа PP,
инструкция CALL вызывает Point.Show или Circle.Show, или метод Show
любого другого типа, порожденного от Point.
Если Show - статический метод, то для вызова PP^.Show будет
генерироваться код:
les di,PP ; Загрузить PP в ES:DI
push es ; передать как Self параметр
push di
call Point.Show ; прямой вызов Point.Show
Здесь вне зависимости от того, на что указывает PP, код будет
всегда будет вызывать метод Point.Show.
Соглашение о вызовах методов.
Методы используют такие же соглашения о вызовах, как обычные
процедуры и функции, за исключением того, что каждый метод имеет
дополнительный неявный параметр, называемый Self, который
соответствует var - параметру того же типа, как объектный тип
метода. Параметр Self всегда передается как последний параметр и
всегда имеет форму 32-х битового указателя на экземпляр, через
который метод вызывается. Например, для переменной PP типа
PointPtr, определенный ранее, вызов PP^.MoveTo(10, 20) кодируется:
Mov ax,10 ; загрузить 10 в AX
Push ax ; передать РX как параметр
Mov ax,20 ; загрузить 20 в AX
Push ax ; передать РY как параметр
les di, pp ; Загрузить PP в ES:DI
push es ; передать как Self параметр
push di
mov di,es:[di+6] ; отметить смещение VMT из поля VMT
call DWORD PTR [di+16] ; вызвать точку входа в VMT для
; MoveTo
До возврата метод должен удалить параметр Self из стека так
же, как он должен удалить любые обычные параметры.
Метод всегда использует дальнюю модель вызова (FAR CALL) вне
зависимости от установления директивы компилятора $F.
Констракторы и дестракторы.
Констракторы и дестракторы используют те же соглашения о
вызовах, что и обычные методы, за исключением того, что
дополнительный параметр, размером в слово, называемый
VMT-параметром, передается в стек перед параметром Self.
Для констракторов параметр VMT содержит смещение VMT,
принадлежащее Self, для того, чтобы инициализировать этот Self.
Когда констрактор вызывается для распределения динамических
объектов, используя расширенный синтаксис New, указатель nil
передается в параметре Self. Это заставляет констрактор
распределять новый динамический объект, адрес которого передается
назад в DX:AX при возврате из констрактора. Если констрактор не
может распределить объект, указатель nil возвращается в DX:AX (см.
"Восстановление ошибок констрактора" в следующем разделе).
Наконец, когда констрактор вызывается, используя
уточненный идентификатор метода (т.е. идентификатор объектного
типа), с последующей точкой и идентификатором метода, значение ноль
передается в параметре VMT. Это указывает констрактору, что он не
должен инициализировать поле VMT для Self.
Для дестрактора ноль в параметре VMT указывает о нормальном
вызове, и ненулевое значение указывает, что дестрактор был вызван,
используя расширенный синтаксис Dispose. Это заставляет дестрактор
освобождать Self сразу перед возвратом (размер Self определяется
просмотром первого слова VMT для этого Self).
Расширения для New и Dispose.
Стандартные процедуры New и Dispose расширены для
использования вызова констрактора или дестрактора как второго
параметра, для распределения или освобождения динамических
переменных объектного типа. Синтаксис:
New(P, Construct);
и
Dispose(P, Destruct);
где P - переменная указатель, указывающий на объектный тип, а
Construct и Destruct вызывают констракторы и дестракторы для этого
объектного типа. Для New действие расширенного синтаксиса
равносильно выполнению
New(P);
P^.Construct;
и для Dispose
P^.Destruct;
Dispose(P);
Без расширенного синтаксиса появление таких пар вызова New с
последующим вызовом констрактора и вызова дестрактора с последующим
вызовом Dispose будут слишком частыми. Расширенный синтаксис
улучшает читабельность, а так же генерирует более короткий и
эффективный код. Следующее иллюстрирует использование расширенного
синтаксиса New и Dispose.
var
SP: StrFieldPtr;
ZP: ZipFieldPtr;
begin
New(SP, Init(1, 1, 25, 'FirstName'));
New(ZP, Init(1, 2, 5, 'Zip Code', 0, 99999));
SP^.Edit;
ZP^.Edit;
...
Dispose(ZP, Down);
Dispose(SP, Down);
end;
Дополнительное расширение позволяет использовать New как
функцию, которая распределят и возвращает динамическую переменную
указанного типа. Синтаксис :
New(T);
или
New(T, Construct);
В первой форме Т может быть указателем на любой тип. Во второй
- Т должно указывать на объектный тип и Construct должен быть
вызван для создания этого объектного типа. В обоих случаях функция
возвращает результат Т.
Пример :
var
F1, F2: FieldPtr;
begin
F1 := New(StrFieldPtr, Init(1, 1, 25, 'FirstName'));
F2 := New(ZipFieldPtr, Init(1, 2, 5, 'Zip Code', 0, 99999));
...
Writeln(F1^.GetStr); {вызов StrField.GetStr}
Writeln(F2^.GetStr); {вызов ZipField.GetStr}
...
Dispose(F2, Down); {вызов Field.Down}
Dispose(F1, Down); {вызов StrField.Down}
end;
Заметим, что даже, хотя F1 и F2 типа FieldPtr, расширенные
правила совместимости по присваиванию указателей позволяют F1 и F2
присваивать указатели на любой порожденный тип от Field; и
поскольку GetStr и Down являются виртуальными методами, механизм
диспетчеризации виртуальных методов будет корректно вызывать
StrField.GetStr, ZipField.GetStr, Field.Down и StrField.Down
соответственно.
Методы на ассемблере.
Методы, реализованные на языке ассемблера могут быть включены
в программу на Turbo Pascal, используя директиву компилятора $L и
ключевое слово External. Объявление внешнего метода в объектном
типе не отличается от обычного метода; однако, реализация метода
состоит только из заголовка метода с последующим зарезервированным
словом External.
В исходном тексте на ассемблере, @ используется вместо
точки при написании уточненного идентификатора (т.к. точка имеет
другое значение в ассемблере и не может быть частью
идентификатора). Например, идентификатор Паскаля Rect.Init пишется
в ассемблере как Rect@Init. Синтаксис с @ может быть использован
для объявления и PUBLIC и EXTRN идентификаторов.
В качестве примера метода на ассемблере мы реализуем простой
объект Rect.
Type
Rect = Object
X1, X2, Y1, Y2: Integer;
procedure Init(XA, YA, XB, YB: Integer);
procedure Union(var R: Rect);
function Contains(X, Y: Integer): Boolean;
end;
Rect представляет прямоугольник, ограниченный четырьмя
координатами X1, Y1, X2, Y2. Верхний левый угол прямоугольника
определяется X1, Y1 и нижний правый - X2, Y2. Метод Init
присваивает значение координатам прямоугольника; метод Union
вычисляет наименьший прямоугольник, который содержит и этот
прямоугольник, и другой прямоугольник; метод Contains возвращает
True, если данная точка лежит внутри прямоугольника, и False - если
нет. Другие методы, такие как перемещение, масштабирование,
вычисление пересечений и тестирование на равенство могут быть легко
реализованы, чтобы сделать Rect более полезным объектом.
Реализация методов для Rect на Паскале только перечисляет
заголовки этих методов с ключевым словом External.
{$L Rect}
procedure Rect.Init(XA, YA, XB, YB : Integer); External;
procedure Rect.Union(var R : Rect); External;
function Rect.Contains(X, Y : Integer) : Boolean; External;
Разумеется, нет никаких требований, чтобы все методы были
реализованы как внешние. Любой метод может быть реализован либо на
Паскале, либо на Ассемблере, по необходимости.
Исходный текст программы на ассемблере Rect.Asm, который
реализует эти три внешних метода:
TITLE Rect
LOCALS @@
; Rect Structure
Rect STRUC
X1 DW ?
Y1 DW ?
X2 DW ?
Y2 DW ?
Rect ENDS
code SEGMENT BYTE PUBLIC
ASSUME cs:code
; procedure Rect.Init(XA, YA, XB, YB: Integer)
PUBLIC Rect@Init
Rect@Init PROC FAR
@XA EQU (WORD PTR[bp+16])
@YA EQU (WORD PTR[bp+14])
@XB EQU (WORD PTR[bp+12])
@YB EQU (WORD PTR[bp+10])
@Self EQU (DWORD PTR[bp+6])
Push bp ; сохранить bp
Mov bp,sp ; установить размер стека
les di,@Self ; загрузить Self в ES:DI
cld ; сдвинуть вперед
mov ax,@XA ; X1 := XA
stosw
mov ax,@YA ; Y1 := YA
stosw
mov ax,@XB ; X2 := XB
stosw
mov ax,@YB ; Y2 := YB
stosw
pop bp ; восстановить bp
ret 12 ; выгрузить параметры и возврат
Rect@Init ENDP
; procedure Rect.Union(var R: Rect)
PUBLIC Rect@Union
Rect@Union PROC FAR
@R EQU (DWORD PTR [bp+10])
@Sef EQU (DWORD PTR [bp+6])
push bp ; сохранить bp
mov bp,sp ; установить размер стека
push ds ; сохранить ds
lds si,@R ; загрузить R в DS:SI
les di,@Self ; загрузить Self в DS:SI
cld ; передвинуть вперед
lodsw ; if R.X1 >= X1 goto @@1
scasw
jge @@1
dec di ; X1 := R.X1
dec di
stosw
@@1: lodsw ; if R.Y1 >= Y1 goto @@2
scasw
jge @@2
dec di ; Y1 := R.Y1
dec di
stosw
@@2: lodsw ; if R.X2 <= X2 goto @@3
scasw
jle @@3
dec di ; X2 := R.X2
dec di
stosw
@@3: lodsw ; if R.Y2 <= Y2 goto @@4
scasw
jle @@4
dec di ; Y2 := R.Y2
dec di
stosw
@@4 pop ds ; восстановить ds
pop bp ; восстановить bp
ret 8 ; выгрузить параметры и возврат
Rect@Union ENDP
; function Rect.Contains(X, Y: Integer): Boolean
PUBLIC Rect@Contains
Rect@Contains PROC FAR
@X EQU (WORD PTR[BP+12])
@Y EQU (WORD PTR[BP+10])
@Self EQU (DWORD PTR[BP+6])
push bp ; сохранить bp
mov bp,sp ; установить размер стека
les di,@Self ; загрузить Self в ES:DI
mov al,0 ; возвращает false
mov dx,@X ; if (X
|