Обнаружение ошибок конструктора

manual


Часть 19

Глава 17. Объекты
Внутренние форматы данных объектов
Внутренний формат данных объекта имеет сходство с внутренним форматом записи. Поля объекта записываются в порядке их описаний как непрерывная последовательность переменных. Любое поле, унаследованное от родительского (порождающего) типа, записывается пер
ед новыми полями, определенными в производном (порожденном) типе.
     Если объектный тип определяет виртуальные методы, конструктор или деструктор, то компилятор размещает в объектном типе дополнительное поле данных. Это 16-битовое поле, называемое полем таблицы виртуальных методов (ТВМ), используется для запоминания 
смещения таблицы виртуальных методов в сегменте данных. Поле таблицы виртуальных методов следует непосредственно после обычных полей объектного типа. Если объектный тип наследует виртуальные методы, конструкторы или деструкторы, то он также наследует и п
оле ТВМ, благодаря чему дополнительное поле таблицы виртуальных методов не размещается. 
     Инициализация поля таблицы виртуальных методов экземпляра объекта осуществляется конструктором (или конструкторами) объектного типа. Программа никогда не инициализирует поле таблицы виртуальных методов явно и не имеет к нему доступа. 
     Следующие примеры иллюстрируют внутренние форматы данных объектных типов. 
     type
     TLocationPtr = ^Location;

     PLocation = object
        X,Y: integer;
        procedure Init(PX, PY: integer);
        function GetX: integer;
        function GetY: integer;
     end;

     PPointPtr = ^Point;

     TPoint = object(Location)
        Color: integer;
        constructor Init(PX, PY, PColor: integer);
        destructor Done; virtual;
        procedure Show; virtual;
        procedure Hide; virtual;
        procedure MoveTo(PX, PY: integer); virtual;
     end;

     PCirclePtr = ^Circle;

     TCircle = object(Point)
        Radius: integer;
        constructor Init(PX, PY, PColor, PRadius: integer);
        procedure Show; virtual;
        procedure Hide; virtual;
        procedure Fill; virtual;
     end;

     Рисунок 17.1 показывает размещение экземпляров типов TLocation, TPoint и TCircle: каждый прямоугольник соответствует одному слову памяти. 
     TLocation               TPoint                   TCircle
     ЪДДДДДДДДДДДї          ЪДДДДДДДДДДДї           ЪДДДДДДДДДДДї
     і X         і          і X         і           і X         і
     ГДДДДДДДДДДДґ          ГДДДДДДДДДДДґ           ГДДДДДДДДДДДґ
     і Y         і          і Y         і           і Y         і
     АДДДДДДДДДДДЩ          ГДДДДДДДДДДДґ           ГДДДДДДДДДДДґ
                            і Color     і           і Color     і
                            ГДДДДДДДДДДДґ           ГДДДДДДДДДДДґ
                            і NDV       і           і NDV       і
                            АДДДДДДДДДДДЩ           ГДДДДДДДДДДДґ
                                                    і Radius    і
                                                    АДДДДДДДДДДДЩ

     Рис. 17.1 Схема экземпляров типов TLocation, TPoint и TCircle

                   Таблица виртуальных методов

     Каждый объектный тип, содержащий или наследующий виртуальные методы, конструкторы или деструкторы, имеет связанную с ним таблицу виртуальных методов, в которой запоминается инициализируемая часть сегмента данных программы. Для каждого объектного тип
а (но не для каждого экземпляра) имеется только одна таблица виртуальных методов, однако два различных объектных типа никогда не разделяют одну таблицу виртуальных методов, независимо от того, насколько эти типы идентичны. Таблицы виртуальных методов соз
даются автоматически компилятором, и программа никогда не манипулирует ими непосредственно. Аналогично, указатели на таблицы виртуальных методов автоматически запоминаются в экземплярах объектных типов с помощью конструкторов, программа никогда не работа
ет с этими указателями непосредственно. 
      Первое слово таблицы виртуальных методов содержит размер экземпляров соответствующего объектного типа. Эта информация используется конструкторами и деструкторами для определения того, сколько байт выделяется или освождается при использовании расшир
енного синтаксиса стандартных процедур New и Dispose. 
     Второе слово таблицы виртуальных методов содержит отрицательный размер экземпляров соответствующего объектного типа эта информация используется ратификационным (т.е. подтверждающим действительность) механизмом вызова виртуального метода для выявлени
я инициализируемых объектов (экземпляров, для которых должен выполняться конструктор) и для проверки согласованности таблицы виртуальных методов. Когда разрешена ратификация виртуального вызова (с помощью директивы {$R+} компилятора, которая расширена и 
включает в себя проверку виртуальных методов), компилятор генерирует вызов программы проверки допустимости таблицы виртуальных методов перед каждым вызовом виртуального метода. Программа ратификации таблицы виртуальных методов проверяет, что первое слово
 таблицы виртуальных методов не равно нулю и что что сумма первого и второго слов равна нулю. Если любая из проверок неудачна, то генерируется ошибка 210 исполняющей системы Турбо Паскаля. 
     Разрешение проверок границ диапазонов и проверок вызовов виртуальных методов замедляет выполнение программы и делает ее несколько больше, поэтому используйте {$R+} только во время отладки и переключите эту директиву в состояние {$R-} в окончательной
 версии программы. 
     Третье слово ТВМ содержит смещение сегмента данных объектного типа в таблице динамических методов (ТДМ), или 0, если объект не содержит динамических методов. 
     Четвертое слово ТВМ резервируется и всегда равно 0.

     Наконец, начиная со смещения 8 таблицы виртуальных методов следует список 32-разрядных указателей методов (один указатель на каждый виртуальный метод в порядке их описаний). Каждая позиция содержит адрес точки входа соответствующего виртуального мет
ода. 
     На Рис. 17.2 показано размещение таблиц виртуальных методов типов TPoint и TCircle (тип TLocation не имеет таблицы виртуальных методов, т.к. не содержит в себе виртуальных методов, конструкторов и деструкторов): каждый маленький прямоугольник соотве
тствует одному слову памяти, а каждый большой прямоугольник - двум словам памяти. 
           ТВМ TPoint                        ТВМ TCircle
           ЪДДДДДДДДДДДДДДДДДДї              ЪДДДДДДДДДДДДДДДДДДї
           і $0008            і              і $000A            і
           ГДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і $FFFB            і              і $FFF6            і
           ГДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і                  і              і                  і
           і @TPoint.Done     і              і @TPoint.Done     і
           ГДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і                  і              і                  і
           і @TPoint.Show     і              і @TCircle.Show    і
           ГДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і                  і              і                  і
           і @TPoint.Hide     і              і @TCircle.Hide    і
           ГДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і                  і              і                  і
           і @TPoint.MoveTo   і              і @TPoint.MoveTo   і
           АДДДДДДДДДДДДДДДДДДЩ              ГДДДДДДДДДДДДДДДДДДґ
                                             і                  і
                                             і @Circle.Fill     і
                                             АДДДДДДДДДДДДДДДДДДЩ

     Рис. 17.2 Схемы таблиц виртуальных методов для TPoint и TCircle 
     Обратите внимание на то, как Circle наследует методы Done и MoveTo типа Point и как он переопределяет Show и Hide. 
     Как уже упоминалось, конструкторы объектных типов содержат специальный код, который запоминает смещение таблицы виртуальных методов объектного типа в инициализируемых экземплярах. Например, если имеется экземпляр P типа TPoint и экземпляр C типа TCi
rcle, то вызов P.Init будет автоматически записывать смещение таблицы виртуальных методов типа TPoint в поле таблицы виртуальных методов экземпляра P, а вызов C.Init точно так же запишет смещение таблицы виртуальных методов типа TCircle в поле таблицы ви
ртуальных методов экземпляра C. Эта автоматическая инициализация является частью кода входа конструктора, поэтому если управление передается в начало операторной секции, то поле Self таблицы виртуальных методов также будет установлено. Таким образом, при
 возникновении необходимости, конструктор может выполнить вызов виртуального метода. 
               Таблица динамических методов

     Таблица вирутальных методов (ТДМ) объектного типа содержит для каждого описанного в объектном типе виртуального метода и его предков четырехбайтовую запись. В тех случаях, когда в порождающих типах (предках) определяется большее число виртуальных ме
тодов, в процессе создания производных типов может использоваться достаточно большой объем памяти, особенно если создается много производных типов. Хотя в производных типах могут переопределяться только некоторые из наследуемых методов, таблица вирутальн
ых методов каждого производного типа содержит указатели метода для всех наследуемых виртуальных методов, даже если они не изменялись. 
     Динамические методы обеспечивают в таких ситуациях альтернативу. В Турбо Паскале для Windows вводится новый формат таблицы методов и новый способ диспетчеризации методов с поздним связыванием. Вместо кодирования для всех методов объектного типа с по
здним связыванием, в таблице динамических методов кодируются только те методы, которые были в объектном типе переопределены. Если в наследующих типах переопределяются только некоторые из большого числа методов с поздним связыванием, формат таблицы динами
ческих методов использует меньшее пространство, чем формат таблицы вирутальных методов. 
     Формат таблицы динамических методов иллюструют следующие два объектных типа: 
     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;

     На Рис. 17.3 и 17.4 показаны схемы таблицы вирутальных методов и таблицы динамических методов для TBase и TDerived. Каждая ячейка соответствует слову памяти, а каждая большая ячейка - двум словам памяти. 
           ТВМ TBase                         ТДМ TBase
           ЪДДДДДДДДДДДДДДДДДДї              ЪДДДДДДДДДДДДДДДДДДї
           і 4                і              і 0                і
           ГДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і -4               і              і индекс в кеше    і
           ГДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і Смещ. ТДМ TBase  і              і смещение записи  і
           ГДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і 0                і              і 4                і
           ГДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і                  і              і 10               і
           і @TBase.Done      і              ГДДДДДДДДДДДДДДДДДДґ
           і                  і              і 20               і
           АДДДДДДДДДДДДДДДДДДЩ              ГДДДДДДДДДДДДДДДДДДґ
                                             і 30               і
                                             ГДДДДДДДДДДДДДДДДДДґ
                                             і 40               і
                                             ГДДДДДДДДДДДДДДДДДДґ
                                             і                  і
                                             і @TBase.P10       і
                                             і                  і
                                             ГДДДДДДДДДДДДДДДДДДґ
                                             і                  і
                                             і @TBase.P20       і
                                             і                  і
                                             ГДДДДДДДДДДДДДДДДДДґ
                                             і                  і
                                             і @TBase.P30       і
                                             і                  і
                                             ГДДДДДДДДДДДДДДДДДДґ
                                             і                  і
                                             і @TBase.P40       і
                                             і                  і
                                             АДДДДДДДДДДДДДДДДДДЩ

     Рис. 17.3 Схемы таблицы вирутальных методов и таблицы динамических методов для TBase 
     Объектный тип имеет таблицу динамических методов только в том случае, если в нем вводятся или переопределяются динамические методы. Если объектный тип наследует динамические методы, но они не переопределяются, и новые динамические методы не вводятся
, то он просто наследует таблицу динамических методов своего предка. 
     Как и в случае таблицы вирутальных методов, таблица динамических методов записывается в инициализированную часть сегмента данных прикладной программы. 
           ТВМ TDerived                         ТДМ TDerived
           ЪДДДДДДДДДДДДДДДДДДДї              ЪДДДДДДДДДДДДДДДДДДї
           і 6                 і              і Смещ. ТДМ TBase  і
           ГДДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і -6                і              і индекс в кеше    і
           ГДДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і Смещ. ТДМ TDerivedі              і смещение записи  і
           ГДДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і 0                 і              і 3                і
           ГДДДДДДДДДДДДДДДДДДДґ              ГДДДДДДДДДДДДДДДДДДґ
           і                   і              і 10               і
           і @TBase.Done       і              ГДДДДДДДДДДДДДДДДДДґ
           і                   і              і 30               і
           АДДДДДДДДДДДДДДДДДДДЩ              ГДДДДДДДДДДДДДДДДДДґ
                                              і 50               і
                                              ГДДДДДДДДДДДДДДДДДДґ
                                              і                  і
                                              і @TDerived.P10    і
                                              і                  і
                                              ГДДДДДДДДДДДДДДДДДДґ
                                              і                  і
                                              і @TDerived.P30    і
                                              і                  і
                                              ГДДДДДДДДДДДДДДДДДДґ
                                              і                  і
                                              і @TDerived.T50    і
                                              і                  і
                                              АДДДДДДДДДДДДДДДДДДЩ

     Рис. 17.4 Схемы таблицы вирутальных методов и таблицы динамических методов для TDerived 
     Первое слово таблицы динамических методов содержит смещение сегмента данных родительской таблицы динамических методов, или 0, если родительская таблица динамических методов отсутствует. 
     Второе и третье слово таблицы динамических методов используется в кеш-буфере просмотра динамических методов (см. далее). 
     Четвертое слово таблицы динамических методов содержит счетчик записи таблицы динамических методов. Непосредственно за ним следует список слов, каждое из которых содержит индекс динамического метода, а затем список соответствующих указателей методов.
 Длина каждого списка задается счетчиком записи таблицы динамических методов. 
                      Функция SizeOf

     Будучи примененной к экземпляру объектного типа, имеющего таблицу виртуальных методов, стандартная функция SizeOf возвратит записанный в таблице виртуальных методов размер. Таким образом, для объектов, имеющих таблицу виртуальных методов, функция Si
zeOf всегда возвращает действительный размер экземпляра, а не описанный его размер. 
                      Функция TypeOf

     Турбо Паскаль предоставляет новую стандартную функцию TypeOf, которая возвращает указатель на таблицу виртуальных методов объектного типа. Функция TypeOf принимает единственный параметр, который может быть либо идентификатором объектного типа либо э
кземпляром объектного типа. В обоих случаях результат типа pointer является указателем на таблицу виртуальных методов объектного типа. TypeOf может применяться только к объектным типам, имеющим таблицы виртуальных методов. Применение этой функции к други
м типам приведет к ошибке. 
     Функция TypeOf может использоваться для проверки фактического типа экземпляра. Например: 
     if TypeOf(Self) = TypeOf(Point) then ...

                   Вызовы виртуальных методов

     Для вызова виртуального метода компилятор генерирует код, который выбирает адрес таблицы виртуальных методов из поля таблицы виртуальных методов объекта, и затем вызывает метод, используя связанную с ним точку входа. Например, если дана переменная P
P типа PointPtr, то вызов PP^.Show будет генерировать следующий код: 
     les    di, PP             ; загрузить РР в ES:DI
     push   es                 ; передать, как параметр Self
     push   di
     mov    di, es:[di + 6]    ; извлечь смещение ТВМ из поля ТВМ
     call   DWORD PTR [di + 8] ; вызвать запись ТВМ для  Show

     Правила совместимости типов для объектных типов позволяют PP указывать на TPoint и на TCircle или на любых других потомков TPoint. И если вы просмотрите показанные здесь таблицы виртуальных методов, то вы увидите, что для типа TPoint точка входа со 
смещением 8 в таблицы виртуальных методов указывает на TPoint.Show. Таким образом, в зависимости от фактического во время выполнеия типа PP, инструкция CALL вызывает либо TPoint.Show, либо TCircle.Show, либо метод любого другого потомка TPoint. 
     Если Show является статическим методом, то для вызова PP^.Show будет генерироваться следующий код: 
     les    di, PP        ; загрузить PP в ES:DI
     push   es            ; передать, как параметр Self
     push   di
     call   Point.Show    ; непосредственно вызвать TPoint.Show

     В данном случае не имеет значения, на что указывает PP, и код всегда будет вызывать метод TPoint.Show. 
                Вызовы динамических методов

     Диспетчеризация вызова динамического метода несколько более сложна и требует больше времени, чем диспетчеризация виртуального метода. Вместо использования инструкции CALL для вызова через указатель метода по статическому смещению в таблице вирутальн
ых методов, таблица динамических методов объетного типа и таблица динамических методов его предка должны просматриваться в поиске "самого верхнего" вхождения индекса конкретного динамического метода, а вызов затем должен выполняться через соответствующий
 указатель метода. Этот процесс требует использования существенно большего числа инструкций, которые можно записать, как "встроенные" (inline), поэтому Турбо Паскаль обеспечивает подпрограмму диспетчеризации, используемую при вызове динамического метода.

     Если бы метод Show показанного выше типа TPoint описывался как динамический метод (с индексом динамического метода 200), то вызов PP^.Show, где PP имеет тип TPointPtr, привел бы к генерации следующего кода: 
     les      di,PP            ; загрузка PP в ED:DI
     push     es               ; передача, как параметра
                               ; Self
     push     di
     mow      di,es:[di+6]     ; извлечение смещения
                               ; таблицы вирутальных методов
                               ; из поля таблицы
                               ; вирутальных методов
     mov      ax,200           ; загрузка в AX индекса
                               ; динамического метода
     call     Dispatch         ; вызов подпрограммы
                               ; диспетчеризации

     Диспетчер выбирает сначала смещение таблицы динамических методов от таблицы вирутальных методов, на которое указывает регистр DI. Затем используется "индекс в кеше" - поле таблицы динамических методов. Диспетчер проверяет, является ли индекс вызванн
