
4. Особенности реализации объектов на языке турбо паскаль
Разработанные объекты удобно оформить в виде модуля ТУРБО ПАСКАЛЯ. При этом в интерфейсной секции модуля помещается описание объектов, а в секции реализации - текст самих методов объектов (см. фрагмент 3.6). Можно размещать описание объекта и в секции реализации. Однако в этом случае объектом можно пользоваться только “внутри” данного модуля. По желанию, часть инкапсулированных полей и методов объекта можно объявить скрытыми от пользователя модуля. Для этого необходимо включить в описание объекта стандартную директиву PRIVATE: TYPE Новый объект = object(объект-родитель) PUBLIC Доступные поля; Доступные методы; PRIVATE Закрытые поля; Закрытые методы; END; Все поля и методы, объявленные после директивы PRIVATE, доступны только внутри того модуля, в котором объявлен сам объект. Если Вы попробуете обратиться к закрытому полю или методу извне модуля, то будет выдано сообщение об ошибке: 44 : Field identifier expected. /Нужен идентификатор поля/ Для вызова метода непосредственного предка данного объекта можно не указывать его тип, а использовать специальное зарезервированное слово INHERITED. Например, фрагмент 2.5 можно представить в виде фрагмента 4.1.
Фрагмент 4.2 TYPE RectangleD = object(Rectangle) { объект--прямоугольник с диагоналями } procedure Show; end; procedure RectangleD.Show; { нарисовать прямоугольник } Begin inherited Show; { аналогично Rectangle.Show } Graph.Line(x,y,x1,y1); Graph.Line(x,y1,x1,y); End; |
Рассмотрим особенности работы с объектами, имеющими динамические поля. По умолчанию, когда нет достаточно памяти для распределения динамического экземпляра объектного типа, вызов конструктора, использующий расширенный синтаксис процедуры New, генерирует ошибку времени выполнения с кодом 203: Heap overflow error. /Стек динамических переменных (куча) переполнен/ ТУРБО ПАСКАЛЬ позволяет с помощью переменной процедурного типа HeapError изменить функцию обработки ошибок кучи, которая вызывается, когда монитор кучи не может выполнить запрос на распределение памяти. Переменная HeapError должна указывать на функцию со следующим заголовком: function HeapFunc(Size: word): integer; far; Где директива FAR устанавливает дальнюю модель вызова для функции обработки ошибок. Свою функцию обработки ошибок кучи можно установить, присвоив адрес такой процедуры переменной HeapError: HeapError := @HeapFunc; В зависимости от успешности выделения памяти функция HeapFunc должна возвращать значение 0, 1 или 2. Если HeapFunc возвращает 0, то немедленно возникает ошибка времени выполнения с кодом 203. Если возвращается 1, то вместо аварийного завершения программы процедуры New или GetMem возвращают указатель, равный Nil. Наконец, если возвращается 2, то выделение памяти прошло успешно. Стандартная процедура обработки ошибок, установленная по умолчанию, всегда возвращает 0. При работе с объектами удобна следующая программа обработки ошибок: function HeapFunc(Size: word): Integer; far; Begin HeapFunc:=1; End; Когда эта функция установлена, New и GetMem будут возвращать Nil при невозможности распределить память, не приводя к аварийному завершению программы. Код, который производит распределение и инициализацию поля ТВМ динамического экземпляра объекта, является частью входной последовательности конструктора: когда управление достигает оператора Begin конструктора, экземпляр уже распределен и инициализирован. Если распределение неуспешно и функция обработки ошибок хиппа возвращает 1, конструктор пропускает выполнение операторной части и возвращает указатель Nil. При этом указатель, заданный в процедуре New, вызвавшей конструктор, установится в Nil. Когда управление достигает оператора Begin конструктора, гарантируется, что экземпляр объектного типа был распределен и инициализирован успешно. Однако конструктор сам может распределять динамические переменные для того, чтобы инициализировать поля указателей в экземпляре объекта и эти распределения могут завершиться неудачно. Если это случится, то правильно разработанный конструктор должен сделать ”откат” всех успешно распределенных динамических полей и в конце освободить экземпляр объектного типа так, чтобы возвращаемый результат получил значение Nil. Чтобы сделать такой “откат” возможным ТУРБО ПАСКАЛЬ реализует новую стандартную процедуру Fail, которая не имеет параметров и может быть вызвана только (!) из конструктора. Вызов Fail заставляет конструктор освободить все успешно распределенные динамические поля, освободить сам динамический экземпляр объекта и вернуть в качестве результата Nil для индикации ошибки. Если экземпляр объекта динамический, то признаком неудачного выполнения конструктора может служить равенство Nil значения указателя на данный экземпляр. Для статического экземпляра объекта ТУРБО ПАСКАЛЬ позволяет использовать конструктор в выражении как булевскую функцию: возврат True говорит об успехе, а возврат False Говорит об ошибке (благодаря вызову Fail внутри конструктора). Данная возможность является единственной, с помощью которой можно проверить успешность вызова унаследованного конструктора (фрагмент 4.2).
Фрагмент 4.2 TYPE A = object Fa : ^real; constructor Init(Fac: real); destructor Destroy; virtual; . . . end; B = object(A) { объект B потомок A } Fb : ^real; constructor Init(Fac,Fbc: real); . . . end; constructor A.Init; Begin New(Fa); if Fa=Nil then Fail { если вызов New(Fa) неудачен } else Fa^:=Fac; End; destructor A.Destroy; Begin if Fa<>Nil then begin Dispose(Fa); Fa:=Nil end; End; constructor B.Init; Begin if not A.Init(Fac) then Fail; { при неудачном вызове A.Init } New(Fb); if Fb=Nil then begin A.Destroy; Fail end; { если вызов New(Fb) неудачен } . . . End; |
Обратите внимание, как проверяется правильность работы процедуры New в конструкторах объектов A и B, правильность выполнения конструктора объекта A в конструкторе объекта B. Приведенный фрагмент показывает, что деструктор обычно используется для освобождения всех динамических полей объекта. Если таких полей нет, то деструктор, как правило, пустой. Фрагмент 4.3 иллюстрирует использование рассмотренных выше возможностей для создания более сложного динамического объекта: фигурки человечка, составленной из рассмотренных ранее объектов (точек, линий и окружностей). Обратите внимание, как использование полиморфизма и соглашения об имени деструктора упростило алгоритм освобождения памяти и рисование сложного объекта. Текст модуля Graph_A описан фрагментом 3.6.
Фрагмент 4.3 USES Graph, Graph_A; TYPE { =========== БЛОК ОПИСАНИЯ ТИПОВ ========== } MenPtr = ^Men; Men = object(Point) { ####### человечек ######### } N : word; { число элементов изображения } P : PPtr; { указатель на массив указателей } constructor Init(Xc,Yc: integer; Cc: word); destructor Destroy; virtual; procedure Show; virtual; end; { =========== БЛОК ПРОЦЕДУР И ФУНКЦИЙ ======= } {$F+} function HeapFunc(Size: word): Integer; Begin HeapFunc:=1; { возвращает Nil, если хипп переполнился } End; {$F-} constructor Men.Init; { --- инициализация человечка --- } var i : word; Begin N:=8; P:=Nil; if not Point.Init(Xc,Yc,Cc) then Fail; if MemAvail<4*N then Exit; { выделяем память для P } GetMem(P,4*N); FillChar(P^,4*N,0); { инициализируем элементы изображения } P^[1]:=New(LinePtr,Init(X-3,Y,X,Y-3,Color)); { ноги } P^[2]:=New(LinePtr,Init(X+3,Y,X,Y-3,Color)); P^[3]:=New(LinePtr,Init(X,Y-3,X,Y-7,Color)); { туловище } P^[4]:=New(LinePtr,Init(X-3,Y-5,X+3,Y-5,Color)); { руки } P^[5]:=New(CirclePtr,Init(X,Y-11,4,Color)); { голова } P^[6]:=New(PointPtr,Init(X-2,Y-12,Color)); { глаза } P^[7]:=New(PointPtr,Init(X+2,Y-12,Color)); P^[8]:=New(PointPtr,Init(X,Y-11,Color)); { нос } for i:=1 to N do if P^[i] = nil then { выход с освобождением памяти } begin Write('Нет памяти'); ReadLn; Men.Destroy; Fail; end; End; destructor Men.Destroy; { --- деструктор человечка --- } var i : word; Begin { освобождаем память } if P<>Nil then begin for i:=1 to N do if P^[i]<>Nil then Dispose(P^[i],Destroy); FreeMem(P,4*N); P:=Nil end; End; procedure Men.Show; var i : word; Begin if P<>Nil then for i:=1 to N do if P^[i]<>Nil then P^[i]^.Show; End; CONST N = 335; VAR i : word; Gd,Gm,Mx,My,Xc,Yc : integer; M1,M2,M3 : LongInt; PIC : array [1..N] of Men; BEGIN HeapError:=@HeapFunc; { установка управления ошибками хиппа } Gd:=Detect; { инициализируем графический режим } InitGraph(Gd,Gm,'D:\TP\BGI'); M1:=MemAvail; Randomize; { включили генератор случайных чисел } Mx:=GetMaxX; My:=GetMaxY; { пределы изменения координат } M2:=M1-MemAvail; PIC[1].Init(100,100,15); for i:=2 to N do { инициализируем фигуры } begin M3:=MemAvail; Xc:=Random(Mx); Yc:=Random(My-100)+100; PIC[i].Init(Xc,Yc,i); if M3-MemAvail<>M2 then WriteLn('Объект ',i,':выделено ',M3-MemAvail,' байт'); end; { for i:= } M2:=MemAvail; for i:=1 to N do PIC[i].Show; { рисуем фигуру } for i:=1 to N do PIC[i].Destroy; { удаляем фигуры из памяти } M3:=MemAvail; WriteLn('1. Свободной памяти до инициализации :',M1); WriteLn('2. Свободной памяти после инициализации :',M2); WriteLn('3. Свободной памяти после удаления фигур:',M3); WriteLn('4. ',N,' объектов занимали ',M1-M2,' байт памяти'); ReadLn; { ожидание нажатия клавиши Enter } END. |
Рассмотрим вопрос о расположении полей объекта в оперативной памяти. Схема такого расположения для рассмотренных выше объектов приведена на рис.4.1.
|
Point |
Line |
Rectangle |
Circle |
Поле 1 |
X |
X |
X |
X |
Поле 2 |
Y |
Y |
Y |
Y |
Поле 3 |
Color |
Color |
Color |
Color |
Поле 4 |
Адрес ТВМ |
Адрес ТВМ |
Адрес ТВМ |
Адрес ТВМ |
Поле 5 |
|
X1 |
X1 |
R |
Поле 6 |
|
Y1 |
Y1 |
|
Рис. 4.1
Как можно видеть, поле со значением смещения адреса ТВМ у всех потомков располагается после поля Color. Как уже отмечалось выше, ТВМ хранится в сегменте данных программы. Узнать адрес ТВМ объекта можно с помощью функции TypeOf, которая возвращает результат типа pointer, содержащий указатель на ТВМ. Размер ТВМ определяется следующим образом:
SizeTVP = NVM * 4 + 8, Где NVM - число виртуальных методов (включая наследуемые виртуальные методы предков). Структура ТВМ для рассмотренных выше объектов показана на рис. 4.2. Каждое поле ТВМ имеет размер 4 байта. В первом поле хранится размер экземпляра объекта в байтах. Это поле используется процедурами New, Dispose и функцией SizeOf для определения размера объекта. Также это поле используется при вызове виртуальных методов для проверки (верификации) инициализации экземпляра объекта конструктором. Суть проверки состоит в следующем. Если экземпляр объекта был инициализирован конструктором, то поле адреса ТВМ содержит верный адрес ТВМ. При этом начиная с этого адреса следующие 4 байта должны содержать не равный нулю размер объекта. Поэтому, если полученное с помощью адреса ТВМ экземпляра объекта число равно нулю, то делается вывод о том, что вызов виртуального метода выполняется до инициализации данного экземпляра объекта конструктором.
|
Point |
Line |
Rectangle |
Circle |
1 |
Размер |
Размер |
Размер |
Размер |
2 |
- Размер |
- Размер |
- Размер |
- Размер |
3 |
@Point.Done |
@Point.Done |
@Point.Done |
@Point.Done |
4 |
@Point.Show |
@Line.Show |
@Rectangle.Show |
@Circle.Show |
Рис. 4.2.
Действительно, если объект имеет хотя бы одно виртуальное правило или одно поле, то его размер не может быть меньше, чем 1 байт. Во втором поле ТВМ хранится отрицательное значение размера объекта. Это поле также используется для верификации вызова виртуального метода. Если поле адреса ТВМ экземпляра объекта содержит правильный адрес ТВМ (т.е. конструктор вызывался), то сумма первого и второго поля ТВМ должна быть равна 0. Данные две проверки (в первом поле не 0 и сумма 1-го и 2-го полей равна 0) выполняются при каждом вызове виртуального метода, если включен механизм верификации методов (директива $R+ компилятора или опция Range Сhecking интегрированной среды). Поэтому рекомендуется для увеличения скорости выполнения отлаженной программы данную опцию отключать. Если механизм верификации включен, то при невыполнении проверок выдается сообщение об ошибке с кодом 210 и программа снимается. Вызов виртуального метода до вызова конструктора объекта при отключенном механизме верификации, как правило, приводит к “зависанию” программы (в режиме real процессора) или к ошибке защиты памяти (в режиме protection процессора).
|
Контрольные вопросы |
1. Какие сложности могут возникнуть при использовании динамических экземпляров объекта? 2. Каково назначение процедуры Fail? 3. Как можно проверить правильность работы конструктора? 4. Каково назначение функции TypeOf? 5. Каким образом расположены поля объекта в оперативной памяти? 6. Где расположена ТВМ? Каково ее строение? 7. В чем заключается механизм верификации методов объекта?
|
Лабораторная работа №4 |
|
1. Набрать текст программы из фрагмента 4.3. Выполнить данный пример, посмотреть и проанализировать результат. 2. Разработать свои сложные геометрические объекты, составленные из простых, имеющихся в модуле Graph_A. 3. Разработать объект МАСШТАБИРУЕМОЕ ОКНО как потомок объекта ОКНО. Масштабируемое окно должно уметь переводить мировые координаты точек в графические, отрисовывать линии, текст. 4. Разработать иерархию МЕНЮ (вертикальное, горизонтальное, блочное). В основе данной иерархии должен лежать объект ОКНО. 5. На основе ООП предложите возможную реализацию игры “Полет к Тау Кита”. В прямоугольной области случайным образом расположены звезды. Массы звезд выбираются случайно из некоторого интервала. Точка старта космического корабля расположена в левом нижнем углу области, а звезда Тау Кита - в правом верхнем. Корабль имеет некоторый запас топлива и может по желанию включать двигатель, ориентируя его произвольным образом в плоскости полета. Звезды считать неподвижными. Корабль в соответствии с законом всемирного тяготения притягивается к звездам. В игре требуется так управлять кораблем, чтобы попасть на звезду Тау Кита.