ого динамического метода индексом того динамического метода, который вызывался последним. Если это так, он немедленно передает этому методу управление (путем перехода с помощью указателя метода, записанного по смещению, заданному полем "смещение записи")
. Если динамический индекс вызванного метода не совпадает с тем, который записан в кеше, то диспетчер просматривает таблицу динамических методов и родительскую таблицу динамических методов (следуя по связям в таблице динамических методов), пока он не най
дет запись, с данным индексом динамического метода. Индекс и смещение соответствующего указателя метода записываются затем в поле таблицы динамических методов, а управление передается методу. Если по каким-либо причинам диспетчер не может найти запись с 
данным индексом динамического метода, он завершает прикладную программу с кодом ошибки этапа выполнения 210. 
     Вопреки кешированию и высокооптимизированной подпрограмме диспетчеризации, диспетчеризация динамического метода может потребовать существенно больше времени, чем вызов вирутального метода. Однако в тех случаях, когда сами дествия, выполняемые динами
ческим методом, требуют много времени, дополнительное пространство, сохраняемое таблицами динамических методов, может перевесить этот недостаток. 
                  Соглашения о вызовах методов

     Методы используют те же соглашения о вызовах, что и обычные процедуры и функции, за тем исключением, что каждый метод имеет неявный дополнительный параметр Self, который соответствует параметру-переменной того же типа, что и объектный тип данного ме
тода. Параметр Self всегда передается последним и всегда имеет форму 32 -разрядного указателя на экземпляр, из которого вызывается метод. Например, если переменная PP имеет тип TPointPtr, как определено выше, то вызов PP^.MoveTo (10, 20) кодируется следу
ющим образом: 
     mov      ax, 10          ; загрузить 10 в  AX
     push     ax              ; передать PX как параметр
     mov      ax, 20          ; загрузить 20 в AX
     push     ax              ; передать PY как параметр
     les      di, PP          ; загрузить PP в ES:DI
     push     es              ; передать, как параметр Self
     push     di
     mov      di, es:[di + 6] ; извлечь смещение ТВМ из поля ТВМ
     call     DWORD PTR [di + 16] ; вызвать запись ТВМ для MoveTo

     Во время возврата метод должен удалить параметр Self из стека точно так же, как он удаляет обычные параметры. 
     Методы всегда используют дальний тип вызова, независимо от состояния директивы $F компилятора. 
                   Конструкторы и деструкторы

     Конструкторы и деструкторы используют те же соглашения о вызовах, что и обычные методы, за тем исключением, что дополнительный параметр размером в слово, называемый параметром таблицы виртуальных методов, передается через стек непосредственно перед 
параметром Self. 
     Для конструкторов параметр таблицы виртуальных методов содержит смещение таблицы виртуальных методов для запоминания поля Self таблицы виртуального метода, чтобы инициализировать Self. 
     Более того, если конструктор вызывается для размещения динамического объекта с помощью расширенного синтаксиса стандартной процедуры New, через параметр Self передается указатель nil. Это заставляет конструктор размещать новый динамический объект, а
дрес которого передается вызывающей программе через DX:AX при возврате из конструктора. Если конструктор не может разместить объект, то в DX:AX возвращается пустой указатель nil. (См. далее "Восстановление ошибок конструктора"). 
     Наконец, если конструктор вызывается с использованием уточненного идентификатора метода (т.е. идентификатора типа объекта, за которым следуют точка и идентификатор метода), то в параметре таблицы виртуальных методов передается нулевое значение. Это 
является указанием конструктору на то, что ему не следует инициализировать поле Self таблицы виртуальных методов. 
     Для деструкторов нулевое значение параметра таблицы виртуальных методов означает обычный вызов, а ненулевое указывает, что деструктор был вызван с использованием расширенного синтаксиса стандартной процедуры Dispose. Это заставляет деструктор удалит
ь Self непосредственно перед возвратом (размер Self определяется из первого слова Self в ТВМ). 
             Расширения процедур New и Dispose

     Стандартные процедуры New и Dispose расширены таким образом, что допускают использование в качестве второго параметра конструктора или деструктора. Это позвляет создать или уничтожить динамическую переменную объектного типа. Синтаксис вызова этих пр
оцедур следующий: 
     New(P, Construct)
 и
     Dispose(P, Destruct)
 где P - это переменная-указатель, указывающая на объектный тип, а Construct и Destruct представляют собой вызовы конструкторов или деструкторов данного объектного типа. Для процедуры New действие расширенного синтаксиса эквивалентно выполнению следующих
 операторов: 
     New(P);
     P^.Consctruct;

     Для процедуры Dispose действие расширенного синтаксиса эквивалентно выполнению следующих операторов: 
     P^.Destruct;
     Dispose(P);

     Без расширенного синтаксиса вхождение таких пар вызовов New с последующим вызовом конструктора и вызовов десруктора, за которым следует вызов Dispose, было бы достаточно общим. Расширенный синтаксис улучшает читаемость кода и позволяет генеровать бо
лее короткий и эффективный код. 
     Использование расширенного синтаксиса New и Dispose иллюстрирует следующий пример: 
     var
        SP: StrFieldPtr;
        ZP: ZipFieldPtr;
     begin
        New(SP, Init(1, 1, 25, 'Firstname'));
        New(SP, Init(1, 2, 5, 'Zip code', 0, 99999));
        SP^.Edit;
        SP^.Edit;
        ...
     Dispose(ZP, Done);
     Dispose(SP, Done);
     end;

     Дополнительное расширение позволяет использовать New, как функцию, которая распределяет и возвращает дополнительную переменную заданного типа. При этом используется следующий синтаксис: 
     New(T);
 или 
     New(T, Construct);

     В первой форме T может быть любым типом указателей. Во втором случае T должно указывать на объектный тип, а Construct должно быть вызовом конструктора данного объектного типа. В обоих случаях типом результата функции будет T. 
     Приведем пример:

     var
        F1, F2: FieldPtr;
     begin
        F1 := New(StrFieldPtr, Init(1, 1, 25, 'Firstname'));
        F2 := New(SP, Init(1, 2, 5, 'Zip code', 0, 99999));
        ...
        Writeln(F1^.GetStr);      { вызов StrField.GetStr }
        Writeln(F1^.GetStr);      { вызов ZipField.GetStr }
        ...
        Dispose(F2, Done)         { вызов Field.Done }
        Dispose(F1, Done)         { вызов StrField.Done }
     end;

     Обратите внимание, что хотя F1 и F2 имеют тип FieldPtr, расширенные правила совместимости указателей по присваиванию позволяют присваивать F1 и F2 любому потомку Field, и поскольку GetStr и Done являются виртуальными методами, механизм диспетчирован
ия виртуальных методов корректно вызывает StrField.GetStr, ZipField.GetStr, Field. Done и StrField.Done соответственно. 
                   Методы на языке Ассемблера

     Написанные на языке Ассемблера реализации методов могут компоноваться с программами на Турбо Паскале с помощью директивы $L компилятора и зарезервированного слова external. Описание внешнего метода в объектном типе не отличается от описания обычного
 метода, однако, реализация списка методов осуществляется путем приведения только заголовков методов, за каждым из которых следует зарезервированное слово external. 
     В исходном тексте на языке Ассемблера для записи уточненного идентификатора вместо точки (.) используется @ (точка в языке Ассемблера имеет другой смысл и не может быть частью идентификатора). Например, идентификатор Паскаля Rect.Init записывается н
а языке Ассемблера, как Rect@Init. Синтаксис операции @ может использоваться для объявления как общедоступных (PUBLIC), так и для внешних (EXTRN) идентификаторов. 
     В качестве примера методов на языке Ассемблера введем следующий простой объект Rect: 
     type
      Rect = object
        X1, Y1, X2, 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

     Rect  STRUCT
     X1    DW ?
     Y1    DW ?
     X2    DW ?
     Y2    DW ?
     Rect  ENDS

     code  SEGMENT BYTE PUBLIC

     ASSUME cs:code

     ; процедура 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 d 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
       ret12           ; излечь параметры и выполнить возврат

     Rect@Init ENDP

     ; Процедура Rect.Union (var R: Rect)

     PUBLIC    Rect@Union

     Rect@Union PROC FAR

     @R         EQU (DWORD PTR [bp + 10])
     @Self      EQU (DWORDPTR [bp + 6])

       push     bp        ; сохранить bp
       mov      bp, sp    ; установить границу стека
       push     ds        ; сохранить ds
       lds      si, @R    ; загрузить R в DS:SI
       les      di, @Self ; загрузить Self в ES:DI
       cld                ; продвинуться вперед
       lodsw              ; если R.X1 >= X1, перейти к @@1
       scasw
       jge      @@1
       dec      di        ; X1 := R.X1
       dec      di
       stosw
     @@1: lod   sw        ; если  R.Y1 >= Y1, перейти к @@2
       scasw
       jge      @@2
       decdi;Y1 := R.Y1
       decdi
       stos
     @@2: lod   sw        ; если R.X2 <= X2, перейти к @@3
       scasw
       jle      @@3
       decdi              ; X2 := R.X2
       decdi
       stosw
     @@3: lodsw           ; если R.Y2 <= Y2, перейти к @@4
       scasw
       jle      @@4
       decdi              ; Y2 := R.Y2
       decdi
       stosw
     @@4: popds           ; восстановить ds
       pop      bp        ; восстановить bp
       ret8               ; извлечь параметры и выполнить возврат

     Rect@Union ENDP

     ; функция 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    ; если (X < X1) or (X > X2) - перейти
                         ; на @@1
       cmp     dx, es:[di].X1
       jl      @@1
       cmp     dx, es:[di].X2
       jg      @@1
       mov     dx, @Y    ; если (Y < Y1) or (Y > Y2) - на @@1
       cmp     dx, es:[di].Y1
       jl      @@1
       cmp     dx, es:[di].Y2
       jg      @@1
       inc     ax        ; возвратить true
     @@1: pop  bp        ; восстановить bp
       ret8              ; извлечь параметры и выйти

     Rect@ContainsENDP

     code  ENDS

     END

                 Обнаружение ошибок конструктора

     Как описано в Главе 16, Турбо Паскаль позволяет вам установить функцию ошибок динамически распределяемой области памяти посредством переменной HeapError модуля System. Эта функциональность поддерживается и в Турбо Паскале, однако теперь она оказывае
т воздействие на способ работы конструкторов. 
     По умолчанию, если не хватает памяти для размещения динамического экземпляра объектного типа, то вызов конструктора, использующий расширенный синтаксис стандартной процедуры New, генерирует ошибку 203 исполняющей системы. Если вы устанавливаете функ
цию ошибок динамической памяти, которая возвращает 1, а не стандартный результат функции 0, то вызов конструктора через New будет возвращать nil, если конструктор не сможет завершить запрос (вместо прекращения выполнения программы). 
     Код, который выполняет размещение и инициализацию поля таблицы виртуальных методов динамического экземпляра, является частью последовательности точки входа в конструктор. Если управление передается в точку begin операторной части конструктора, то эк
земпляр уже будет размещен и инициализирован. Если размещение завершилось неудачно, и если функция ошибки динамически распределяемой области памяти возвратила 1, то конструктор пропускает выполнение операторной части и возвращает указатель nil. Таким обр
азом, указатель, который задан в расширенной процедуре New, вызывающей конструктор, будет установлен в nil (пусто). 
     Примечание: Имеется новая стандартная процедура Fail.

     Если только управление передается в точку begin операторной части конструктора, то это гарантирует, что экземпляр объектного типа уже успешно размещен и инициализирован. Однако, сам конструктор может попытаться разместить динамическую переменную, чт
обы инициализировать поле указателя экземпляра, и эта попытка может потерпеть неудачу. Если это произойдет, то конструктор с хорошо продуманным поведением должен дать обратный ход успешному размещению и в конце концов удалить размещенный экземпляр объект
ного типа, чтобы конечным результатом явился указатель nil. Чтобы сделать возможным этот обратный ход, Турбо Паскаль предоставляет новую стандартную процедуру Fail, которая не имеет параметров и которая может вызываться только изнутри конструктора. Вызов
 Fail заставляет конструктор удалить динамический экземпляр, который был размещен при входе в конструктор, и ведет к возврату указателя nil для индикации неудачной попытки. 
     Если динамические экземпляры размещаются с помощью расширенного синтаксиса New, то результирующее значение nil специфицированного указателя указывает на неудачную операцию. Несомненно, нет таких переменных типа указатель, которые можно было бы прове
рить после создания статического экземпляра или после вызова наследованного конструктора. Вместо этого, Турбо Паскаль позволяет использовать в выражениях конструкторы как функции, возвращающие результат типа boolean. Возвращаемое значение True означает у
спех, а False - неудачу, благодаря вызову Fail внутри конструктора. 
     Следующая программа предоставляет два простых объектных типа, содержащих указатели. Эта первая версия программы не использует обнаружение ошибок конструктора. 
     type
     LinePtr = ^Line;
     Line = string [79];
     BasePtr = ^Base;
     Base = object
        L1, L2: LinePtr;
        constructor Init(S1, S2: Line);
        destructor Done; virtual;
        procedure Dump; virtual;
     end;

     DerivedPtr = ^Derived;

     Derived = object(base)
     L3, L4: LinePtr;
        constructor Init(S1, S2, S3, S4: Line);
        destructor Done; virtual;
        procedure Dump; virtual;
     end;

     var
     BP: BasePtr;
     DP: DerivedPtr;

     constructor Base.Init(S1, S2: Line);
     begin
        New (L1);
        New (L2);
        L1^ := S1;
        L2^ := S2;
     end;

     destructor Base.Done;
     begin
        Dispose(L2);
        Dispose(L1);
     end;

     procedure Base.Dump;
     begin
        Writeln('B: ', L1^, ', ', L2^, '.');
     end;

     constructor Derived.Init(S1, S2, S3, S4: Line);
     begin
        Base.Init(S1, S2);
        New(L3);
        New(L4);
        L3^ := S3;
        L4^ := S4;
     end;

     destructor Derived.Done;
     begin
        Dispose(L4);
        Dispose(L3);
        Base.Done;
     end;

     procedure Derived.Dump;
     begin
        Writeln('D: ',L1^,', ',L2^,', ',L3^,', ',L4^,'.');
     end;

     begin
        New(BP, Init('Турбо', 'Паскаль');
        New(DP, Init('Север', 'Восток', 'Юг', 'Запад');
        BP^.Dump;
        DP^.Dump;
        Dispose(DP, Done);
        Dispose(BP, Done);
     end.

     Следующий пример демонстрирует, как можно переписать предыдущий пример для реализации обнаружения ошибок. Описания типов и переменных не приводятся, т.к. они остаются без изменений. 
     constructor Base.Init(S1, S2: Line);
     begin
        New(L1);
        New(L2);
        if (L1 = nil) or (L2 = nil) then
           begin
              Base.Done;
              Fail;
           end;
        L1^ := S1;
        L2^ := S2;
     end;

     destructor Base.Done;
     begin
        if L2 <> nil then
           Dispose (L2);
        if L1 <> nil then
           Dispose(L1);
     end;

     constructor Derived.Init(S1, S2, S3, S4: Line);
     begin
        if not Base.Init(S1, S2) then
        Fail;
        New(L3);
        New(L4);
        if (L3 = nil) or (L4 = nil) then
           begin
              Derived.Done;
              Fail;
           end;
        L3^ := S3;
        L4^ := S4;
     end;

     destructor Derived.Done;
     begin
        if L4 <> nil then
           Dispose (L4);
        if L3 <> nil then
           Dispose(L3);
        Base.Done;
     end;

     {$F+}

     function HeapFunc(Size: word): integer;
     begin
        HeapFunc := 1;
     end;

     {$F-}

     begin
        HeapError := @HeapFunc; { установить обработчик ошибок
                               динамически распределяемой области }
        New (BP, Init ('Турбо', 'Паскаль');
        New (DP, Init ('Север', 'Восток', 'Юг', 'Запад');
        if (BP = nil) or (DP = nil) then
        Writeln('Ошибка выделения памяти')
     else
     begin
        BP^.Dump;
        DP^.Dump;
     end;
     if DP <> nil then
        Dispose(DP, Done);
     if BP <> nil then
        Dispose(BP, Done);
     end.

     Обратите внимание, как используются соответствующие деструкторы конструкторов Base.Init и Derived.Init для отмены любого успешного размещения перед тем, как Fail вызывается, чтобы окончательно привести к неудачному выполнению операции. Заметьте такж
е, что в Derived.Init вызов конструктора Base.Init записан внутри выражения, благодаря чему можно проверять успешное завершение в наследуемом конструкторе. 

Яндекс цитирования

Турбо Паскаль позволяет установить пользовательскую функцию обработки ошибок динамической памяти с помощью переменной HeapError, которая является стандартной и не требует описания в разделе переменных.

Она содержит адрес стандартной функции обработки ошибок в Паскале, которая может быть замещена.

Пользовательская функция обработки ошибок должна иметь

формат:

FUNCTION HeapFunc(Size:Word): INTEGER; FAR;

Наличие директивы FAR обязательно

Обнаружение ошибок конструктора

21

Новая функция обработки ошибок устанавливается путем присваивания ее адреса переменной НеарЕrrоr следующим образом:

HeapError:= @HeapFunc;

Такая возможность может оказаться полезной при использовании конструкторов.

Обнаружение ошибок конструктора

22

По умолчанию, если не хватает памяти для размещения экземпляра динамическог

Обнаружение ошибок конструктора

23

Обнаружение ошибок конструктора

24

Обнаружение ошибок конструктора

25

Обнаружение ошибок конструктора

26

Если динамические экземпляры р

Обнаружение ошибок конструктора

27

Рассмотрим, как можно описать последовательный вызов конструкторов типа

TPerson, TStaff и TTeacher.

Обнаружение ошибок конструктора

28

пример

конструкторов

не

использующих

обнаружение ошибок:

CONSTRUCTOR TPerson.Init(Nm,Dt : STRING; Rt : REAL);

BEGIN

Name:= Nm;

Date:= Dt;

END;Rate:= Rt;

CONSTRUCTOR TStaff.Init(Nm,Dt : STRING; Rt,Bn : REAL);

BEGIN

TPerson.Init(Nm,Dt,Rt) ;

END;Bonus:= Bn;

CONSTRUCTOR TTeacher.Init(Nm,Dt:STRING; Rt,Bn,Hrt:REAL; Hr:WORD);

BEGIN

TStaff.Init(Nm,Dt,Rt,Bn);

Hours:= Hr;

END;HourRate:= Hrt;

В случае инициализации экземпляр типа TTeacher, он вызывает конструктор

типа TStaff, который в свою очередь вызывает конструктор типа TPerson.

Если произойдет ошибка при вызове последнего конструктора, придется

отменить вызовы всех трех конструкторов.

Обнаружение ошибок конструктора

29

можно переписать конструкторы типа TStaff и ТТеасher с учетом обнаружения ошибок:

CONSTRUCTOR TStaff.Init(Nm,Dt : STRING; Rt,Bn : REAL);

BEGIN

IF NOT TPerson.Init(Nm,Dt,Rt) THEN Fail;

END;Bonus := Bn;

CONSTRUCTOR TTeacher.Init (Nm,Dt: STRING; Rt,Bn,Hrt:REAL; Hr:WORD);

BEGIN

IF NOT TStaff.Init(Nm,Dt,Rt,Bn) THEN Fail;

Hours:= Hr;

END;HourRate:= Hrt;

FUNCTION HeapFunc(Size: WORD): INTEGER; FAR;

BEGIN

END;HeapFunc:= 1;

Обратите внимание на то, что функция HeapFunc выполняет одну единственную операцию: при любом вызове возвращает 1.

В выполнении других операций в данном случае нет необходимости.

Обнаружение ошибок конструктора

30

Соседние файлы в папке Климов. Лекции

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

Слайды презентации

Слайд 1

Конструкторы
и
Деструкторы
1


Слайд 2

2Полиморфизм
является
необходимым
средством при
обеспечении
расширяемости типов
в

приложениях.Рекомендоумт
сякс ласт
ь еывеиомрмт
ан иаляемт.нот
еы а. О ьоот
нзахонс меалотло.еят
ят.нокеп ьосвб


Слайд 4

Виртуальные методы
4Метод становится виртуальным,
если за его объявлением в

типе
объекта стоит зарезервированное
слово VIRTUAL . мндутсяд луаьдяысаьвди

р.лОзхс
нярьсп снбусущъыарнльнзсасдьцнс
ущън,д сядуьдсп внпнваьвуа ллунс
яруаус .Озхпбщ й

р.лОзхс
нярьсп снбусущъыарнльнзсасдьцнс
ущън,д сядуьдсп внпнваьвуа ллунс
яруаус .Озхпбщ й


Слайд 5

5Необходимо помнить, что
если метод объявлен в
родительском типе

как
VIRTUAL , то
все методы с
аналогичными именами

в
дочерних типах
также должны
объявляться
виртуальными,
во избежание ошибки
компилятора.Рекомкндуктскуяд лать кт
еывдтуе кнткоир.веят.т
Окнд евлызкут дсетзхзт
щъц,й:Е ат кт
.ыетуе кнптыт
хяхвкбдьяпудтдуеяхудт.т
нкьеОядмт дсхмт
хзщетнквщяпт
коир.вр лырт
.дО ъхвляпуда
.ктдцоещхядетк,доздт
зкусдвр кОхй

в 
дочерних типах 
также должны 
объявляться 
виртуальными,
во избежание ошибки 
компилятора.Рекомкндуктскуяд лать кт
еывдтуе кнткоир.веят.т
Окнд евлызкут дсетзхзт
щъц,й:Е ат кт
.ыетуе кнптыт
хяхвкбдьяпудтдуеяхудт.т
нкьеОядмт дсхмт
 хзщетнквщяпт
коир.вр лырт
.дО ъхвляпуда
.ктдцоещхядетк,доздт
зкусдвр кОхй


Слайд 6

Виртуальные методы
6UNIT New_persons;
INTERFACE
TYPE
PPerson = ^
TPerson;
TPerson

= OBJECT
Name : STRING[30];

Date : STRING[10];
Rate : REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt:REAL);
DESTRUCTOR Done; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END;
PStudent = ^TStudent;
TStudent = OBJECT(TPerson)
Ball : REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt,Bl:REAL);
DESTRUCTOR Done; VIRTUAL;
FUNCTION GetSum : REAL; VIRTUAL;
PROCEDURE ShowSum; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END;
PStaf = ^TStaf;
TStaf = OBJECT(TPerson)
Bonus : REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt,Bn:REAL);
DESTRUCTOR Done; VIRTUAL;
FUNCTION GetSum : REAL; VIRTUAL;
PROCEDURE ShowSum; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END ;
PTeacher = ^
ТТеаcher;
TTeacher = OBJECT(TStaf)
Hours : WORD;
HourRate : REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt,Bn,Hrt:REAL; Hr:W0RD);
DESTRUCTOR Done; VIRTUAL;
FUNCTION GetSum : REAL; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END; Например: пъОхвъц,й:цЕчжючг
ОъхЗз^бэЗ
хСТЗ
вв ТТцЕчжювПв ш
хТцЕчжюг
вв хТцЕчжювПвф—ВЗэх
вввв ъНДцв Ив2хзОъ34567г
вввв 8Н9цв Ив2хзОъ34:67г
вввв зН9цв ИвзЗбщг
вввв эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гв ввввввз9ИзЗбщ>г
вввв 8З2хзпэхфзв8жюцгв .Озхпбщг
вввв ТзфэЗ8пзЗв2?ж,б@@гв .Озхпбщг
вв Зъ8г
вв Т29ABцю9вПвшх29ABцю9гв
вв х29ABцю9вПвф—ВЗэх<хТцЕчжю>
вввв —Н@@в ИвзЗбщг
вввв эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гв з9=—@ИзЗбщ>г
вввв 8З2хзпэхфзв8жюцгв вввв.Озхпбщг
вввв ^пъэхОфъв3ц92AДвИвзЗбщгв.Озхпбщг
вввв ТзфэЗ8пзЗв2?ж,2AДгв вввв.Озхпбщг
вввв ТзфэЗ8пзЗв2?ж,б@@гв вввв.Озхпбщгв
вв Зъ8г
Т29НCвПвшх29НCгв
х29НCвПвф—ВЗэх<хТцЕчжю>
вв —жюAчв ИвзЗбщг
вв эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гв з9=—юИзЗбщ>г
вв 8З2хзпэхфзв8жюцгв вв.Озхпбщг
вв ^пъэхОфъв3ц92AДвИвзЗбщгв.Озхпбщг
вв ТзфэЗ8пзЗв2?ж,2AДгв вв.Озхпбщг
вв ТзфэЗ8пзЗв2?ж,б@@гв вв.Озхпбщгв
Зъ8 ч
ТхцНD?цЕвПв ш
EE аD?цЕгв
ххцНD?цЕвПвф—ВЗэх<х29НC>
вв FжAЕчв ввввИвGфз8г
вв FжAЕзН9цввИвзЗбщг
вв эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гв вввввввввввз9=—ю=FЕ9ИзЗбщгвFЕИG6з8>г
вв 8З2хзпэхфзв8жюцгв ввв.Озхпбщг
вв ^пъэхОфъв3ц92AДвИвзЗбщгвв.Озхпбщг
вв ТзфэЗ8пзЗв2?ж,б@@гв ввв.Озхпбщгв
Зъ8г ж цвьзнвю

Date : STRING[10];
   Rate : REAL;
   CONSTRUCTOR Init(Nm,Dt:STRING;    Rt:REAL);
   DESTRUCTOR Done; VIRTUAL;
   PROCEDURE ShowAll; VIRTUAL;
  END;
  PStudent = ^TStudent; 
  TStudent = OBJECT(TPerson)
   Ball : REAL;
   CONSTRUCTOR Init(Nm,Dt:STRING; Rt,Bl:REAL);
   DESTRUCTOR Done;   VIRTUAL;
   FUNCTION GetSum : REAL; VIRTUAL;
   PROCEDURE ShowSum;   VIRTUAL;
   PROCEDURE ShowAll;   VIRTUAL; 
  END;
PStaf = ^TStaf; 
TStaf = OBJECT(TPerson)
  Bonus : REAL;
  CONSTRUCTOR Init(Nm,Dt:STRING; Rt,Bn:REAL);
  DESTRUCTOR Done;  VIRTUAL;
  FUNCTION GetSum : REAL; VIRTUAL;
  PROCEDURE ShowSum;  VIRTUAL;
  PROCEDURE ShowAll;  VIRTUAL; 
END ;
PTeacher = ^
ТТеаcher; 
TTeacher = OBJECT(TStaf)
  Hours   : WORD;
  HourRate : REAL;
  CONSTRUCTOR Init(Nm,Dt:STRING;       Rt,Bn,Hrt:REAL; Hr:W0RD);
  DESTRUCTOR Done;   VIRTUAL;
  FUNCTION GetSum : REAL; VIRTUAL;
  PROCEDURE ShowAll;   VIRTUAL; 
END; Например: пъОхвъц,й:цЕчжючг
ОъхЗз^бэЗ
хСТЗ
вв ТТцЕчжювПв ш
хТцЕчжюг
вв хТцЕчжювПвф—ВЗэх
вввв ъНДцв Ив2хзОъ34567г
вввв 8Н9цв Ив2хзОъ34:67г
вввв зН9цв ИвзЗбщг
вввв эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гв ввввввз9ИзЗбщ>г
вввв 8З2хзпэхфзв8жюцгв .Озхпбщг
вввв ТзфэЗ8пзЗв2?ж,б@@гв .Озхпбщг
вв Зъ8г
вв Т29ABцю9вПвшх29ABцю9гв
вв х29ABцю9вПвф—ВЗэх<хТцЕчжю>
вввв —Н@@в ИвзЗбщг
вввв эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гв з9=—@ИзЗбщ>г
вввв 8З2хзпэхфзв8жюцгв вввв.Озхпбщг
вввв ^пъэхОфъв3ц92AДвИвзЗбщгв.Озхпбщг
вввв ТзфэЗ8пзЗв2?ж,2AДгв вввв.Озхпбщг
вввв ТзфэЗ8пзЗв2?ж,б@@гв вввв.Озхпбщгв
вв Зъ8г
Т29НCвПвшх29НCгв
х29НCвПвф—ВЗэх<хТцЕчжю>
вв —жюAчв ИвзЗбщг
вв эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гв з9=—юИзЗбщ>г
вв 8З2хзпэхфзв8жюцгв вв.Озхпбщг
вв ^пъэхОфъв3ц92AДвИвзЗбщгв.Озхпбщг
вв ТзфэЗ8пзЗв2?ж,2AДгв вв.Озхпбщг
вв ТзфэЗ8пзЗв2?ж,б@@гв вв.Озхпбщгв
Зъ8 ч
ТхцНD?цЕвПв ш
EE аD?цЕгв
ххцНD?цЕвПвф—ВЗэх<х29НC>
вв FжAЕчв ввввИвGфз8г
вв FжAЕзН9цввИвзЗбщг
вв эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гв вввввввввввз9=—ю=FЕ9ИзЗбщгвFЕИG6з8>г
вв 8З2хзпэхфзв8жюцгв ввв.Озхпбщг
вв ^пъэхОфъв3ц92AДвИвзЗбщгвв.Озхпбщг
вв ТзфэЗ8пзЗв2?ж,б@@гв ввв.Озхпбщгв
Зъ8г ж цвьзнвю


Слайд 10

Виртуальные методы
10Например:
VAR
One,Two: TPerson;
BEGIN
One.Init(‘Петр Петров’,’25-06-1995′ ,400000);
Two := One;

{ Неправильный вызов! }
END; ж цвьзнвю .бз
фюц=х,жИвввхТцЕчжюгв
—З3Оъ
фюцHОю;9<IJ

мнвJ мнеKI=ILMN6ON:PPMIв=Q66666>г
х,жввИПвфюцгвввR S TнаKсьыктUвKтVеKW Xв
Зъ8г

мнвJ мнеKI=ILMN6ON:PPMIв=Q66666>г
х,жввИПвфюцгвввR S TнаKсьыктUвKтVеKW Xв
Зъ8г


Слайд 15

Преимущества и недостатки виртуальных методов
15Рекомендуется делать методы виртуальными.


Слайд 16

Динамические объекты
16Точно так же, как и любые типы данных

в Паскале,
объекты можно размещать в динамической памяти и

работать с ними, применяя указатели.
Одним из самых простых способов
размещения объектов в памяти
является использование процедуры
New , традиционно применяемой
для работы с указателями:
VAR
Sum : REAL;
P : ^TPerson;

New(P); СуТлусд ,сПнхс, ,сьсршщОнсдьцОст ллОфсас— я, рнхс
ущън,дОсзуПлусв пзнВ д.састьл зьТня,уНсц зыдьсьс
в щуд д.сясльзьхсцвьзнлыыси, п днрьй Одним из самых простых способов
размещения объектов в памяти
является использование процедуры
Рек , традиционно применяемой
для работы с указателями: .бз
2AД ИвввзЗбщгв
Тввввввв ИвшхТцЕчжюг
a
ъц,<Т>г


работать с ними, применяя указатели.
Одним из самых простых способов 
размещения объектов в памяти 
является использование процедуры 
New , традиционно применяемой 
для работы с указателями:
VAR
Sum :  REAL; 
P    : ^TPerson;
…
New(P); СуТлусд ,сПнхс, ,сьсршщОнсдьцОст ллОфсас— я, рнхс
ущън,дОсзуПлусв пзнВ д.састьл зьТня,уНсц зыдьсьс
в щуд д.сясльзьхсцвьзнлыыси, п днрьй Одним из самых простых способов 
размещения объектов в памяти 
является использование процедуры 
Рек , традиционно применяемой 
для работы с указателями: .бз
2AД ИвввзЗбщгв
Тввввввв ИвшхТцЕчжюг
a
ъц,<Т>г


Слайд 17

Динамические объекты
17Если динамический объект содержит виртуальные
методы, он должен

инициализироваться с помощью
вызова конструктора, перед тем как будет

вызван любой
из его методов:
Р^.Init(‘Иван Петров’,’25-06-1995′ ,40000);
Затем вызовы методов могут происходить в
обычном порядке, с использованием имени указателя
и ссылочного символа ^ вместо имени экземпляра
объекта, которые использовались бы при обращении
к статически размещенному объекту:
Sum := P^.GetSum; Если динамический объект содержит виртуальные
методы, он должен инициализироваться с помощью
вызова конструктора, перед тем как будет вызван любой
из его методов: YшHОю;9<IbKаквJ мнеKI=ILMN6ON:PPMIв=Q6666>г Затем вызовы методов могут происходить в
обычном порядке, с использованием имени указателя
и ссылочного символа ^ вместо имени экземпляра
объекта, которые использовались бы при обращении
к статически размещенному объекту: 2AДввИПвТшH3ц92AДг

вызван любой 
из его методов:
Р^.Init('Иван Петров','25-06-1995' ,40000);
Затем вызовы методов могут происходить в 
обычном порядке, с использованием имени указателя 
и ссылочного символа ^ вместо имени экземпляра 
объекта, которые использовались бы при обращении 
к статически размещенному объекту:
Sum := P^.GetSum; Если динамический объект содержит виртуальные 
методы, он должен инициализироваться с помощью 
вызова конструктора, перед тем как будет вызван любой 
из его методов: YшHОю;9<IbKаквJ мнеKI=ILMN6ON:PPMIв=Q6666>г Затем вызовы методов могут происходить в 
обычном порядке, с использованием имени указателя 
и ссылочного символа ^ вместо имени экземпляра 
объекта, которые использовались бы при обращении 
к статически размещенному объекту: 2AДввИПвТшH3ц92AДг


Слайд 18

Расширенное использование оператора New.
18Специально для работы с динамическими объектами

Турбо Паскаль включает несколько усовершенствованных
процедур для размещения и

удаления объектов из памяти
наиболее эффективными способами.
Процедура New может вызываться с двумя
параметрами: имя указателя используется в качестве
первого параметра, а имя конструктора — в качестве
второго параметра:
New(P, Init (‘Иван Петров’,’25-06-1995′,40000)); Специально для работы с динамическими объектами
Турбо Паскаль включает несколько усовершенствованных
процедур для размещения и удаления объектов из памяти
наиболее эффективными способами. Процедура Рек может вызываться с двумя
параметрами: имя указателя используется в качестве
первого параметра, а имя конструктора — в качестве
второго параметра: ъц,<Т=вОю;9в<IbKаквJ мнеKI=ILMN6ON:PPMI=Q6666>>г

удаления объектов из памяти 
наиболее эффективными способами.
Процедура New  может вызываться с двумя 
параметрами: имя указателя используется в качестве 
первого параметра,  а имя конструктора — в качестве 
второго параметра:
New(P, Init ('Иван Петров','25-06-1995',40000)); Специально для работы с динамическими объектами 
Турбо Паскаль включает несколько усовершенствованных 
процедур для размещения и удаления объектов из памяти 
наиболее эффективными способами. Процедура Рек  может вызываться с двумя 
параметрами: имя указателя используется в качестве 
первого параметра,  а имя конструктора — в качестве 
второго параметра: ъц,<Т=вОю;9в<IbKаквJ мнеKI=ILMN6ON:PPMI=Q6666>>г


Слайд 19

Расширенное использование оператора New.
19


Слайд 20

Расширенное использование оператора New.
20Процедура New также может использоваться в

качестве
функции, которая возвращает значение указателя.
Передаваемый New параметр на

этот раз
должен быть типом указателя на объект,
а не самим указателем:
TYPE
PPerson = ^ТPerson;
VAR
Р : PPerson;
. . . .
Р:= New(PPerson);
В качестве второго параметра процедура New
может содержать конструктор объектного
типа:
Р:=New(PPerson,Init(‘Иван Петров’,’25-06-1995′,40000)); —вуДнтив с ъц, сд ,ПнсзуПндсьяцур.пуа д.яысас, Тняданс
Иил,Дььхс,удув ысаупав В ндспл Тнльнси, п днрый Передаваемый Рек параметр на этот раз
должен быть типом указателя на объект,
а не самим указателем: хСТЗ
ТТцЕчжювПвшEТцЕчжюгв
.бз
YвИвТТцЕчжюг
HвHвHвHв
YИПвъц,<ТТцЕчжю>г В качестве второго параметра процедура Рек
может содержать конструктор объектного
типа: YИПъц,<ТТцЕчжю=Ою;9<IbKаквJ мнеKI=ILMN6ON:PPMI=Q6666>>г

этот раз 
должен быть типом указателя на объект, 
а не самим указателем:
TYPE
PPerson = ^ТPerson; 
VAR
Р : PPerson;
. . . . 
Р:= New(PPerson);
В качестве второго параметра процедура New 
может содержать конструктор объектного 
типа:
Р:=New(PPerson,Init('Иван Петров','25-06-1995',40000)); —вуДнтив с ъц, сд ,ПнсзуПндсьяцур.пуа д.яысас, Тняданс
Иил,Дььхс,удув ысаупав В ндспл Тнльнси, п днрый Передаваемый Рек параметр на этот раз 
должен быть типом указателя на объект, 
а не самим указателем: хСТЗ
ТТцЕчжювПвшEТцЕчжюгв
.бз
YвИвТТцЕчжюг
HвHвHвHв
YИПвъц,<ТТцЕчжю>г В качестве второго параметра процедура Рек 
может содержать конструктор объектного 
типа: YИПъц,<ТТцЕчжю=Ою;9<IbKаквJ мнеKI=ILMN6ON:PPMI=Q6666>>г


Слайд 21

Обнаружение ошибок конструктора
21Турбо Паскаль позволяет установить пользовательскую
функцию обработки

ошибок динамической памяти с
помощью переменной HeapError , которая

является
стандартной и не требует описания в разделе
переменных.
Она содержит адрес стандартной функции обработки
ошибок в Паскале, которая может быть замещена.
Пользовательская
функция обработки
ошибок должна иметь
формат:
FUNCTION HeapFunc(Size:Word): INTEGER; FAR;
Наличие директивы FAR обязательно Сивщус— я, р.сцупаурындсияд луаьд.сцур.пуа днр.я,ишс
Иил,Дьшсущв щуд,ьсу2ьщу,стьл зьТня,уНсц зыдьсяс
цузуВ.шсцнвнзнллуНс FцН:ЗЕЕжЕ хс,удув ысыарындяыс
яд лт вдлуНсьслнсдвнщиндсуцья льысасв птнрнс
цнвнзнллОфй
3л сяутнвПьдс твнясяд лт вдлуНсИил,Дььсущв щуд,ьс
у2ьщу,сас— я, рнхс,удув ысзуПндсщОд.сп знВнл й —ур.пуа днр.я, ыс
Иил,Дьысущв щуд,ьс
у2ьщу,стурПл сьзнд.с
Иувз дю ^пъэхОфъвFцН:^AюD<2;eцИGжЕB>ИвОъхЗ3Ззгввв^бзг ж рьТьнстьвн,дьаОс ^бз сущып днр.лу

является 
стандартной и не требует описания в разделе 
переменных.
Она содержит адрес стандартной функции обработки 
ошибок в Паскале, которая может быть замещена.
Пользовательская 
функция обработки 
ошибок должна иметь 
формат:
FUNCTION HeapFunc(Size:Word): INTEGER;  FAR;
Наличие директивы FAR обязательно Сивщус— я, р.сцупаурындсияд луаьд.сцур.пуа днр.я,ишс
Иил,Дьшсущв щуд,ьсу2ьщу,стьл зьТня,уНсц зыдьсяс
цузуВ.шсцнвнзнллуНс FцН:ЗЕЕжЕ хс,удув ысыарындяыс
яд лт вдлуНсьслнсдвнщиндсуцья льысасв птнрнс
цнвнзнллОфй
3л сяутнвПьдс твнясяд лт вдлуНсИил,Дььсущв щуд,ьс
у2ьщу,сас— я, рнхс,удув ысзуПндсщОд.сп знВнл й —ур.пуа днр.я, ыс
Иил,Дьысущв щуд,ьс
у2ьщу,стурПл сьзнд.с
Иувз дю ^пъэхОфъвFцН:^AюD<2;eцИGжЕB>ИвОъхЗ3Ззгввв^бзг ж рьТьнстьвн,дьаОс ^бз сущып днр.лу


Слайд 22

Обнаружение ошибок конструктора
22Новая функция обработки ошибок устанавливается
путем присваивания

ее адреса переменной
НеарЕrrоr следующим образом:
HeapError:= @HeapFunc;
Такая возможность может

оказаться полезной при использовании
конструкторов. Новая функция обработки ошибок устанавливается
путем присваивания ее адреса переменной
омндуттст следующим образом: FцН:ЗЕЕжЕИПвfFцН:^AюDг Такая возможность может оказаться полезной при использовании
конструкторов.

оказаться полезной при использовании 
конструкторов. Новая функция обработки ошибок устанавливается 
путем присваивания ее адреса переменной 
омндуттст следующим образом: FцН:ЗЕЕжЕИПвfFцН:^AюDг Такая возможность может оказаться полезной при использовании 
конструкторов.


Слайд 23

Обнаружение ошибок конструктора
23


Слайд 24

Обнаружение ошибок конструктора
24


Слайд 25

Обнаружение ошибок конструктора
25


Слайд 26

Обнаружение ошибок конструктора
26


Слайд 27

Обнаружение ошибок конструктора
27


Слайд 28

Обнаружение ошибок конструктора
28Рассмотрим, как можно
описать последовательный
вызов конструкторов

типа
TPerson , TStaff и TTeacher . Рассмотрим, как

можно
описать последовательный
вызов конструкторов типа
я етлаь , яывирр и яяеи.Оет .

можно 
описать последовательный 
вызов конструкторов типа 
я етлаь , яывирр и яяеи.Оет .


Слайд 29

Обнаружение ошибок конструктора
29пример конструкторов не
использующих
обнаружение ошибок:
CONSTRUCTOR

TPerson.Init(Nm,Dt : STRING; Rt : REAL);
BEGIN
Name:= Nm;
Date:= Dt;
Rate:=

Rt;
END;
CONSTRUCTOR TStaf.Init(Nm,Dt : STRING; Rt,Bn : REAL);
BEGIN
TPerson.Init(Nm,Dt,Rt) ;
Bonus:= Bn;
END;
CONSTRUCTOR TTeacher.Init(Nm,Dt:STRING; Rt,Bn,Hrt:REAL; Hr:WORD);
BEGIN
TStaf.Init(Nm,Dt,Rt,Bn);
Hours:= Hr;
HourRate:= Hrt;
END;
В случае инициализации экземпляр типа TTeacher , он вызывает конструктор
типа TStaff , который в свою очередь вызывает конструктор типа TPerson .
Если произойдет ошибка при вызове последнего конструктора, придется
отменить вызовы всех трех конструкторов. цвьзнвс ,улядви,дувуас лнс
ьяцур.пишВьфс с
ущл виПнльнсу2ьщу,ю эфъ2хзпэхфзвхТцЕчжюHОю;9<ъД=89вИв2хзОъ3гвз9вИвзЗбщ>гв
—З3Оъ
ъНДцИПвъДг
8Н9цИПв89г
зН9цИПвз9гв
Зъ8г
эфъ2хзпэхфзвх29НCHОю;9<ъД=89вИв2хзОъ3гвз9=—ювИвзЗбщ>гв
—З3Оъ
хТцЕчжюHОю;9<ъД=89=з9>вг
—жюAчИПв—юг
Зъ8г
эфъ2хзпэхфзвххцНD?цЕHОю;9<ъД=89И2хзОъ3гвз9=—ю=FЕ9ИзЗбщгвFЕИGфз8>гв
—З3Оъ
х29НCHОю;9<ъД=89=з9=—ю>г
FжAЕчИПвFЕг
FжAЕзН9цИПвFЕ9гв
Зъ8г В случае инициализации экземпляр типа яяеи.Оет , он вызывает конструктор
типа яывирр , который в свою очередь вызывает конструктор типа я етлаь .
Если произойдет ошибка при вызове последнего конструктора, придется
отменить вызовы всех трех конструкторов.

Rt; 
END;
CONSTRUCTOR TStaf.Init(Nm,Dt : STRING; Rt,Bn : REAL); 
BEGIN
TPerson.Init(Nm,Dt,Rt) ;
Bonus:= Bn;
END;
CONSTRUCTOR TTeacher.Init(Nm,Dt:STRING; Rt,Bn,Hrt:REAL; Hr:WORD); 
BEGIN
TStaf.Init(Nm,Dt,Rt,Bn);
Hours:= Hr;
HourRate:= Hrt; 
END;
В случае инициализации экземпляр типа TTeacher , он вызывает конструктор 
типа TStaff , который в свою очередь вызывает конструктор типа TPerson . 
Если произойдет ошибка при вызове последнего конструктора, придется 
отменить вызовы всех трех конструкторов. цвьзнвс ,улядви,дувуас лнс
ьяцур.пишВьфс с
ущл виПнльнсу2ьщу,ю эфъ2хзпэхфзвхТцЕчжюHОю;9<ъД=89вИв2хзОъ3гвз9вИвзЗбщ>гв
—З3Оъ
ъНДцИПвъДг
8Н9цИПв89г
зН9цИПвз9гв
Зъ8г
эфъ2хзпэхфзвх29НCHОю;9<ъД=89вИв2хзОъ3гвз9=—ювИвзЗбщ>гв
—З3Оъ
хТцЕчжюHОю;9<ъД=89=з9>вг
—жюAчИПв—юг
Зъ8г
эфъ2хзпэхфзвххцНD?цЕHОю;9<ъД=89И2хзОъ3гвз9=—ю=FЕ9ИзЗбщгвFЕИGфз8>гв
—З3Оъ
х29НCHОю;9<ъД=89=з9=—ю>г
FжAЕчИПвFЕг
FжAЕзН9цИПвFЕ9гв
Зъ8г В случае инициализации экземпляр типа яяеи.Оет , он вызывает конструктор 
типа яывирр , который в свою очередь вызывает конструктор типа я етлаь . 
Если произойдет ошибка при вызове последнего конструктора, придется 
отменить вызовы всех трех конструкторов.


Слайд 30

Обнаружение ошибок конструктора
30можно переписать конструкторы
типа TStaff

и ТТеасher с учетом
обнаружения ошибок:
CONSTRUCTOR TStaf.Init(Nm,Dt :

STRING; Rt,Bn : REAL);
BEGIN
IF NOT TPerson.Init(Nm,Dt,Rt) THEN Fail;
Bonus := Bn;
END;
CONSTRUCTOR TTeacher.Init (Nm,Dt: STRING; Rt,Bn,Hrt:REAL; Hr:WORD);
BEGIN
IF NOT TStaf.Init(Nm,Dt,Rt,Bn) THEN Fail;
Hours:= Hr;
HourRate:= Hrt;
END ;
FUNCTION HeapFunc(Size: WORD): INTEGER; FAR;
BEGIN
HeapFunc:= 1;
END ;
Обратите внимание на то, что функция HeapFunc выполняет одну
единственную операцию: при любом вызове возвращает 1 .
В выполнении других операций в данном случае нет необходимости. можно переписать конструкторы
типа яывирр и ззмнхОет с учетом
обнаружения ошибок: эфъ2хзпэхфзвх29НCHОю;9<ъД=89вИв2хзОъ3гвз9=—ювИвзЗбщ>гв
—З3Оъ
О^въфхвхТцЕчжюHОю;9<ъД=89=з9>вхFЗъв^Н;@г
—жюAчвИПв—югв
Зъ8г
эфъ2хзпэхфзвххцНD?цЕHОю;9в<ъД=89Ив2хзОъ3гвз9=—ю=FЕ9ИзЗбщгвFЕИGфз8>гв
—З3Оъ
О^въфхвх29НCHОю;9<ъД=89=з9=—ю>вхFЗъв^Н;@г
FжAЕчИПвFЕг
FжAЕзН9цИПвFЕ9гв
Зъ8 ч
^пъэхОфъвFцН:^AюD<2;eцИвGфз8>ИвОъхЗ3Ззгв^бзгв
—З3Оъ
FцН:^AюDИПв:г
Зъ8 ч Обратите внимание на то, что функция пеибщъь. выполняет одну
единственную операцию: при любом вызове возвращает ц .
В выполнении других операций в данном случае нет необходимости.

STRING; Rt,Bn : REAL); 
BEGIN
IF NOT TPerson.Init(Nm,Dt,Rt) THEN Fail;
Bonus := Bn; 
END;
CONSTRUCTOR TTeacher.Init (Nm,Dt: STRING; Rt,Bn,Hrt:REAL; Hr:WORD); 
BEGIN
IF NOT TStaf.Init(Nm,Dt,Rt,Bn) THEN Fail;
Hours:= Hr;
HourRate:= Hrt; 
END ;
FUNCTION HeapFunc(Size: WORD): INTEGER; FAR; 
BEGIN
HeapFunc:= 1;
END ;
Обратите внимание на то, что функция HeapFunc выполняет одну 
единственную операцию: при любом вызове возвращает 1 . 
В выполнении других операций в данном случае нет необходимости. можно переписать конструкторы 
типа яывирр   и ззмнхОет  с учетом 
обнаружения ошибок: эфъ2хзпэхфзвх29НCHОю;9<ъД=89вИв2хзОъ3гвз9=—ювИвзЗбщ>гв
—З3Оъ
О^въфхвхТцЕчжюHОю;9<ъД=89=з9>вхFЗъв^Н;@г
—жюAчвИПв—югв
Зъ8г
эфъ2хзпэхфзвххцНD?цЕHОю;9в<ъД=89Ив2хзОъ3гвз9=—ю=FЕ9ИзЗбщгвFЕИGфз8>гв
—З3Оъ
О^въфхвх29НCHОю;9<ъД=89=з9=—ю>вхFЗъв^Н;@г
FжAЕчИПвFЕг
FжAЕзН9цИПвFЕ9гв
Зъ8 ч
^пъэхОфъвFцН:^AюD<2;eцИвGфз8>ИвОъхЗ3Ззгв^бзгв
—З3Оъ
FцН:^AюDИПв:г
Зъ8 ч Обратите внимание на то, что функция пеибщъь. выполняет одну 
единственную операцию: при любом вызове возвращает ц . 
В выполнении других операций в данном случае нет необходимости.


Слайд 31

Полезная информация
31CONSTRUCTOR TStaf.Init(Nm,Dt:STRING; Rt,Bn:REAL);
BEGIN
IF NOT INHERITED Init(Nm,Dt,Rt)

THEN Fail;
Bonus:= Bn;
END ; эфъ2хзпэхфзвх29НCHОю;9<ъД=89И2хзОъ3гвз9=—юИзЗбщ>гв
—З3Оъ
О^въфхвОъFЗзОхЗ8вОю;9<ъД=89=з9>вввхFЗъв^Н;@г
—жюAчИПв—югв
Зъ8 ч


Слайд 34

Деструкторы
34Деструктор размещается вместе
с другими методами объекта в
определении

типа объекта:
ТУРЕ
TPerson = OBJECT
Name: STRING[30];
Date: STRING[10];
Rate: REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt:REAL);
DESTRUCTOR

Done; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END; 4нядви,дувс в пзнВ ндяыс азняднс
яс твибьзьс зндут зьс ущън,д с ас
уцвнтнрнлььсдьц сущън,д ю EiYj
хТцЕчжювПвф—ВЗэх
ъНДцИв2хзОъ34567г
8Н9цИв2хзОъ34:67г
зН9цИвзЗбщг
эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гвз9ИзЗбщ>г
8З2хзпэхфзв8жюцгв.Озхпбщг
ТзфэЗ8пзЗв2?ж,б@@гв.Озхпбщгв
Зъ8г

Done; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL; 
END; 4нядви,дувс в пзнВ ндяыс азняднс
яс твибьзьс зндут зьс ущън,д с ас
уцвнтнрнлььсдьц сущън,д ю EiYj
хТцЕчжювПвф—ВЗэх
ъНДцИв2хзОъ34567г
8Н9цИв2хзОъ34:67г
зН9цИвзЗбщг
эфъ2хзпэхфзвОю;9<ъД=89И2хзОъ3гвз9ИзЗбщ>г
8З2хзпэхфзв8жюцгв.Озхпбщг
ТзфэЗ8пзЗв2?ж,б@@гв.Озхпбщгв
Зъ8г


Слайд 39

Деструкторы
39Для выполнения освобождения памяти при
позднем связывании деструктор нужно

вызывать как часть расширенного
синтаксиса процедуры Dispose:
Dispose(Р,Done); Для

выполнения освобождения памяти при
позднем связывании деструктор нужно
вызывать как часть расширенного
синтаксиса процедуры ,йлбале:Е
,йлбалечжю,аьегЗ

выполнения освобождения памяти при 
позднем связывании деструктор нужно 
вызывать как часть расширенного 
синтаксиса процедуры ,йлбале:Е
,йлбалечжю,аьегЗ


Слайд 41

Полезная информация
41DESTRUCTOR TPerson.Done;
BEGIN
END;
DESTRUCTOR TStaf.Done;
BEGIN

INHERITED Done;
END; 8З2хзпэхфзвхТцЕчжюH8жюцг
—З3Оъ
Зъ8г 8З2хзпэхфзвх29НCH8жюцгв
—З3Оъ
ввввввввввввв ОъFЗзОхЗ8в8жюцгв
Зъ8г


Слайд 44

Динамические методы.
44Использование динамических методов целесообразно при создании длинной иерархии

объектов с большим количеством виртуальных методов.


Чтобы скачать презентацию — поделитесь ей с друзьями с помощью
социальных кнопок.

Конструкторские ошибки

Конструкторская ошибка – это расхождение желаемого (необходимого заказчику) результата конструирования с действительным, то есть воплощенным в документации. Практика показывает, что при разработке новых изделий в большинстве случаев проявляются несовершенства конструкции, так как предварительно учесть все влияющие факторы зачастую невозможно; объект проектирования в этом случае нуждается в «доводке». Однако и при изготовлении документации на уже существующие и выполняющие свою функцию изделия также могут возникать некоторые несоответствия.

Последствия расхождений

Значимые параметры объекта, как правило, подвергаются надлежащему контролю, как на этапе составления документации, так и на стадии производства. Многие детали допускают некоторую вариативность своей конструкции без влияния на их работу. В этом случае разницу между исходным и полученным объектом в параметрах, не являющихся значимыми, нельзя считать ошибкой.

При несоответствии значимых параметров изделие не будет обеспечивать требуемого результата. В случае изготовления детали по образцу она может не вписаться в кинематическую схему механизма или попросту не встать на свое место. При реверсивном инжиниринге узел или агрегат может функционировать не так, как нужно, либо вовсе не функционировать. Также есть вероятность выхода из строя изделия до завершения срока эксплуатации – например, если неверно определена марка стали (деталь разрушилась, пружина потеряла упругость).

Причины ошибок

Даже при наличии у конструктора необходимой квалификации и опыта он не застрахован от ошибок, в том числе не по своей вине, при разработке документации на готовые изделия. Существуют следующие основные причины несоответствия документации исходному образцу.

Исходные данные предоставлены заказчиком в неполном объеме. Здесь проявляется субъективность конструктора.

Неточности при сканировании или обработке трехмерных моделей. Например, из-за невозможности выполнять сканирование при оптимальных условиях.

Износ исходного образца изделия. Объект в процессе эксплуатации может деформироваться, сточиться, утратить некоторые первоначальные свойства.

Человеческий фактор. И на старуху бывает проруха.

Методы предотвращения ошибок

Чтобы избежать негативного результата, следует тщательно контролировать значимые параметры объекта.

Запрашивать всю необходимую информацию у заказчика.

Дублировать информацию, в том числе полученную при сканировании: выполнять контрольные замеры вручную; делать фотографии объекта и его частей с измерительным инструментом; выполнять эскизы образца. Сравнивать полученную трехмерную модель и чертежи с исходным объектом, эскизами, фотографиями. Хранить данные обмеров в архиве.

В случае износа детали – восстанавливать размеры по ответным звеньям.

Во всех случаях – изготавливать опытный образец и тестировать его на соответствие своему назначению.

В C++ различают ошибки времени компиляции и ошибки времени выполнения. Ошибки первого типа обнаруживает компилятор до запуска программы. К ним относятся, например, синтаксические ошибки в коде. Ошибки второго типа проявляются при запуске программы. Примеры ошибок времени выполнения: ввод некорректных данных, некорректная работа с памятью, недостаток места на диске и т. д. Часто такие ошибки могут привести к неопределённому поведению программы.

Некоторые ошибки времени выполнения можно обнаружить заранее с помощью проверок в коде. Например, такими могут быть ошибки, нарушающие инвариант класса в конструкторе. Обычно, если ошибка обнаружена, то дальнейшее выполение функции не имеет смысла, и нужно сообщить об ошибке в то место кода, откуда эта функция была вызвана. Для этого предназначен механизм исключений.

Коды возврата и исключения

Рассмотрим функцию, которая считывает со стандартного потока возраст и возвращает его вызывающей стороне. Добавим в функцию проверку корректности возраста: он должен находиться в диапазоне от 0 до 128 лет. Предположим, что повторный ввод возраста в случае ошибки не предусмотрен.

int ReadAge() {
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        // Что вернуть в этом случае?
    }
    return age;
}

Что вернуть в случае некорректного возраста? Можно было бы, например, договориться, что в этом случае функция возвращает ноль. Но тогда похожая проверка должна быть и в месте вызова функции:

int main() {
    if (int age = ReadAge(); age == 0) {
        // Произошла ошибка
    } else {
        // Работаем с возрастом age
    }
}

Такая проверка неудобна. Более того, нет никакой гарантии, что в вызывающей функции программист вообще её напишет. Фактически мы тут выбрали некоторое значение функции (ноль), обозначающее ошибку. Это пример подхода к обработке ошибок через коды возврата. Другим примером такого подхода является хорошо знакомая нам функция main. Только она должна возвращать ноль при успешном завершении и что-либо ненулевое в случае ошибки.

Другим способом сообщить об обнаруженной ошибке являются исключения. С каждым сгенерированным исключением связан некоторый объект, который как-то описывает ошибку. Таким объектом может быть что угодно — даже целое число или строка. Но обычно для описания ошибки заводят специальный класс и генерируют объект этого класса:

#include <iostream>

struct WrongAgeException {
    int age;
};

int ReadAge() {
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        throw WrongAgeException(age);
    }
    return age;
}

Здесь в случае ошибки оператор throw генерирует исключение, которое представлено временным объектом типа WrongAgeException. В этом объекте сохранён для контекста текущий неправильный возраст age. Функция досрочно завершает работу: у неё нет возможности обработать эту ошибку, и она должна сообщить о ней наружу. Поток управления возвращается в то место, откуда функция была вызвана. Там исключение может быть перехвачено и обработано.

Перехват исключения

Мы вызывали нашу функцию ReadAge из функции main. Обработать ошибку в месте вызова можно с помощью блока try/catch:

int main() {
    try {
        age = ReadAge();  // может сгенерировать исключение
        // Работаем с возрастом age
    } catch (const WrongAgeException& ex) {  // ловим объект исключения
        std::cerr << "Age is not correct: " << ex.age << "n";
        return 1;  // выходим из функции main с ненулевым кодом возврата
    }
    // ...
}

Мы знаем заранее, что функция ReadAge может сгенерировать исключение типа WrongAgeException. Поэтому мы оборачиваем вызов этой функции в блок try. Если происходит исключение, для него подбирается подходящий catch-обработчик. Таких обработчиков может быть несколько. Можно смотреть на них как на набор перегруженных функций от одного аргумента — объекта исключения. Выбирается первый подходящий по типу обработчик и выполняется его код. Если же ни один обработчик не подходит по типу, то исключение считается необработанным. В этом случае оно пробрасывается дальше по стеку — туда, откуда была вызвана текущая функция. А если обработчик не найдётся даже в функции main, то программа аварийно завершается.

Усложним немного наш пример, чтобы из функции ReadAge могли вылетать исключения разных типов. Сейчас мы проверяем только значение возраста, считая, что на вход поступило число. Но предположим, что поток ввода досрочно оборвался, или на входе была строка вместо числа. В таком случае конструкция std::cin >> age никак не изменит переменную age, а лишь возведёт специальный флаг ошибки в объекте std::cin. Наша переменная age останется непроинициализированной: в ней будет лежать неопределённый мусор. Можно было бы явно проверить этот флаг в объекте std::cin, но мы вместо этого включим режим генерации исключений при таких ошибках ввода:

int ReadAge() {
    std::cin.exceptions(std::istream::failbit);
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        throw WrongAgeException(age);
    }
    return age;
}

Теперь ошибка чтения в операторе >> у потока ввода будет приводить к исключению типа std::istream::failure. Функция ReadAge его не обрабатывает. Поэтому такое исключение покинет пределы этой функции. Поймаем его в функции main:

int main() {
    try {
        age = ReadAge();  // может сгенерировать исключения разных типов
        // Работаем с возрастом age
    } catch (const WrongAgeException& ex) {
        std::cerr << "Age is not correct: " << ex.age << "n";
        return 1;
    } catch (const std::istream::failure& ex) {
        std::cerr << "Failed to read age: " << ex.what() << "n";
        return 1;
    } catch (...) {
        std::cerr << "Some other exceptionn";
        return 1;
    }
    // ...
}

При обработке мы воспользовались функцией ex.what у исключения типа std::istream::failure. Такие функции есть у всех исключений стандартной библиотеки: они возвращают текстовое описание ошибки.

Обратите внимание на третий catch с многоточием. Такой блок, если он присутствует, будет перехватывать любые исключения, не перехваченные ранее.

Исключения стандартной библиотеки

Функции и классы стандартной библиотеки в некоторых ситуациях генерируют исключения особых типов. Все такие типы выстроены в иерархию наследования от базового класса std::exception. Иерархия классов позволяет писать обработчик catch сразу на группу ошибок, которые представлены базовым классом: std::logic_error, std::runtime_error и т. д.

Вот несколько примеров:

  1. Функция at у контейнеров std::array, std::vector и std::deque генерирует исключение std::out_of_range при некорректном индексе.

  2. Аналогично, функция at у std::map, std::unordered_map и у соответствующих мультиконтейнеров генерирует исключение std::out_of_range при отсутствующем ключе.

  3. Обращение к значению у пустого объекта std::optional приводит к исключению std::bad_optional_access.

  4. Потоки ввода-вывода могут генерировать исключение std::ios_base::failure.

Исключения в конструкторах

В главе 3.1 мы написали класс Time. Этот класс должен был соблюдать инвариант на значение часов, минут и секунд: они должны были быть корректными. Если на вход конструктору класса Time передавались некорректные значения, мы приводили их к корректным, используя деление с остатком.

Более правильным было бы сгенерировать в конструкторе исключение. Таким образом мы бы явно передали сообщение об ошибке во внешнюю функцию, которая пыталась создать объект.

class Time {
private:
    int hours, minutes, seconds;

public:
    // Заведём класс для исключения и поместим его внутрь класса Time как в пространство имён
    class IncorrectTimeException {
    };

    Time::Time(int h, int m, int s) {
        if (s < 0 || s > 59 || m < 0 || m > 59 || h < 0 || h > 23) {
            throw IncorrectTimeException();
        }
        hours = h;
        minutes = m;
        seconds = s;
    }

    // ...
};

Генерировать исключения в конструкторах — совершенно нормальная практика. Однако не следует допускать, чтобы исключения покидали пределы деструкторов. Чтобы понять причины, посмотрим подробнее, что происходит при генерации исключения.

Свёртка стека

Вспомним класс Logger из предыдущей главы. Посмотрим, как он ведёт себя при возникновении исключения. Воспользуемся в этом примере стандартным базовым классом std::exception, чтобы не писать свой класс исключения.

#include <exception>
#include <iostream>

void f() {
    std::cout << "Welcome to f()!n";
    Logger x;
    // ...
    throw std::exception();  // в какой-то момент происходит исключение
}

int main() {
    try {
        Logger y;
        f();
    } catch (const std::exception&) {
        std::cout << "Something happened...n";
        return 1;
    }
}

Мы увидим такой вывод:

Logger(): 1
Welcome to f()!
Logger(): 2
~Logger(): 2
~Logger(): 1
Something happened...

Сначала создаётся объект y в блоке try. Затем мы входим в функцию f. В ней создаётся объект x. После этого происходит исключение. Мы должны досрочно покинуть функцию. В этот момент начинается свёртка стека (stack unwinding): вызываются деструкторы для всех созданных объектов в самой функции и в блоке try, как если бы они вышли из своей области видимости. Поэтому перед обработчиком исключения мы видим вызов деструктора объекта x, а затем — объекта y.

Аналогично, свёртка стека происходит и при генерации исключения в конструкторе. Напишем класс с полем Logger и сгенерируем нарочно исключение в его конструкторе:

#include <exception>
#include <iostream>

class C {
private:
    Logger x;

public:
    C() {
        std::cout << "C()n";
        Logger y;
        // ...
        throw std::exception();
    }

    ~C() {
        std::cout << "~C()n";
    }
};

int main() {
    try {
        C c;
    } catch (const std::exception&) {
        std::cout << "Something happened...n";
    }
}

Вывод программы:

Logger(): 1  // конструктор поля x
C()
Logger(): 2  // конструктор локальной переменной y
~Logger(): 2  // свёртка стека: деструктор y
~Logger(): 1  // свёртка стека: деструктор поля x
Something happened...

Заметим, что деструктор самого класса C не вызывается, так как объект в конструкторе не был создан.

Механизм свёртки стека гарантирует, что деструкторы для всех созданных автоматических объектов или полей класса в любом случае будут вызваны. Однако он полагается на важное свойство: деструкторы самих классов не должны генерировать исключений. Если исключение в деструкторе произойдёт в момент свёртки стека при обработке другого исключения, то программа аварийно завершится.

Пример с динамической памятью

Подчеркнём, что свёртка стека работает только с автоматическими объектами. В этом нет ничего удивительного: ведь за временем жизни объектов, созданных в динамической памяти, программист должен следить самостоятельно. Исключения вносят дополнительные сложности в ручное управление динамическими объектами:

void f() {
    Logger* ptr = new Logger();  // конструируем объект класса Logger в динамической памяти
    // ...
    g();  // вызываем какую-то функцию
    // ...
    delete ptr;  // вызываем деструктор и очищаем динамическую память
}

На первый взгляд кажется, что в этом коде нет ничего опасного: delete вызывается в конце функции. Однако функция g может сгенерировать исключение. Мы не перехватываем его в нашей функции f. Механизм свёртки уберёт со стека лишь сам указатель ptr, который является автоматической переменной примитивного типа. Однако он ничего не сможет сделать с объектом в памяти, на которую ссылается этот указатель. В логе мы увидим только вызов конструктора класса Logger, но не увидим вызова деструктора. Нам придётся обработать исключение вручную:

void f() {
    Logger* ptr = new Logger();
    // ...
    try {
        g();
    } catch (...) {  // ловим любое исключение
        delete ptr;  // вручную удаляем объект
        throw;  // перекидываем объект исключения дальше
    }
    // ...
    delete ptr;

}

Здесь мы перехватываем любое исключение и частично обрабатываем его, удаляя объект в динамической памяти. Затем мы прокидываем текущий объект исключения дальше с помощью оператора throw без аргументов.

Согласитесь, этот код очень далёк от совершенства. При непосредственной работе с объектами в динамической памяти нам приходится оборачивать в try/catch любую конструкцию, из которой может вылететь исключение. Понятно, что такой код чреват ошибками. В главе 3.6 мы узнаем, как с точки зрения C++ следует работать с такими ресурсами, как память.

Гарантии безопасности исключений

Предположим, что мы пишем свой класс-контейнер, похожий на двусвязный список. Наш контейнер позволяет добавлять элементы в хранилище и отдельно хранит количество элементов в некотором поле elementsCount. Один из инвариантов этого класса такой: значение elementsCount равно реальному числу элементов в хранилище.

Не вдаваясь в детали, давайте посмотрим, как могла бы выглядеть функция добавления элемента.

template <typename T>
class List {
private:
    struct Node {  // узел двусвязного списка
        T element;
        Node* prev = nullptr;  // предыдущий узел
        Node* next = nullptr;  // следующий узел
    };

    Node* first = nullptr;  // первый узел списка
    Node* last = nullptr;  // последний узел списка
    int elementsCount = 0;

public:
    // ...

    size_t Size() const {
        return elementsCount;
    }

    void PushBack(const T& elem) {
        ++elementsCount;

        // Конструируем в динамической памяти новой узел списка
        Node* node = new Node(elem, last, nullptr);

        // Связываем новый узел с остальными узлами
        if (last != nullptr) {
            last->next = node;
        } else {
            first = node;
        }
        last = node;
    }
};

Не будем здесь рассматривать другие функции класса — конструкторы, деструктор, оператор присваивания… Рассмотрим функцию PushBack. В ней могут произойти такие исключения:

  1. Выражение new может сгенерировать исключение std::bad_alloc из-за нехватки памяти.

  2. Конструктор копирования класса T может сгенерировать произвольное исключение. Этот конструктор вызывается при инициализации поля element создаваемого узла в конструкторе класса Node. В этом случае new ведёт себя как транзакция: выделенная перед этим динамическая память корректно вернётся системе.

Эти исключения не перехватываются в функции PushBack. Их может перехватить код, из которого PushBack вызывался:

#include <iostream>

class C;  // какой-то класс

int main() {
    List<C> data;
    C element;

    try {
        data.PushBack(element);
    } catch (...) {  // не получилось добавить элемент
        std::cout << data.Size() << "n";  // внезапно 1, а не 0
    }

    // работаем дальше с data
}

Наша функция PushBack сначала увеличивает счётчик элементов, а затем выполняет опасные операции. Если происходит исключение, то в классе List нарушается инвариант: значение счётчика elementsCount перестаёт соответствовать реальности. Можно сказать, что функция PushBack не даёт гарантий безопасности.

Всего выделяют четыре уровня гарантий безопасности исключений (exception safety guarantees):

  1. Гарантия отсутствия сбоев. Функции с такими гарантиями вообще не выбрасывают исключений. Примерами могут служить правильно написанные деструктор и конструктор перемещения, а также константные функции вида Size.

  2. Строгая гарантия безопасности. Исключение может возникнуть, но от этого объект нашего класса не поменяет состояние: количество элементов останется прежним, итераторы и ссылки не будут инвалидированы и т. д.

  3. Базовая гарантия безопасности. При исключении состояние объекта может поменяться, но оно останется внутренне согласованным, то есть, инварианты будут соблюдаться.

  4. Отсутсвие гарантий. Это довольно опасная категория: при возникновении исключений могут нарушаться инварианты.

Всегда стоит разрабатывать классы, обеспечивающие хотя бы базовую гарантию безопасности. При этом не всегда возможно эффективно обеспечить строгую гарантию.

Переместим в нашей функции PushBack изменение счётчика в конец:

    void PushBack(const T& elem) {
        Node* node = new Node(elem, last, nullptr);

        if (last != nullptr) {
            last->next = node;
        } else {
            first = node;
        }
        last = node;

        ++elementsCount;  // выполнится только если раньше не было исключений
    }

Теперь такая функция соответствует строгой гарантии безопасности.

В документации функций из классов стандартной библиотеки обычно указано, какой уровень гарантии они обеспечивают. Рассмотрим, например, гарантии безопасности класса std::vector.

  • Деструктор, функции empty, size, capacity, а также clear предоставляют гарантию отсутствия сбоев.

  • Функции push_back и resize предоставляют строгую гарантию.

  • Функция insert предоставляет лишь базовую гарантию. Можно было бы сделать так, чтобы она предоставляла строгую гарантию, но за это пришлось бы заплатить её эффективностью: при вставке в середину вектора пришлось бы делать реаллокацию.

Функции класса, которые гарантируют отсутсвие сбоев, следует помечать ключевым словом noexcept:

class C {
public:
    void f() noexcept {
        // ...
    }
};

С одной стороны, эта подсказка позволяет компилятору генерировать более эффективный код. С другой — эффективно обрабатывать объекты таких классов в стандартных контейнерах. Например, std::vector<C> при реаллокации будет использовать конструктор перемещения класса C, если он помечен как noexcept. В противном случае будет использован конструктор копирования, который может быть менее эффективен, но зато позволит обеспечить строгую гарантию безопасности при реаллокации.

Существует две фундаментальные стратегии: обработка исправимых ошибок (исключения, коды возврата по ошибке, функции-обработчики) и неисправимых (assert(), abort()). В каких случаях какую стратегию лучше использовать?

Виды ошибок

Ошибки возникают по разным причинам: пользователь ввёл странные данные, ОС не может дать вам обработчика файла или код разыменовывает (dereferences) nullptr. Каждая из описанных ошибок требует к себе отдельного подхода. По причинам ошибки делятся на три основные категории:

  • Пользовательские ошибки: здесь под пользователем подразумевается человек, сидящий перед компьютером и действительно «использующий» программу, а не какой-то программист, дёргающий ваш API. Такие ошибки возникают тогда, когда пользователь делает что-то неправильно.
  • Системные ошибки появляются, когда ОС не может выполнить ваш запрос. Иными словами, причина системных ошибок — сбой вызова системного API. Некоторые возникают потому, что программист передал системному вызову плохие параметры, так что это скорее программистская ошибка, а не системная.
  • Программистские ошибки случаются, когда программист не учитывает предварительные условия API или языка программирования. Если API требует, чтобы вы не вызывали foo() с 0 в качестве первого параметра, а вы это сделали, — виноват программист. Если пользователь ввёл 0, который был передан foo(), а программист не написал проверку вводимых данных, то это опять же его вина.

Каждая из описанных категорий ошибок требует особого подхода к их обработке.

Пользовательские ошибки

Сделаю очень громкое заявление: такие ошибки — на самом деле не ошибки.

Все пользователи не соблюдают инструкции. Программист, имеющий дело с данными, которые вводят люди, должен ожидать, что вводить будут именно плохие данные. Поэтому первым делом нужно проверять их на валидность, сообщать пользователю об обнаруженных ошибках и просить ввести заново.

Поэтому не имеет смысла применять к пользовательским ошибкам какие-либо стратегии обработки. Вводимые данные нужно как можно скорее проверять, чтобы ошибок не возникало.

Конечно, такое не всегда возможно. Иногда проверять вводимые данные слишком дорого, иногда это не позволяет сделать архитектура кода или разделение ответственности. Но в таких случаях ошибки должны обрабатываться однозначно как исправимые. Иначе, допустим, ваша офисная программа будет падать из-за того, что вы нажали backspace в пустом документе, или ваша игра станет вылетать при попытке выстрелить из разряженного оружия.

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

Системные ошибки

Обычно системные ошибки нельзя предсказать. Более того, они недетерминистские и могут возникать в программах, которые до этого работали без нареканий. В отличие от пользовательских ошибок, зависящих исключительно от вводимых данных, системные ошибки — настоящие ошибки.

Но как их обрабатывать, как исправимые или неисправимые?

Это зависит от обстоятельств.

Многие считают, что ошибка нехватки памяти — неисправимая. Зачастую не хватает памяти даже для обработки этой ошибки! И тогда приходится просто сразу же прерывать выполнение.

Но падение программы из-за того, что ОС не может выделить сокет, — это не слишком дружелюбное поведение. Так что лучше бросить исключение и позволить catch аккуратно закрыть программу.

Но бросание исключения — не всегда правильный выбор.

Кто-то даже скажет, что он всегда неправильный.

Если вы хотите повторить операцию после её сбоя, то обёртывание функции в try-catch в цикле — медленное решение. Правильный выбор — возврат кода ошибки и цикличное исполнение, пока не будет возвращено правильное значение.

Если вы создаёте вызов API только для себя, то просто выберите подходящий для своей ситуации путь и следуйте ему. Но если вы пишете библиотеку, то не знаете, чего хотят пользователи. Дальше мы разберём подходящую стратегию для этого случая. Для потенциально неисправимых ошибок подойдёт «обработчик ошибок», а при других ошибках необходимо предоставить два варианта развития событий.

Обратите внимание, что не следует использовать подтверждения (assertions), включающиеся только в режиме отладки. Ведь системные ошибки могут возникать и в релизной сборке!

Программистские ошибки

Это худший вид ошибок. Для их обработки я стараюсь сделать так, чтобы мои ошибки были связаны только с вызовами функций, то есть с плохими параметрами. Прочие типы программистских ошибок могут быть пойманы только в runtime, с помощью отладочных макросов (assertion macros), раскиданных по коду.

При работе с плохими параметрами есть две стратегии: дать им определённое или неопределённое поведение.

Если исходное требование для функции — запрет на передачу ей плохих параметров, то, если их передать, это считается неопределённым поведением и должно проверяться не самой функцией, а оператором вызова (caller). Функция должна делать только отладочное подтверждение (debug assertion).

С другой стороны, если отсутствие плохих параметров не является частью исходных требований, а документация определяет, что функция будет бросать bad_parameter_exception при передаче ей плохого параметра, то передача — это хорошо определённое поведение (бросание исключения или любая другая стратегия обработки исправимых ошибок), и функция всегда должна это проверять.

В качестве примера рассмотрим получающие функции (accessor functions) std::vector<T>: в спецификации на operator[] говорится, что индекс должен быть в пределах валидного диапазона, при этом at() сообщает нам, что функция кинет исключение, если индекс не попадает в диапазон. Более того, большинство реализаций стандартных библиотек обеспечивают режим отладки, в котором проверяется индекс operator[], но технически это неопределённое поведение, оно не обязано проверяться.

Примечание: необязательно бросать исключение, чтобы получилось определённое поведение. Пока это не упомянуто в исходных условиях для функции, это считается определённым. Всё, что прописано в исходных условиях, не должно проверяться функцией, это неопределённое поведение.

Когда нужно проверять только с помощью отладочных подтверждений, а когда — постоянно?

К сожалению, однозначного рецепта нет, решение зависит от конкретной ситуации. У меня есть лишь одно проверенное правило, которому я следую при разработке API. Оно основано на наблюдении, что проверять исходные условия должен вызывающий, а не вызываемый. А значит, условие должно быть «проверяемым» для вызывающего. Также условие «проверяемое», если можно легко выполнить операцию, при которой значение параметра всегда будет правильным. Если для параметра это возможно, то это получается исходное условие, а значит, проверяется только посредством отладочного подтверждения (а если слишком дорого, то вообще не проверяется).

Но конечное решение зависит от многих других факторов, так что очень трудно дать какой-то общий совет. По умолчанию я стараюсь свести к неопределённому поведению и использованию только подтверждений. Иногда бывает целесообразно обеспечить оба варианта, как это делает стандартная библиотека с operator[] и at().

Хотя в ряде случаев это может быть ошибкой.

Об иерархии std::exception

Если в качестве стратегии обработки исправимых ошибок вы выбрали исключения, то рекомендуется создать новый класс и наследовать его от одного из классов исключений стандартной библиотеки.

Я предлагаю наследовать только от одного из этих четырёх классов:

  • std::bad_alloc: для сбоев выделения памяти.
  • std::runtime_error: для общих runtime-ошибок.
  • std::system_error (производное от std::runtime_error): для системных ошибок с кодами ошибок.
  • std::logic_error: для программистских ошибок с определённым поведением.

Обратите внимание, что в стандартной библиотеке разделяются логические (то есть программистские) и runtime-ошибки. Runtime-ошибки — более широкое определение, чем «системные». Оно описывает «ошибки, обнаруживаемые только при выполнении программы». Такая формулировка не слишком информативна. Лично я использую её для плохих параметров, которые не являются исключительно программистскими ошибками, а могут возникнуть и по вине пользователей. Но это можно определить лишь глубоко в стеке вызовов. Например, плохое форматирование комментариев в standardese приводит к исключению при парсинге, проистекающему из std::runtime_error. Позднее оно ловится на соответствующем уровне и фиксируется в логе. Но я не стал бы использовать этот класс иначе, как и std::logic_error.

Подведём итоги

Есть два пути обработки ошибок:

  • как исправимые: используются исключения или возвращаемые значения (в зависимости от ситуации/религии);
  • как неисправимые: ошибки журналируются, а программа прерывается.

Подтверждения — это особый вид стратегии обработки неисправимых ошибок, только в режиме отладки.

Есть три основных источника ошибок, каждый требует особого подхода:

  • Пользовательские ошибки не должны обрабатываться как ошибки на верхних уровнях программы. Всё, что вводит пользователь, должно проверяться соответствующим образом. Это может обрабатываться как ошибки только на нижних уровнях, которые не взаимодействуют с пользователями напрямую. Применяется стратегия обработки исправимых ошибок.
  • Системные ошибки могут обрабатываться в рамках любой из двух стратегий, в зависимости от типа и тяжести. Библиотеки должны работать как можно гибче.
  • Программистские ошибки, то есть плохие параметры, могут быть запрещены исходными условиями. В этом случае функция должна использовать только проверку с помощью отладочных подтверждений. Если же речь идёт о полностью определённом поведении, то функции следует предписанным образом сообщать об ошибке. Я стараюсь по умолчанию следовать сценарию с неопределённым поведением и определяю для функции проверку параметров лишь тогда, когда это слишком трудно сделать на стороне вызывающего.

Гибкие методики обработки ошибок в C++

Иногда что-то не работает. Пользователи вводят данные в недопустимом формате, файл не обнаруживается, сетевое соединение сбоит, в системе кончается память. Всё это ошибки, и их надо обрабатывать.

Это относительно легко сделать в высокоуровневых функциях. Вы точно знаете, почему что-то пошло не так, и можете обработать это соответствующим образом. Но в случае с низкоуровневыми функциями всё не так просто. Они не знают, что пошло не так, они знают лишь о самом факте сбоя и должны сообщить об этом тому, кто их вызвал.

В C++ есть два основных подхода: коды возврата ошибок и исключения. Сегодня широко распространено использование исключений. Но некоторые не могут / думают, что не могут / не хотят их использовать — по разным причинам.

Я не буду принимать чью-либо сторону. Вместо этого я опишу методики, которые удовлетворят сторонников обоих подходов. Особенно методики пригодятся разработчикам библиотек.

Проблема

Я работаю над проектом foonathan/memory. Это решение предоставляет различные классы выделения памяти (allocator classes), так что в качестве примера рассмотрим структуру функции выделения.

Для простоты возьмём malloc(). Она возвращает указатель на выделяемую память. Если выделить память не получается, то возвращается nullptr, то есть NULL, то есть ошибочное значение.

У этого решения есть недостатки: вам нужно проверять каждый вызов malloc(). Если вы забудете это сделать, то выделите несуществующую память. Кроме того, по своей натуре коды ошибок транзитивны: если вызвать функцию, которая может вернуть код ошибки, и вы не можете его проигнорировать или обработать, то вы тоже должны вернуть код ошибки.

Это приводит нас к ситуации, когда чередуются нормальные и ошибочные ветви кода. Исключения в таком случае выглядят более подходящим решением. Благодаря им вы сможете обрабатывать ошибки только тогда, когда вам это нужно, а в противном случае — достаточно тихо передать их обратно вызывающему.

Это можно расценить как недостаток.

Но в подобных ситуациях исключения имеют также очень большое преимущество: функция выделения памяти либо возвращает валидную память, либо вообще ничего не возвращает. Это функция «всё или ничего», возвращаемое значение всегда будет валидным. Это полезное следствие согласно принципу Скотта Майера «Make interfaces hard to use incorrectly and easy to use correctly».

Учитывая вышесказанное, можно утверждать, что вам следует использовать исключения в качестве механизма обработки ошибок. Этого мнения придерживается большинство разработчиков на С++, включая и меня. Но проект, которым я занимаюсь, — это библиотека, предоставляющая средства выделения памяти, и предназначена она для приложений, работающих в реальном времени. Для большинства разработчиков подобных приложений (особенно для игроделов) само использование исключений — исключение.

Каламбур детектед.

Чтобы уважить эту группу разработчиков, моей библиотеке лучше обойтись без исключений. Но мне и многим другим они нравятся за элегантность и простоту обработки ошибок, так что ради других разработчиков моей библиотеке лучше использовать исключения.

Так что же делать?

Идеальное решение: возможность включать и отключать исключения по желанию. Но, учитывая природу исключений, нельзя просто менять их местами с кодами ошибок, поскольку у нас не будет внутреннего кода проверки на ошибки — весь внутренний код опирается на предположение о прозрачности исключений. И даже если бы внутри можно было использовать коды ошибок и преобразовывать их в исключения, это лишило бы нас большинства преимуществ последних.

К счастью, я могу определить, что вы делаете, когда обнаруживаете ошибку нехватки памяти: чаще всего вы журналируете это событие и прерываете программу, поскольку она не может корректно работать без памяти. В таких ситуациях исключения — просто способ передачи контроля другой части кода, которая журналирует и прерывает программу. Но есть старый и эффективный способ передачи контроля: указатель функции (function pointer), то есть функция-обработчик (handler function).

Если у вас включены исключения, то вы просто их бросаете. В противном случае вызываете функцию-обработчика и затем прерываете программу. Это предотвратит бесполезную работу функции-обработчика, та позволит программе продолжить выполняться в обычном режиме. Если не прервать, то произойдёт нарушение обязательного постусловия функции: всегда возвращать валидный указатель. Ведь на выполнении этого условия может быть построена работа другого кода, да и вообще это нормальное поведение.

Я называю такой подход обработкой исключений и придерживаюсь его при работе с памятью.

Решение 1: обработчик исключений

Если вам нужно обработать ошибку в условиях, когда наиболее распространённым поведением будет «журналировать и прервать», то можно использовать обработчика исключений. Это такая функция-обработчик, которая вызывается вместо бросания объекта-исключения. Её довольно легко реализовать даже в уже существующем коде. Для этого нужно поместить управление обработкой в класс исключений и обернуть в макрос выражение throw.

Сначала дополним класс и добавим функции для настройки и, возможно, запрашивания функции-обработчика. Я предлагаю делать это так же, как стандартная библиотека обрабатывает std::new_handler:

class my_fatal_error
{
public:
    // тип обработчика, он должен брать те же параметры, что и конструктор,
    // чтобы у них была одинаковая информация
    using handler = void(*)( ... );

    // меняет функцию-обработчика
    handler set_handler(handler h);

    // возвращает текущего обработчика
    handler get_handler();

    ... // нормальное исключение
};

Поскольку это входит в область видимости класса исключений, вам не нужно именовать каким-то особым образом. Отлично, нам же легче.

Если исключения включены, то для удаления обработчика можно использовать условное компилирование (conditional compilation). Если хотите, то также напишите обычный подмешанный класс (mixin class), дающий требуемую функциональность.

Конструктор исключений элегантен: он вызывает текущую функцию-обработчика, передавая ей требуемые аргументы из своих параметров. А затем комбинирует с последующим макросом throw:

If```cpp #if EXCEPTIONS #define THROW(Ex) throw (Ex) #else #define THROW(Ex) (Ex), std::abort() #endif

> Такой макрос throw также предоставляется [foonathan/compatiblity](https://github.com/foonathan/compatibility).

Можно использовать его и так:

```cpp
THROW(my_fatal_error(...))

Если у вас включена поддержка исключений, то будет создан и брошен объект-исключение, всё как обычно. Но если поддержка выключена, то объект-исключение всё равно будет создан, и — это важно — только после этого произойдёт вызов std::abort(). А поскольку конструктор вызывает функцию-обработчика, то он и работает, как требуется: вы получаете точку настройки для журналирования ошибки. Благодаря же вызову std::abort() после конструктора пользователь не может нарушить постусловие.

Когда я работаю с памятью, то при включённых исключениях у меня также включён и обработчик, который вызывается при бросании исключения.

Так что при этой методике вам ещё будет доступна определённая степень кастомизации, даже если вы отключите исключения. Конечно, замена неполноценная, мы только журналируем и прерываем работу программы, без дальнейшего продолжения. Но в ряде случаев, в том числе при исчерпании памяти, это вполне пригодное решение.

А если я хочу продолжить работу после бросания исключения?

Методика с обработчиком исключений не позволяет этого сделать в связи с постусловием кода. Как же тогда продолжить работу?

Ответ прост — никак. По крайней мере, это нельзя сделать так же просто, как в других случаях. Нельзя просто так вернуть код ошибки вместо исключения, если функция на это не рассчитана.
Есть только одно решение: сделать две функции. Одна возвращает код ошибки, а вторая бросает исключения. Клиенты, которым нужны исключения, будут использовать второй вариант, остальные — первый.

Извините, что говорю такие очевидные вещи, но ради полноты изложения я должен был об этом сказать.

Для примера снова возьмём функцию выделения памяти. В этом случае я использую такие функции:

void* try_malloc(..., int &error_code) noexcept;

void* malloc(...);

При сбое выделения памяти первая версия возвращает nullptr и устанавливает error_code в коде ошибки. Вторая версия не возвращает nullptr, зато бросает исключение. Обратите внимание, что в рамках первой версии очень легко реализовать вторую:

void* malloc(...)
{
    auto error_code = 0;
    auto res = try_malloc(..., error_code);
    if (!res)
        throw malloc_error(error_code);
    return res;
}

Не делайте этого в обратной последовательности, иначе вам придётся ловить исключение, а это дорого. Также это не даст нам скомпилировать код без включённой поддержки исключений. Если сделаете, как показано, то можете просто стереть другую перегрузку (overload) с помощью условного компилирования.

Но даже если у вас включена поддержка исключений, клиенту всё равно может понадобиться вторая версия. Например, когда нужно выделить наибольший возможный объём памяти, как в нашем примере. Будет проще и быстрее вызывать в цикле и проверять по условию, чем ловить исключение.

Решение 2: предоставить две перегрузки

Если недостаточно обработчика исключений, то нужно предоставить две перегрузки. Одна использует код возврата, а вторая бросает исключение.

Если рассматриваемая функция не имеет возвращаемого значения, то можете её использовать для кода ошибки. В противном случае вам придётся возвращать недопустимое значение для сигнализирования об ошибке — как nullptr в вышеприведённом примере, — а также установить выходной параметр для кода ошибки, если хотите предоставить вызывающему дополнительную информацию.

Пожалуйста, не используйте глобальную переменную errno или что-то типа GetLastError()!

Если возвращаемое значение не содержит недопустимое значение для обозначения сбоя, то по мере возможности используйте std::optional или что-то похожее.

Перегрузка исключения (exception overload) может — и должна — быть реализована в рамках версии с кодом ошибки, как это показано выше. Если компилируете без исключений, сотрите перегрузку с помощью условного компилирования.

std::system_error

Подобная система идеально подходит для работы с кодами ошибок в С++ 11.

Она возвращает непортируемый (non-portable) код ошибки std::error_code, то есть возвращаемый функцией операционной системы. С помощью сложной системы библиотечных средств и категорий ошибок вы можете добавить собственные коды ошибок, или портируемые std::error_condition. Для начала почитайте об этом здесь. Если нужно, то можете использовать в функции кода ошибки std::error_code. А для функции исключения есть подходящий класс исключения: std::system_error. Он берёт std::error_code и применяется для передачи этих ошибок в виде исключений.

Эту или подобную систему должны использовать все низкоуровневые функции, являющиеся закрытыми обёртками ОС-функций. Это хорошая — хотя и сложная — альтернатива службе кодов ошибок, предоставляемой операционной системой.

Да, и мне ещё нужно добавить подобное в функции виртуальной памяти. На сегодняшний день они не предоставляют коды ошибок.

std::expected

Выше упоминалось о проблеме, когда у вас нет возвращаемого значения, содержащего недопустимое значение, которое можно использовать для сигнализирования об ошибке. Более того, выходной параметр — не лучший способ получения кода ошибки.

А глобальные переменные вообще не вариант!

В № 4109 предложено решение: std::expected. Это шаблон класса, который также хранит возвращаемое значение или код ошибки. В вышеприведённом примере он мог бы использоваться так:

std::expected<void*, std::error_code> try_malloc(...);

В случае успеха std::expected будет хранить не-null указатель памяти, а при сбое — std::error_code. Сейчас эта методика работает при любых возвращаемых значениях. Комбинация std::expected и функции исключения определённо допускает любые варианты использования.

Заключение

Если вы создаёте библиотеки, то иногда приходится обеспечивать максимальную гибкость использования. Под этим подразумевается и разнообразие средств обработки ошибок: иногда требуются коды возврата, иногда — исключения.

Одна из возможных стратегий — улаживание этих противоречий с помощью обработчика исключений. Просто удостоверьтесь, что когда нужно, то вызывается callback, а не бросается исключение. Это замена для критических ошибок, которая в любом случае будет журналироваться перед прерыванием работы программы. Как таковой этот способ не универсален, вы не можете переключаться в одной программе между двумя версиями. Это лишь обходное решение при отключённой поддержке исключений.

Более гибкий подход — просто предоставить две перегрузки, одну с исключениями, а вторую без. Это даст пользователям максимальную свободу, они смогут выбирать ту версию, что лучше подходит в их ситуации. Недостаток этого подхода: вам придётся больше потрудиться при создании библиотеки.

Слайд 2Полиморфизм является необходимым средством при обеспечении расширяемости типов в

приложениях.

Полиморфизм является необходимым средством при обеспечении расширяемости типов в приложениях.


Слайд 3Виртуальные методы
Полиморфизм позволяет не переделывать структуру программы
Если возникает необходимость

определения нового типа данных, например нового типа геометрической фигуры
Использование полиморфизма

необходимо, если процедура построения фигур работает с фиксированным набором объектов, описанных в модуле, а пользователь, работающий с модулем, хочет определить новый тип фигуры

Для этого применяются виртуальные методы.

Виртуальные методыПолиморфизм позволяет не переделывать структуру программыЕсли возникает необходимость определения нового типа данных, например нового типа геометрической


Слайд 4Виртуальные методы
Метод становится виртуальным, если за его объявлением в

типе объекта стоит зарезервированное слово VIRTUAL.

Виртуальные методыМетод становится виртуальным, если за его объявлением в типе объекта стоит зарезервированное слово VIRTUAL.


Слайд 5Необходимо помнить, что если метод объявлен в родительском типе

как VIRTUAL, то
все методы с аналогичными именами в дочерних

типах
также должны объявляться виртуальными,
во избежание ошибки компилятора.

Необходимо помнить, что если метод объявлен в родительском типе как VIRTUAL, то все методы с аналогичными именами


Слайд 6Виртуальные методы
UNIT New_persons;
INTERFACE
TYPE
PPerson = ^TPerson;
TPerson = OBJECT

Name : STRING[30];
Date : STRING[10];
Rate

: REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt:REAL);
DESTRUCTOR Done; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END;

PStudent = ^TStudent;
TStudent = OBJECT(TPerson)
Ball : REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt,Bl:REAL);
DESTRUCTOR Done; VIRTUAL;
FUNCTION GetSum : REAL; VIRTUAL;
PROCEDURE ShowSum; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END;
PStaff = ^TStaff;
TStaff = OBJECT(TPerson)
Bonus : REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt,Bn:REAL);
DESTRUCTOR Done; VIRTUAL;
FUNCTION GetSum : REAL; VIRTUAL;
PROCEDURE ShowSum; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END;

PTeacher = ^ТТеаcher;
TTeacher = OBJECT(TStaff)
Hours : WORD;
HourRate : REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt,Bn,Hrt:REAL; Hr:W0RD);
DESTRUCTOR Done; VIRTUAL;
FUNCTION GetSum : REAL; VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END;

Например:

Виртуальные методыUNIT New_persons;INTERFACETYPE PPerson = ^TPerson; TPerson = OBJECT  Name 	: STRING[30];  Date 	: STRING[10];


Слайд 7Виртуальные методы
Обратите внимание, что метод ShowSum, показанный для типа

TTeacher, теперь удален из его определения.
Типу ТТеасher уже не

нужно переопределять метод ShowSum типа TStaff.
Вместо этого ShowSum может просто наследоваться от TStaff со всеми вложенными в него вызовами, которые в этом случае уже будут вызывать методы из TTeacher, а не из TStaff, как это происходило в полностью статической иерархии объектов.

Виртуальные методыОбратите внимание, что метод ShowSum, показанный для типа TTeacher, теперь удален из его определения. Типу ТТеасher


Слайд 8Виртуальные методы
Обратите внимание на зарезервированное слово CONSTRUCTOR (конструктор), заменившее

зарезервированное слово PROCEDURE для процедур Init.
Конструктор является специальным типом процедуры,

которая выполняет некоторую установочную работу для механизма виртуальных методов.

Конструктор должен вызываться перед вызовом любого виртуального метода.

Вызов виртуального метода без предварительного вызова конструктора может привести к блокированию системы.
У компилятора нет способа проверить порядок вызова методов.

Виртуальные методыОбратите внимание на зарезервированное слово CONSTRUCTOR (конструктор), заменившее зарезервированное слово PROCEDURE для процедур Init.Конструктор является специальным


Слайд 9Виртуальные методы
Каждый тип объекта, имеющий виртуальные методы, обязан иметь

конструктор.
Понятие DESTRUCTOR (деструктор), обратное понятию CONSTRUCTOR, будет объяснено далее.
Каждый экземпляр

объекта должен инициализироваться отдельным вызовом конструктора.

Недостаточно инициализировать один экземпляр объекта и затем присваивать этот экземпляр другим.
Другие экземпляры, даже если они содержат правильные данные, не будут инициализированы оператором присваивания и заблокируют систему при любых вызовах их виртуальных методов.

Виртуальные методыКаждый тип объекта, имеющий виртуальные методы, обязан иметь конструктор.Понятие DESTRUCTOR (деструктор), обратное понятию CONSTRUCTOR, будет объяснено


Слайд 10Виртуальные методы
Например:
VAR
One,Two: TPerson;
BEGIN
One.Init(‘Петр Петров’,’25-06-1995′ ,400000);
Two := One;

{Неправильный вызов!}
END;

Виртуальные методыНапример:VAR	One,Two:  TPerson; BEGIN	One.Init('Петр Петров','25-06-1995' ,400000);	Two := One;  {Неправильный вызов!} END;


Слайд 11Виртуальные методы
Каждый тип объекта, содержащий виртуальные методы, имеет таблицу

виртуальных методов (ТВМ), хранящуюся в сегменте данных.
ТВМ содержит размер

типа объекта и для каждого виртуального метода указатель кода, исполняющий данный метод.
Конструктор устанавливает связь между вызывающим его экземпляром объекта и ТВМ этого объекта.

Необходимо учесть, что имеется только одна ТВМ для каждого типа объекта.

Отдельные экземпляры объекта (т.е. переменные Данного типа) содержат только адрес ТВМ, но не саму ТВМ.

Конструктор устанавливает значение адреса ТВМ.

Вызов виртуального метода до вызова конструктора приведет к ошибке т.к. к этому моменту поле адреса ТВМ еще не инициализировано и содержит неопределенный адрес.

Виртуальные методыКаждый тип объекта, содержащий виртуальные методы, имеет таблицу виртуальных методов (ТВМ), хранящуюся в сегменте данных. ТВМ


Слайд 12Виртуальные методы
При отладке программы для контроля правильности вызовов виртуальных

методов можно использовать директиву компилятора $R.
Если директива $R находится во

включенном состоянии {$R+}; то все вызовы виртуальных методов будут проверяться на состояние инициализации объекта, выполняющего вызов метода.
Если выполняющий вызов объект еще не был инициализирован конструктором, то произойдет ошибка выполнения.

Отрицательной стороной использования данной директивы является замедление работы программы.

Это происходит из-за того, что при каждом вызове виртуального метода дополнительно выполняется процедура проверки правильности инициализации.

Если скорость работы программы является критическим параметром, то рекомендуется использовать проверку виртуальных методов только на этапе отладки.

Виртуальные методыПри отладке программы для контроля правильности вызовов виртуальных методов можно использовать директиву компилятора $R.Если директива $R


Слайд 13Виртуальные методы
Как только родительский тип объекта объявит метод виртуальным,

все его потомки также должны объявить этот метод виртуальным.

Статический

метод никогда не может переопределить виртуальный метод.

Если Вы попытаетесь сделать это, компилятор выдаст сообщение об ошибке.

После того, как метод стал виртуальным, его заголовок не может изменяться в объектах более низкого уровня иерархии.

Определение виртуального метода можно представить как шаблон для всех родственных ему методов. Поэтому заголовки всех реализаций одного и того же виртуального метода должны быть идентичными, включая число параметров и их типы.

Статический метод, переопределяющий одноименный метод предка, может иметь другое число параметров и другие типы при необходимости

Виртуальные методыКак только родительский тип объекта объявит метод виртуальным, все его потомки также должны объявить этот метод


Слайд 14Расширяемость объектов
Преимуществом использования виртуальных методов является то, что типы

объектов и методы, определенные в модуле, могут поставляться пользователю в

виде TPU-файла, т.е. без исходного кода.

Для работы с объектами модуля необходимо знать содержание только интерфейсной части модуля.
Используя полиморфные объекты и виртуальные методы, пользователь TPU-файла может свободно добавлять новые методы к уже существующим.

Новое понятие, связанное с добавлением новых функциональных характеристик в программу без модификации ее исходного кода, называется способностью к расширению.

Способность к расширению является естественным продолжением наследования

Наследуются все свойства, которыми обладают порождающие типы, а затем добавляются новые по мере необходимости.
Позднее связывание позволяет новые методы связать с уже существующими во время выполнения программы, благодаря чему расширение существующего кода выглядит как бы невидимым, требуя лишь небольшого увеличения таблицы виртуальных методов.

Расширяемость объектовПреимуществом использования виртуальных методов является то, что типы объектов и методы, определенные в модуле, могут поставляться


Слайд 15Преимущества и недостатки виртуальных методов
Рекомендуется делать методы виртуальными.

Использование статических

методов имеет смысл, если требуется получить оптимальную эффективность скорости выполнения

и использования памяти. Однако в этом случае теряется возможность расширения.

Иногда не известно, является метод виртуальным или нет.

В таких случаях лучше определить его виртуальным, особенно, если имеется предположение, что метод будет перекрываться кем-либо из потомков, а его код должен быть доступным в дальнейшем.

Если у объекта есть виртуальные методы, то для него будет создана ТВМ и любой экземпляр этого объекта будет с ней связан.

Каждый вызов виртуального метода должен проходить через ТВМ, тогда как статические методы вызываются непосредственно.
Хотя просмотр ТВМ весьма эффективен, вызов статического метода все равно остается более быстрым, чем вызов виртуального.

Всегда необходимо учитывать возможность последующей модификации программы.

Дополнительная скорость и эффективное использование памяти для статических методов должны уравновешиваться гибкостью, которая присуща виртуальным методам, ведь имеющийся код можно расширить спустя большой промежуток времени с момента его компиляции.

Преимущества и недостатки виртуальных методовРекомендуется делать методы виртуальными.Использование статических методов имеет смысл, если требуется получить оптимальную эффективность


Слайд 16Динамические объекты
Точно так же, как и любые типы данных

в Паскале, объекты можно размещать в динамической памяти и работать

с ними, применяя указатели.

Одним из самых простых способов размещения объектов в памяти является использование процедуры New, традиционно применяемой для работы с указателями:

VAR
Sum : REAL;
P : ^TPerson;

New(P);

Динамические объектыТочно так же, как и любые типы данных в Паскале, объекты можно размещать в динамической памяти


Слайд 17Динамические объекты
Если динамический объект содержит виртуальные методы, он должен

инициализироваться с помощью вызова конструктора, перед тем как будет вызван

любой из его методов:

Р^.Init(‘Иван Петров’,’25-06-1995′ ,40000);

Затем вызовы методов могут происходить в обычном порядке, с использованием имени указателя и ссылочного символа ^ вместо имени экземпляра объекта, которые использовались бы при обращении к статически размещенному объекту:

Sum := P^.GetSum;

Динамические объектыЕсли динамический объект содержит виртуальные методы, он должен инициализироваться с помощью вызова конструктора, перед тем как


Слайд 18Расширенное использование оператора New.
Специально для работы с динамическими объектами

Турбо Паскаль включает несколько усовершенствованных процедур для размещения и удаления

объектов из памяти наиболее эффективными способами.

Процедура New может вызываться с двумя параметрами: имя указателя используется в качестве первого параметра, а имя конструктора — в качестве второго параметра:

New(P, Init (‘Иван Петров’,’25-06-1995′,40000));

Расширенное использование оператора New.Специально для работы с динамическими объектами Турбо Паскаль включает несколько усовершенствованных процедур для размещения


Слайд 19Расширенное использование оператора New.
При использовании расширенного синтаксиса процедуры New

конструктор Init выполняет динамическое размещение объекта, используя специальный сгенерированный код,

вызываемый оператором CONSTRUCTOR и выполняемый до основного кода конструктора.

Имя экземпляра объекта не может использоваться в качестве первого параметра процедуры, т.к. во время вызова процедуры New экземпляр, инициализируемый с помощью Init, еще не существует.

Компилятор определяет правильность вызова конструктора, проверяя тип указателя, передаваемого в качестве первого параметра.

Расширенное использование оператора New.При использовании расширенного синтаксиса процедуры New конструктор Init выполняет динамическое размещение объекта, используя специальный


Слайд 20Расширенное использование оператора New.
Процедура New также может использоваться в

качестве функции, которая возвращает значение указателя.
Передаваемый New параметр на этот

раз должен быть типом указателя на объект,
а не самим указателем:

TYPE
PPerson = ^ТPerson;
VAR
Р : PPerson;
. . . .
Р:= New(PPerson);

В качестве второго параметра процедура New может содержать конструктор объектного типа:

Р:=New(PPerson,Init(‘Иван Петров’,’25-06-1995′,40000));

Расширенное использование оператора New.Процедура New также может использоваться в качестве функции, которая возвращает значение указателя.Передаваемый New параметр


Слайд 21Обнаружение ошибок конструктора
Турбо Паскаль позволяет установить пользовательскую функцию обработки

ошибок динамической памяти с помощью переменной HeapError, которая является стандартной

и не требует описания в разделе переменных.
Она содержит адрес стандартной функции обработки ошибок в Паскале, которая может быть замещена.

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

FUNCTION HeapFunc(Size:Word): INTEGER; FAR;

Наличие директивы FAR обязательно

Обнаружение ошибок конструктораТурбо Паскаль позволяет установить пользовательскую функцию обработки ошибок динамической памяти с помощью переменной HeapError, которая


Слайд 22Обнаружение ошибок конструктора
Новая функция обработки ошибок устанавливается путем присваивания

ее адреса переменной НеарЕrrоr следующим образом:
HeapError:= @HeapFunc;
Такая возможность может оказаться

полезной при использовании конструкторов.

Обнаружение ошибок конструктораНовая функция обработки ошибок устанавливается путем присваивания ее адреса переменной НеарЕrrоr следующим образом:HeapError:= @HeapFunc;Такая возможность


Слайд 23Обнаружение ошибок конструктора
По умолчанию, если не хватает памяти для

размещения экземпляра динамического объекта, вызов конструктора, использующий расширенный синтаксис стандартной

процедуры New, генерирует фатальную ошибку выполнения с кодом 203.

Программа прекращает свою работу.

Если устанавливается пользовательская функция обработки ошибок динамической памяти, которая возвращает 1, а не стандартный результат 0, то вызов конструктора через New будет возвращать NIL в том случае, если конструктор не сможет завершить запрос .

Программа продолжает работать.

Обнаружение ошибок конструктораПо умолчанию, если не хватает памяти для размещения экземпляра динамического объекта, вызов конструктора, использующий расширенный


Слайд 26Обнаружение ошибок конструктора
Для реализации изложенного выше механизма, Турбо Паскаль

предоставляет новую стандартную процедуру Fail, которая не имеет параметров и

которая может вызываться только изнутри конструктора.

Вызов Fail заставляет конструктор удалить динамический экземпляр, который был размещен при входе в конструктор, и приводит к возврату указателя NIL для индикации неудачной попытки.

Обнаружение ошибок конструктораДля реализации изложенного выше механизма, Турбо Паскаль предоставляет новую стандартную процедуру Fail, которая не имеет


Слайд 27Обнаружение ошибок конструктора
Если динамические экземпляры размещаются с помощью расширенного

синтаксиса New, то результирующее значение NIL, передаваемое указателю, свидетельствует о

неудачной операции.

Но нет таких переменных типа указатель, которые можно было бы проверить после создания статического экземпляра или после вызова унаследованного конструктора.

Турбо Паскаль в качестве функций позволяет использовать конструкторы, которые возвращают результат типа BOOLEAN.

Возвращаемое значение TRUE означает успех, a FALSE — неудачу, благодаря вызову Fail внутри конструктора.

Обнаружение ошибок конструктораЕсли динамические экземпляры размещаются с помощью расширенного синтаксиса New, то результирующее значение NIL, передаваемое указателю,


Слайд 28Обнаружение ошибок конструктора
Рассмотрим, как можно описать последовательный вызов конструкторов

типа TPerson, TStaff и TTeacher.

Обнаружение ошибок конструктораРассмотрим, как можно описать последовательный вызов конструкторов типа TPerson, TStaff и TTeacher.


Слайд 29Обнаружение ошибок конструктора
пример конструкторов не использующих обнаружение ошибок:
CONSTRUCTOR TPerson.Init(Nm,Dt

: STRING; Rt : REAL);
BEGIN
Name:= Nm;
Date:= Dt;
Rate:= Rt;
END;

CONSTRUCTOR

TStaff.Init(Nm,Dt : STRING; Rt,Bn : REAL);
BEGIN
TPerson.Init(Nm,Dt,Rt) ;
Bonus:= Bn;
END;

CONSTRUCTOR TTeacher.Init(Nm,Dt:STRING; Rt,Bn,Hrt:REAL; Hr:WORD);
BEGIN
TStaff.Init(Nm,Dt,Rt,Bn);
Hours:= Hr;
HourRate:= Hrt;
END;

В случае инициализации экземпляр типа TTeacher, он вызывает конструктор типа TStaff, который в свою очередь вызывает конструктор типа TPerson. Если произойдет ошибка при вызове последнего конструктора, придется отменить вызовы всех трех конструкторов.

Обнаружение ошибок конструкторапример конструкторов не использующих обнаружение ошибок:CONSTRUCTOR TPerson.Init(Nm,Dt : STRING; Rt : REAL); BEGIN	Name:= Nm;	Date:= Dt;	Rate:=


Слайд 30Обнаружение ошибок конструктора
можно переписать конструкторы типа TStaff и ТТеасher

с учетом обнаружения ошибок:
CONSTRUCTOR TStaff.Init(Nm,Dt : STRING; Rt,Bn : REAL);

BEGIN
IF NOT TPerson.Init(Nm,Dt,Rt) THEN Fail;
Bonus := Bn;
END;

CONSTRUCTOR TTeacher.Init (Nm,Dt: STRING; Rt,Bn,Hrt:REAL; Hr:WORD);
BEGIN
IF NOT TStaff.Init(Nm,Dt,Rt,Bn) THEN Fail;
Hours:= Hr;
HourRate:= Hrt;
END;

FUNCTION HeapFunc(Size: WORD): INTEGER; FAR;
BEGIN
HeapFunc:= 1;
END;

Обратите внимание на то, что функция HeapFunc выполняет одну единственную операцию: при любом вызове возвращает 1.
В выполнении других операций в данном случае нет необходимости.

Обнаружение ошибок конструктораможно переписать конструкторы типа TStaff и ТТеасher с учетом обнаружения ошибок:CONSTRUCTOR TStaff.Init(Nm,Dt : STRING; Rt,Bn


Слайд 31Полезная информация
В приведенных примерах вложенные вызовы конструкторов осуществляются путем

указания имени предка, за которым следует точка и имя конструктора.
Турбо

Паскаль предоставляет специальное слово INHERITED, используя которое, можно вызывать методы предка без указания имени типа предка и точки

Это может пригодиться при использовании большой иерархии объектов, когда запомнить все связи типа «предок-потомок» невозможно.

CONSTRUCTOR TStaff.Init(Nm,Dt:STRING; Rt,Bn:REAL);
BEGIN
IF NOT INHERITED Init(Nm,Dt,Rt) THEN Fail;
Bonus:= Bn;
END;

Полезная информацияВ приведенных примерах вложенные вызовы конструкторов осуществляются путем указания имени предка, за которым следует точка и


Слайд 32Деструкторы
Подобно другим типам данных, размещаемые в динамической памяти объекты

могут удаляться в случае необходимости с помощью процедуры Dispose
Однако при

удалении ненужного объекта может понадобиться нечто большее, чем простое освобождение занимаемой им динамической памяти.

Объект может содержать указатели на динамические структуры или объекты, которые нужно освободить или очистить в определенном порядке, особенно если используется сложная динамическая структура данных.

Все операции, необходимые для очистки динамического объекта, должны объединяться в один метод таким образом, чтобы объект мог быть уничтожен с помощью единственного вызова: Person.Done;

ДеструкторыПодобно другим типам данных, размещаемые в динамической памяти объекты могут удаляться в случае необходимости с помощью процедуры


Слайд 33Деструкторы
Метод Done должен инкапсулировать все детали очистки своего объекта,

а также всех структур данных и вложенных объектов.
Для обозначения таких

методов обычно рекомендуется использовать идентификатор Done.

Часто полезно и допустимо определять несколько методов очистки для данного типа объекта.

В зависимости от их размещения или использования, или в зависимости от состояния и режима на момент очистки сложные объекты могут потребовать очистки несколькими различными путями.

Турбо Паскаль предоставляет специальный тип метода, называемый «сборщиком мусора» или деструктором, для очистки и удаления динамических объектов.

Деструктор объединяет этап удаления объекта с другими действиями или задачами, необходимыми для данного типа объекта.
Для одного типа объекта можно определить несколько деструкторов.

ДеструкторыМетод Done должен инкапсулировать все детали очистки своего объекта, а также всех структур данных и вложенных объектов.Для


Слайд 34Деструкторы
Деструктор размещается вместе с другими методами объекта в определении

типа объекта:
ТУРЕ
TPerson = OBJECT
Name: STRING[30];
Date: STRING[10];
Rate: REAL;
CONSTRUCTOR Init(Nm,Dt:STRING; Rt:REAL);
DESTRUCTOR Done;

VIRTUAL;
PROCEDURE ShowAll; VIRTUAL;
END;

ДеструкторыДеструктор размещается вместе с другими методами объекта в определении типа объекта:ТУРЕ	TPerson = OBJECT		Name: STRING[30];		Date: STRING[10];		Rate: REAL;		CONSTRUCTOR Init(Nm,Dt:STRING;


Слайд 35Деструкторы
Деструкторы можно наследовать, и они могут быть либо статическими,

либо виртуальными.
Поскольку различные программы завершения обычно требуют различных типов объектов,

рекомендуется всегда определять деструкторы виртуальными, чтобы для каждого типа объекта выполнялся правильный деструктор.

Нет необходимости указывать зарезервированное слово DESTRUCTOR для каждого метода очистки, даже если определение типа объекта содержит виртуальные методы.

Деструкторы, в действительности, работают только с динамическими объектами.
При очистке таких объектов деструктор осуществляет некоторые специальные функции, гарантируя, что в динамической памяти всегда будет освобождаться правильное число байтов.

Применение деструктора к статическим объектам также не является ошибкой и не может привести к некорректной работе программы.

ДеструкторыДеструкторы можно наследовать, и они могут быть либо статическими, либо виртуальными.Поскольку различные программы завершения обычно требуют различных


Слайд 36Деструкторы
Основное преимущество использования деструктора заключается в удалении из памяти

полиморфных объектов.
Полиморфными являются те объекты, которые были присвоены экземпляру родительского

типа, благодаря правилам совместимости типов.

Экземпляр объекта типа TStudent, присвоенный переменной типа TPerson, является примером полиморфного объекта.

Эти правила могут применяться и к указателям на объекты

Указатель на TStudent может свободно присваиваться указателю на TPerson, а адресуемый им объект также будет полиморфным.

ДеструкторыОсновное преимущество использования деструктора заключается в удалении из памяти полиморфных объектов.Полиморфными являются те объекты, которые были присвоены


Слайд 37Деструкторы
Термин «полиморфный» означает, что компилятор, строя код объекта, во

время компиляции точно не знает, какой тип объекта будет в

действительности использован.

Единственное, что он знает, это то, что объект принадлежит иерархии объектов, являющихся потомками указанного типа предка.

Очевидно, что размеры типов объектов отличаются.

Во время компиляции из полиморфного объекта нельзя извлечь какую-либо информацию о его размере.

ДеструкторыТермин


Слайд 38Деструкторы
Информация о размере удаляемого объекта становится доступной для деструктора

в момент удаления, благодаря обращению к тому месту, где эта

информация записана, т.е. к таблице виртуальных методов экземпляров объектов определенного типа.

В каждой ТВМ содержится размер в байтах данного типа объекта.

Таблица виртуальных методов любого объекта доступна через скрытый параметр Self, содержащий адрес ТВМ, который передается методу при вызове.

Деструктор также является методом, поэтому, когда объект вызывает его, деструктор получает копию Self через стек.

Таким образом, если объект является полиморфным во время компиляции, он никогда не будет полиморфным во время выполнения, благодаря позднему связыванию.

ДеструкторыИнформация о размере удаляемого объекта становится доступной для деструктора в момент удаления, благодаря обращению к тому месту,


Слайд 39Деструкторы
Для выполнения освобождения памяти при позднем связывании деструктор нужно

вызывать как часть расширенного синтаксиса процедуры Dispose: Dispose(Р,Done);

ДеструкторыДля выполнения освобождения памяти при позднем связывании деструктор нужно вызывать как часть расширенного синтаксиса процедуры Dispose: Dispose(Р,Done);


Слайд 41Полезная информация
Вызов деструктора вне процедуры Dispose не приведет к

автоматическому освобождению памяти.
Сам по себе метод деструктора может быть пустым

и выполнять только функцию связи с процедурой Dispose, поскольку основная информация содержится не в теле деструктора, а связана с его заголовком, содержащим слово DESTRUCTOR.

Деструктор дочернего типа должен последним действием вызывать соответствующий деструктор своего непосредственного предка, чтобы освободить поля всех наследуемых указателей объекта

DESTRUCTOR TPerson.Done;
BEGIN
END;

DESTRUCTOR TStaff.Done;
BEGIN
INHERITED Done;
END;

Полезная информацияВызов деструктора вне процедуры Dispose не приведет к автоматическому освобождению памяти.Сам по себе метод деструктора может


Слайд 42Динамические методы.
В Турбо Паскале имеется дополнительный класс методов позднего

связывания, которые называются динамическими.
Фактически, динамические методы являются подклассом виртуальных

методов и отличаются от них только способом вызова на этапе выполнения.
Во всех других отношениях динамический метод можно рассматривать как эквивалентный виртуальному.

Динамические методы.В Турбо Паскале имеется дополнительный класс методов позднего связывания, которые называются динамическими. Фактически, динамические методы являются


Слайд 43Динамические методы.
Описание динамического метода аналогично описанию виртуального за исключением

того, что оно должно включать в себя индекс динамического метода,

который указывается сразу за ключевым словом VIRTUAL.

Индекс динамического метода должен задаваться целочисленной константой в диапазоне значений от 1 до 65535 и представлять собой уникальное значение среди индексов других динамических методов, содержащихся в объектном типе или его предках.
Например: FUNCTION GetSum:REAL; VIRTUAL 10;

Переопределение динамического метода должно точно соответствовать порядку, типу и именам параметров, а также типу результата функции, если метод является функцией.

Переопределение должно включать в себя директиву VIRTUAL, за которой следует тот же индекс динамического метода, что и заданный в объектном типе предка.

Динамические методы.Описание динамического метода аналогично описанию виртуального за исключением того, что оно должно включать в себя индекс


Слайд 44Динамические методы.
Использование динамических методов целесообразно при создании длинной иерархии

объектов с большим количеством виртуальных методов.
Для виртуальных методов в иерархии

будут создаваться очень длинные ТВМ с указанием всех виртуальных методов предков, хотя переопределяться может только часть из них.
Это требует большого объема используемой памяти для хранения ТВМ.

При использовании динамических методов создается альтернативная таблице виртуальных методов таблица динамических методов (ТДМ).

В которой указываются только те виртуальные методы, которые переопределяются, что позволяет экономить память.

Динамические методы.Использование динамических методов целесообразно при создании длинной иерархии объектов с большим количеством виртуальных методов.Для виртуальных методов


Понравилась статья? Поделить с друзьями:
  • Обнаружена улучшенная версия приложения бильярд что делать как исправить
  • Обнаружена следующая ошибка указанная служба была отмечена для удаления 0x80070430
  • Обнаружена проблема универсальный аудио драйвер на windows 10 как исправить
  • Ночь ошибок фильм 1974 скачать торрент
  • Ночь ошибок телеспектакль 1974