AbstractErrorProc — Переменная Delphi


AbstractErrorProc — Переменная Delphi

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

Все это мы видели на примерах в предыдущем разделе. Но представим себе такую задачу. Вы объявили в приложении массив объектов типа TPerson:
var PersArray: array[1..10] of TPerson;

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

for i:=l to 10 do Memol.Lines.Add(PersArray[i].PersonToStr);

Или аналогичная задача с использованием списка TList:
объявлена переменная var List: TList; в нее заносятся указатели на объекты классов TStudent и TEmpl, а затем вы хотите пройти в цикле элементы этого списка и отобразить их в окне Memo:

for i:=0 to List.Count — 1 do Memol.Lines.Add(TPerson(List[i]).PersonToStr);

Реализуйте один из этих вариантов (не забудьте при завершении приложения удалять объекты из памяти), и посмотрите, что получится. А получится очевидный результат: во всех случаях сработает метод PersonToStr родительского класса TPerson, так как соответствующие объекты объявлены объектами этого класса. И программа, естественно, не знает, объекты какого класса хранятся в массиве или в списке.

Конечно, можно усложнить код, проверять каждый раз операцией is истинный класс объекта, и указывать операцией as этот класс (см. об этих операциях в разд. 3.5.4). Для массива это будет выглядеть так:

А для списка аналогичный код имеет вид:

Задача будет решена, но некрасиво. Во-первых, код усложняется. А во-вторых, если вы впоследствии решите создать какие-то новые классы, производные от TPerson, вам придется вводить соответствующие дополнительные проверки в эти коды.

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

будут автоматически вызывать методы PersonToStr того класса (TStudent, TEmpl или других производных от TPerson), к которому относится каждый объект. Такой подход, облегчающий работу с множеством родственных объектов, называется полиморфизмом.

Сделать метод родительского класса виртуальным очень просто. При объявлении в классе виртуальных методов после точки с занятой, завершающей объявление метода, добавляется ключевое слово virtual. Например, чтобы объявить в базовом классе TPerson метод PersonToStr виртуальным, надо в его объявление добавить слово virtual:

function PersonToStr: string; virtual;

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

function PersonToStr: string; override;
И это все! Методы стали виртуальными и приведенные в начале данного раздела операторы, будут работать безо всяких проверок if.

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

function PersonToStr: string; overr > В родительском классе виртуальный метод не обязательно должен быть реализован. Такой виртуальный метод, реализация которого не определена в том классе, в котором он объявлен, называется абстрактным. Предполагается, что этот метод будет перегружен в классах-наследниках. Только в тех классах, в которых он перегружен, его и можно вызывать.

Объявляется абстрактный метод с помощью ключевого слова abstract после слова virtual. Например, вы можете в классе TPerson объявить метод PersonToStr следующим образом:

function PersonToStr: string; virtual; abstract;

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

Удачи!
Встретимся в следующем уроке!

Источник: www.thedelphi.ru
Автор: Савельев Александр
Опубликовано: 14 Июля 2013
Просмотров:

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

AbstractErrorProc — Переменная Delphi

Профессиональная разработка приложений с помощью Delphi5

Часть 3. Оформление приложений для Windows95/98/NT/2000 в Delphi

© Сергей Трепалин
УКЦ Interface Ltd.
КомпьютерПресс #3 2001
Статья была опубликована в КомпьютерПресс

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

Объектно-ориентированное программирование (ООП) – это основа для создания приложений в Windows. В 80-х годах материалов, посвященных объектно-ориентированному программированию, публиковалось довольно много. Несмотря на то что за прошедшее время в синтаксисе объектно-ориентированных языков программирования произошли существенные изменения и появились новые возможности, авторы современных учебных пособий почему-то не уделяют этому вопросу должного внимания. Мы решили восполнить этот пробел. В данной публикации в первую очередь рассматривается то новое, что появилось в Delphi по мере совершенствования этого продукта.

Материалы, изложенные в настоящем разделе (если это особо не оговаривается) применимы как к Delphi, так и к C++Builder (за исключением синтаксиса).

Современное название объекта – класс; термин «объект» иногда используется для обозначения рабочей копии (экземпляра) класса.

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

Каждому классу, за исключением самого первого, должен предшествовать класс-родитель. В свою очередь, любой класс можно использовать для создания других классов, и он в этом случае будет являться их родителем. В Delphi у класса бывает только один родитель, в C++ родителей может быть несколько. Поэтому в Delphi классы образуют иерархическое дерево с классом TObject в роли корня. Иерархию классов в Delphi можно проследить при вызове команды View/Browser после успешной компиляции какого-либо проекта.

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

Иерархическое дерево реализуется довольно просто: при определении в среде-разработке нового класса указывается класс-предок:

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

Такое объявление однозначно определяет место нового класса в иерархии классов. Кроме того, оно означает следующее: все переменные и все методы класса-предка копируются в новый класс. Простым объявлением TMyListBox=class(TListBox) мы получили новый класс, который обладает всеми свойствами списка: в него можно добавлять строки, он будет показан на форме, при необходимости на нем автоматически появится вертикальная полоса прокрутки и т.д. Таким образом, при продвижении по ветвям иерархического дерева происходит накопление переменных и методов. Например, класс TWinControl имеет все переменные и методы, определенные в классах TControl, TComponent, TPersistent и TObject.

Самый простой класс – TObject — не имеет предка. Он также не имеет полезных при обычном написании приложений методов и переменных. Однако он играет важнейшую роль в поведении объектов.

Объявление переменных и методов класса

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

TMy > public
FName:string;
FAge:integer;
end;

В класс TMyClass к переменным класса TObject добавлены две новые переменные: FName и FAge. Названия переменных в классе принято (но не обязательно) начинать с буквы F от слова field. Классовые переменные, определенные внутри класса, отличаются от глобальных (служебное слово var в секции interface или implementation модуля) и локальных (служебное слово var в процедуре или функции) переменных. При загрузке приложения память для глобальных переменных выделяется немедленно и освобождается по завершении приложения. Локальным же переменным память выделяется в стеке при вызове метода, и после завершения работы метода эти ресурсы возвращаются системе.Так, если была объявлена, например, одна глобальная (или локальная) переменная типа N:integer, то резервируется 4 байта памяти, куда можно поместить одно значение. При объявлении же классовых переменных во время загрузки приложения не выделяется память для их хранения – она выделяется только при создании экземпляра класса после вызова конструктора (см. ниже). Поскольку экземпляров класса может быть несколько, в работающем приложении может быть несколько копий классовых переменных (в том числе и нулевое количество). Соответственно в каждой из этих переменных могут храниться различающиеся данные. Этим и определяется отличие классовых переменных от глобальных и локальных – для последних имеется только одна копия. Еще одной интересной особенностью классовых переменных является то, что при создании экземпляра класса они инициализируются нулями (то есть все их биты заполняются нулями). Поэтому если такая переменная представляет собой указатель, то он равен nil, если целое число, то оно равно 0, если логическую переменную, то она равна False. Локальные и глобальные переменные не инициализируются.

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

TMy > public
FName:string;
FAge:integer;
procedure DoSomething(K:integer);
function AddOne(N:integer):integer;
end;

В данном примере к методам TObject добавлены два новых метода – DoSomething и AddOne. Синтаксис Object Pascal разрешает объявлять новые методы только после объявления переменных – приведенный ниже пример вызовет ошибку компиляции:

TMy > public
FName:string;
procedure DoSomething(K:integer);
FAge:integer;
function AddOne(N:integer):integer;
end;

После объявления какого-либо метода в классе необходимо в секции implementation данного модуля описать его реализацию. Перед заголовком метода следует поместить указание на класс, к которому он относится. Это необходимо делать, поскольку различные классы могут иметь методы с одинаковыми названиями:


interface
type
TMy > procedure DoSomething(N:integer);
end;
TSecond > procedure DoSomething(N:integer);
end;


implementation
procedure TMyClass.DoSomething(N:integer);
begin

end;

procedure TSecondClass.DoSomething(N:integer);
begin

end;

Методы, объявленные в классе

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

TMy > class procedure DoSomething(K:integer);

function AddOne(N:integer):integer; absent – means method>
end;

implementation

class procedure TMyClass.DoSomething(K:integer);
begin

end;

function TMyClass.AddOne(N:integer):integer;
begin

end;

Обратите внимание, что и в секции реализации используется служебное слово class.

Классовые методы можно вызывать без создания экземпляра класса. Разновидность классовых методов – конструкторы – используются для создания рабочих копий (экземпляров) класса. О создании экземпляров класса будет сказано ниже, пока же достаточно знать, что может быть несколько (в том числе и ни одной) копий экземпляров класса. Классовый метод полностью характеризуется одним адресом (указателем) в памяти, в этом его отличие от других методов класса. Процессор просто передает управление по этому адресу при вызове соответствующего метода. В классовых методах (но не в конструкторах!) нельзя использовать классовые переменные: они становятся доступными только после создания экземпляра класса, а классовый метод можно вызывать, не создавая экземпляр.

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

Для работы с методами имеется структура TMethod, определенная в модуле SysUtils:

type
TMethod = record
Code, Data: Pointer;
end;

Эта запись позволяет «разобрать» и вновь «собрать» метод класса на две переменные типа Pointer, что бывает полезным для передачи ссылки на метод во внутренний (in-process) сервер автоматизации.

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

TMy > function AddOne(N:integer):integer; – means
static method>
procedure Rotate(Angle:single); virtual;
procedure Move(Distance:single); dynamic; method>
end;

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

В классическом объектно-ориентированном программировании требуется наличие виртуальных методов. Динамические методы поддерживают не все объектно-ориентированные языки, но их использование является достаточно эффективным. Для понимания различий между ними рассмотрим создание главной формы какого-либо приложения. При этом разберемся, как в памяти компьютера распределяются три метода формы: DoEnter (динамический метод), CreateWnd (виртуальный) и DoKeyDown (статический). Каждый из этих методов определен на уровне TWinControl.

Иерархия классов, которая ведет от TWinControl к TForm1, следующая:

TWinControl -> TScrollingWinControl -> TCustomForm -> TForm ->
TForm1

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

При динамическом методе DoEnter в памяти компьютера создается единственная его копия, а в таблице динамических методов TWinControl указывается его адрес. В таблицах динамических методов классов TScrollingWinControl…TForm1 в качестве адреса этого метода указывается nil. При вызове этого метода из экземпляра класса TForm1 первоначально происходит поиск этого метода в таблице динамических методов TForm1. Естественно, метод найден не будет, и поиск продолжится уже в таблице динамических методов класса TForm. Так будет продолжаться до тех пор, пока не начнется поиск в таблице динамических методов TWinControl, где будет найден его адрес, по которому будет передано управление процессом. Как и статические, динамические методы требуют мало ресурсов: один экземпляр динамического метода обслуживает как экземпляры класса, где он определен, так и экземпляры всех его потомков. Но вызов метода происходит достаточно долго, поскольку для этого приходится просматривать несколько таблиц. Вызов может замедляться еще и при использовании директивы компилятора <$R+>(проверка диапазона допустимых значений).

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

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

Переписанные методы. Полиморфизм. Абстрактные методы

Методы классов могут вызываться как непосредственно программистом, так и автоматически в ответ на какие-либо события. Предположим, перед вызовом метода необходимо выполнить какое-либо действие. Если метод вызывается в явном виде из кода программы, то программист может перед вызовом метода дописать необходимый код. Однако достаточно часто бывает необходимо выполнить действия перед автоматическим вызовом классом какого-либо метода в ответ на какое-либо событие (или после вызова метода). Например, класс TEdit (однострочный редактор текста) позволяет редактировать текст. При этом, например, программист хочет, чтобы пользователь не мог покинуть данный элемент управления, пока в нем не будет введено корректное значение какого-либо текста (например, целое число). Но покинуть данный элемент управления и перейти к другому пользователь может несколькими способами – щелкнуть мышкой на другом элементе, нажать акселератор или клавишу Tab. Если бы программист никак не мог изменить имеющиеся методы, он вынужден был бы перехватывать события о нажатой клавише мыши на всех имеющихся на форме элементах управления, проверять содержимое целевого элемента и при необходимости возвращать ему фокус ввода. Такие же проверки необходимо было бы сделать для обработчика событий, связанных с нажатием на клавиши…

К счастью, в классах виртуальные и динамические (но не статические!) методы можно подменить на другие, созданные программистом. При этом, если данный метод вызывается автоматически, будет выполняться уже новый метод, написанный программистом. Такая подмена осуществляется при использовании служебного слова override. В данном случае (проверка содержимого редактора перед выходом из него) решение будет заключаться в следующем. Любой объект класса TWinControl (и TEdit) вызывает метод DoExit перед тем, как он теряет фокус ввода. Этот метод является динамическим, и его можно переписать:

TMyEdit=class(TEdit)
protected
procedure DoExit; override;
end;

implementation

procedure TMyEdit.DoExit;
var
N,I:integer;
begin
inherited DoExit;
Val(Text,N,I);
if I<>0 then begin
MessageDlg(‘Illegal value’,mtError,[mbOK],0);
SetFocus;
end;
end;

Теперь, создавая копию данного класса в обработчике события OnCreate главной формы

procedure TForm1.FormCreate(Sender: TObject);
begin
with TMyEdit.Create(Self) do begin
Parent:=Self;
Left:=200;
Top:=100;
end;
end;

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

Таким образом, переписывание виртуального или динамического метода осуществляется следующим образом. В классе-потомке определяется метод с тем же самым названием и с тем же самым списком параметров, который был ранее объявлен в каком-либо из классов-предков данного класса. При этом не имеет значения, как называются формальные параметры переписываемого метода, значение имеет их порядок следования, модификаторы (var, const) и их тип. Например, на уровне TWinControl определен виртуальный метод: procedure AlignControls(AControl: TControl; var Rect: TRect); При его переписывании в каком-либо классе-потомке данный метод можно определить следующим образом:

procedure AlignControls(AC: TControl; var R: TRect); override;

Однако компилятор Delphi не пропустит перечисленные ниже определения:

procedure AControls(AControl: TControl; var Rect: TRect); override; is not consistent>
procedure AlignControls(AControl: TControl; Rect: TRect); override; missed>
procedure AlignControls(var Rect: TRect); override;

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

Итак, рассмотрим еще раз, что происходит в классе TEdit и его потомке TMyEdit. Класс TEdit устроен таким образом, что перед тем, как он теряет фокус ввода, приложение обращается к таблице динамических методов TEdit и извлекает оттуда адрес 27-го метода (метод DoExit среди динамических определен 27-м по счету). После этого управление процессом передается по найденному адресу. Класс-потомок TMyEdit имеет собственные таблицы виртуальных и динамических методов. Таблица динамических методов отличается тем, что в нем 27-й адрес уже указывает на реализованный нами метод DoExit. Соответственно приложением извлекается уже новый адрес, и управление процессом передается вновь реализованному методу. При его старте происходит обращение к таблице динамических методов класса TEdit и вызывается 27-й метод – это делает строка inherited DoExit. Затем проверяется содержимое свойства Text в экземпляре класса TMyEdit, и если это не целое число, то об этом сообщается пользователю и фокус ввода вновь переносится на редактор текста. Схематически это можно изобразить на рисунке следующим образом.

При забывании директивы override появляется новый, статический метод с тем же самым названием и списком параметров, что и ранее определенный виртуальный (или динамический) метод. При этом новый метод делает «невидимым» старый, то есть при явном вызове метода из кода приложения будет вызываться новый, определенный программистом метод. Если метод вызывается классом в ответ на какое-либо событие, по-прежнему будет вызываться старый метод (адрес в таблицах не изменится). Такое поведение объекта часто приводит к печальным последствиям, которые иногда очень трудно обнаруживаются.

С переписываемыми методами тесно связано и другое понятие для классов – полиморфизм, суть которого заключается в том, что родительский класс имеет какой-либо виртуальный или динамический метод, а в различных его потомках этот метод переписывается по-разному. После этого выполняя формально одни и те же методы, можно добиться принципиально разного результата . Хороший пример полиморфных классов – класс TStream (базовый) и три его потомка – TFileStream, TMemoryStream и TResourceStream. Эти классы используются для хранения и передачи данных в двоичном формате. В базовом классе TStream определено несколько абстрактных методов, например:

Илон Маск рекомендует:  Выделение всего текста в текстовом поле

function Write(const Buffer; Count: Longint): Longint; virtual;

а в классах-потомках этот метод переписан таким образом, что записывает данные из переменной Buffer в файл (TFileStream), или в ОЗУ (TMemoryStream), или в ресурсы (TResourceStream). Программист при реализации приложения может определить метод для сохранения своих данных в двоичном формате:

procedure SaveToStream(Stream:TStream);
begin
Stream.Write(FYear,sizeof(FYear));
Stream.Write(FMonth,sizeof(FMonth));
<. A very long code to save all data as binary may
be inserted here …>
end;

Далее, вызывая этот метод и используя разные классы-потомки, можно сохранить данные либо в памяти компьютера, либо в файле:

procedure TForm1.Button1Click(Sender: TObject);
var
MStream:TMemoryStream;
FStream:TFileStream;
begin
MStream:=TMemoryStream.Create;
SaveToStream(MStream);
DoSomething(MStream);
MStream.Free;
FStream:=TFileStream.Create(‘C:\Test.dat’,fmCreate);
SaveToStream(FStream);
FStream.Free;
end;

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

TMy > protected
procedure DisplayGraphic(Canvas:TCanvas); virtual;
abstract;
end;

При объявлении метода абстрактным реализация этих методов не требуется (более того – не допускается компилятором). В секции implementation для метода DisplayGraphic абсолютно ничего писать не надо. Для абстрактного метода в виртуальной (или динамической) таблице методов резервируется запись, куда помещается адрес nil. В классах-потомках на место этого адреса подставляются реальные адреса.

При попытке вызвать абстрактный метод возникает исключение – метод-то отсутствует! В частности, для вышеприведенного примера метод Write класса TStream является абстрактным и при попытке вызвать метод SaveToStream

procedure TForm1.Button1Click(Sender: TObject);
var
Stream:TStream;
begin
Stream:=TStream.Create;
SaveToStream(Stream);
Stream.Free;
end;

Классы, содержащие абстрактные методы, называют абстрактными; они являются базовыми для создания классов-потомков. В любом случае не следует создавать экземпляры абстрактных классов в приложении! Компилятор Delphi тем не менее позволяет осуществлять вызов конструкторов абстрактных классов с соответствующим предупреждением.

Так же как и для обычных методов, в методах класса допустима директива overload – перегружаемый метод:

TOver1 > public
procedure DoSmth(N:integer); overload;
class procedure DoSecond(N:integer); overload; dynamic;
end;

Перегружаемые методы могут быть как классовыми, так и обычными. Они способны быть статическими, динамическими и виртуальными. Следующие перегружаемые методы могут быть объявлены как в исходном классе, так и в классах-потомках:

TOver2 > public
procedure DoSmth(S:string); overload;
class procedure DoSecond(S:string); reintroduce; overload; dynamic;
end;

Теперь можно вызывать методы DoSmth и DoSecond из экземпляра TOver2Class с целочисленным и строковым параметром:

procedure TForm1.Button3Click(Sender: TObject);
var
CO:TOver2Class;
begin
CO:=TOver2Class.Create;
CO.DoSmth(1);
CO.DoSmth(‘Test’);
CO.Free;
end;

В файлах помощи Delphi рекомендуют использовать директиву reintroduce, когда вводится перегружаемый виртуальный метод, но и без этой директивы приложение прекрасно работает. Директива reintroduce просто подавляет сообщение компилятора (warning) о возникновении скрытого метода.

Создание экземпляров классов. Конструкторы и деструкторы

Переменная типа класса объявляется в приложении так же, как обычная переменная:

При таком объявлении резервируется 4 байта памяти для любого класса. Очевидно, что этого явно недостаточно для хранения всех переменных в классе. Размер этой переменной говорит о том, что в ней хранится указатель. Экземпляр (или рабочая копия) класса создается посредством вызова его конструктора:

При вызове конструктора класса резервируется память, необходимая для хранения данных в объекте (переменных). Обращаться к переменным и методу класса можно только после вызова конструктора, иначе попытка их чтения/записи приводит к исключительной ситуации. Обращение к переменным и методам экземпляра класса осуществляется с указанием на экземпляр:

procedure TForm1.Button1Click(Sender: TObject);
var
SL1,SL2:TStringList;
begin
SL1:=TStringList.Create;
SL2:=TStringList.Create;
SL1.Add(‘String added to SL1 object’);
SL2.Add(‘String added to SL2 object’);
….
end;

Довольно распространенной ошибкой (которая, правда, тут же обнаруживается) является вызов конструктора с попыткой обратиться к несуществующему экземпляру класса:


procedure TForm1.Button1Click(Sender: TObject);
var
SL:TStringList;
begin
SL:=nil;
try
SL.Create; typed SL:=TStringList.Create;>

finally
if Assigned(SL) then SL.Free;
end;
end;

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

Любой класс может иметь несколько конструкторов. Примером может служить класс Exception, имеющий восемь конструкторов. По соглашению имя конструктора содержит слово Create (CreateFmt, CreateFromFile…). Конструкторы могут быть как статическими, так и виртуальными или динамическими. Последние могут быть переписаны – в классах-потомках при необходимости определяется новый конструктор со служебным словом override. Переписывать конструкторы необходимо только для компонентов Delphi и для форм – во всех остальных классах их можно просто добавлять к существующим методам. Необходимость переписывания конструкторов компонентов и форм обусловлена тем, что их вызывает среда разработки. Забытая директива override в компоненте приводит к тому, что при создании формы не выполняется новый конструктор. В большинстве других классов (не потомков TComponent) конструктор вызывается в явном виде из приложения, и поэтому будет вызываться последний написанный конструктор.

Конструктор необходимо переписывать (или создавать новый), когда необходимо изначально (при создании экземпляра класса) изменить значения переменных, или запомнить в переменных параметры, передаваемые в конструкторе, или создать экземпляры классов, объявленных внутри другого класса:

TMyBox=class(TListBox)
private
FData:TList;
FNAccel:integer;
public
constructor Create(AOwner:TComponent); override;
end;
implementation
constructor TMyBox.Create;
begin
inherited Create(AOwner);
FData:=TList.Create;
FNAccel:=5;
Items.Add(‘1’);
end;

Следует обратить внимание на то, что в конструкторе первым вызывается inherited-метод – конструктор класса-предка и только потом пишется код для инициализации переменных. Это обязательное условие в объектно-ориентированном программировании, которое может нарушаться только в отдельных случаях (примером такого случая является класс TThread). При таком способе записи каждый конструктор предка будет вызывать конструктор своего предка – и так до уровня конструктора класса TObject, который фактически будет первым оператором при вызове конструктора любого класса. Далее происходит выполнение кода в конструкторе класса-потомка и т.д. Для класса TMyBox при обращении к конструктору сначала происходит резервирование памяти для хранения переменных, определенных в данном классе и его предках. Затем вызывается конструктор TObject. Далее происходит обращение к конструктору TComponent, который устанавливает связь экземпляра TMyBox с его владельцем, передаваемым в параметре AOwner. Выполняется код конструктора TCustomListBox, который создает экземпляр класса TStrings и инициализирует ряд переменных. И наконец выполняются операторы, определенные в конструкторе TMyBox. Если оператор inherited поставить последним в конструкторе TMyBox, произойдет исключение при выполнении оператора Items.Add(‘1’) – объект для хранения строк создается в конструкторе класса TCustomListBox, который еще не был вызван.

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

TMyBox=class(TListBox)
private
FData:TList;
FNAccel:integer;
public
constructor Create(AOwner:TComponent); override;
destructor Destroy; override;
end;
implementation
destructor TMyBox.Destroy;
begin
FData.Free;
inherited Destroy;
end;

Так будет выглядеть новый деструктор, который мы обязаны объявить в примере, приведенном выше. При вызове деструктора следует ссылаться на конкретный экземпляр класса, который необходимо разрушить:

Для этого примера должен быть создан новый деструктор, так как внутри экземпляра класса TMyBox создается экземпляр класса TList. Соответственно разрушаться они должны совместно.

При переписывании деструктора прежде всего разрушаются экземпляры классов, созданных внутри данного класса, и только после этого вызывается деструктор класса-предка inherited Destroy (отметим, что в конструкторе используется обратный порядок). При таком способе вызова в последнюю очередь будет вызван метод Destroy класса TObject, который вернет системе память, зарезервированную для хранения переменных класса. В примере с классом TMyBox первоначально будет разрушен экземпляр класса TList, ссылка на который содержится в переменной FData. После этого будет вызван деструктор класса TlistBox, в котором разрушается экземпляр класса TStrings. И наконец, будет вызван деструктор класса TObject, где будет освобождена память, зарезервированная для классовых переменных TMyBox.

Вместо прямого вызова деструктора рекомендуется вызывать метод Free, позволяющий проверить, была ли выделена память для разрушаемого экземпляра класса, и если да, то вызывать его деструктор. Использование этого метода важно еще и потому, что деструктор должен быть описан таким образом, чтобы он мог корректно разрушить частично созданный экземпляр класса. Частично созданный экземпляр класса получается в том случае, если в его конструкторе произошло исключение. При этом немедленно вызывается деструктор данного класса, и после его отработки nil-указатель возвращается на создаваемый экземпляр класса. Если, например, в конструкторе резервировалась память под какую-либо переменную (FPBuf):

constructor TMyBox.Create(AOwner:TComponent);
begin
inherited Create(AOwner);
FData:=TList.Create;
GetMem(FPBuf,65500);
end;
destructor TMyBox.Destroy;
begin
FData.Free;
FreeMem(FPBuf);
inherited Destroy;
end;

то исключение может произойти в конструкторе в момент вызова inherited Create или в момент вызова TList.Create — из-за нехватки системных ресурсов. Сразу же будет вызван деструктор, и в момент выполнения оператора FreeMem произойдет генерация еще одного исключения. При этом метод inherited Destroy не будет вызван, а частично созданный экземпляр TMyBox не будет разрушен. Корректная реализация деструктора выглядит так:

if FPBuf<>nil then FreeMem(FPBuf);

При этом в обязательном порядке необходимо проверить, была ли выделена освобождаемая память ранее. Такие проверки необходимо делать со всеми ресурсами, подлежащими освобождению в деструкторе. В противном случае освобождать ресурс лучше в защищенном блоке try…except…end без вызова метода raise в секции except…end. Распространение исключения из деструктора недопустимо (пользователя не должно волновать, что программист не смог корректно высвободить ресурсы!).

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

TTest=class(TObject)
private
FData:TList;
public
constructor Create(AData:TList);
end;
implementation
constructor TTest.Create(AData:TList);
begin
inherited Create;
FData:=AData;
end;

Если сам объект AData будет разрушен в той процедуре, где он создан, то переписывать деструктор класса TTest для разрушения объекта FData не требуется. Повторный вызов деструктора приводит к исключению. При этом применение метода Free не спасает, он лишь проверяет, что ссылка на экземпляр класса не указывает на nil.

В отличие от конструктора, для которого может быть определено несколько методов, деструктор бывает только один. Невозможно представить себе ситуацию, когда в классе может понадобиться дополнительный деструктор. Тем не менее компилятор Delphi позволяет это сделать – а зря… Классы с двумя деструкторами – довольно частое явление на распространяемых компонентах для Delphi третьих фирм. Причиной тому программист, забывший директиву override. Это часто приводит к тому, что ресурсы, освобождением которых занимается деструктор, не освобождаются. Во-первых, метод Free обращается к первому виртуальному методу класса – Destroy. При этом будет честно вызван деструктор класса-предка, но ресурсы, освобождение которых программист старательно описывал в деструкторе с забытой директивой override, освобождены не будут. Во-вторых, при разрушении формы содержащиеся на ней компоненты также разрушаются через вызов первого метода в виртуальной таблице, что ведет к аналогичному результату.

В заключение следует рассмотреть на первый взгляд странный вопрос: а всегда ли следует вызывать деструктор (непосредственно или через метод Free) из кода приложения? Правомерность постановки такого вопроса обусловлена тем, что программист нигде не пишет кодов вызова деструкторов компонентов, помещенных на форму на этапе разработки. Ответ заключается в структуре и реализации деструктора класса TComponent. Любой компонент в конструкторе запоминает ссылку на своего хозяина (AOwner) и заносит себя в список компонентов, которыми владеет хозяин. При вызове деструктора компонента он в первую очередь вызывает деструкторы своих «вассалов», и только после этого вызывается собственный деструктор. Таким образом, нет необходимости вызывать деструктор класса TComponent или его потомка – он будет автоматически разрушен при вызове деструктора его хозяина:

TMyBox=class(TListBox)
private
FData:TComponent;
public
constructor Create(AOwner:TComponent);
override;
end;
constructor TMyBox.Create(AOwner:TComponent);
begin
inherited Create(AOwner);
FData:=TComponent.Create(Self);
end;

В данном случае деструктор для разрушения объекта FData не нуждается в переписывании, поскольку он будет разрушен автоматически при разрушении объекта TMyBox. Деструктор для TComponent (или его потомка) следует вызывать только в случае, если его владелец – nil.

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

В Dephi5 появились два новых виртуальных метода – AfterConstruction и BeforeDestruction, — которые вызываются сразу же после конструктора или перед вызовом деструктора соответственно. Можно поспорить насчет необходимости введения метода BeforeDestruction: любой класс имеет виртуальный деструктор, который можно переписать. Появление метода AfterConstruction следует приветствовать, поскольку виртуальный конструктор появляется только на уровне TComponent в иерархии классов VCL. Появление виртуального конструктора существенно облегчило написание приложений для распределенных вычислений. Например, TComObject – базовый класс для реализации интерфейсов в COM-серверах — является потомком TObject и не содержит виртуального конструктора. Экземпляры этого класса создаются в ответ на запрос клиентов, а не командами из кода приложений, что затрудняет выполнение инициализации переменных при создании экземпляра класса. Введение виртуального метода AfterConstruction сделало инициализацию данных в этих классах рутинной процедурой.

Дополнительную информацию Вы можете получить в компании Interface Ltd.

«Abstract Error» как исправить?

TStrings — это абстрактный класс. Поэтому нельзя создать экземпляр этого класса. Абстрактный класс — это класс, в котором есть абстрактные методы. Абстрактные методы — это методы, помеченные словом Abstract. Эти методы не имеют реализации в этом классе. В потомках абстрактного класса абстрактные методы могут быть реализованы. Экземпляры таких потомков уже можно создавать. Например, TStrings — это абстрактный класс. Класс TStringList наследуется от класса TStrings. И в TStringList все абстрактные методы, унаследованные от TStrings — реализованы. Поэтому можно создавать экземпляры класса TStringList. При этом можно действовать так. Можно определить переменную типа TStrings. И присвоить ей ссылку на экземпляр TStringList или другого не абстрактного класса-потомка от TStrings:

Реализация избежать абстрактного класса Delphi

В этом вопросе вы видите , что можно создать абстрактный класс , добавив к abstract keywrod. Я перевожу проект в Delphi , но я вижу , что она позволяет создать абстрактный класс. Это код:

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

Когда я пытаюсь вызвать его методу, которые являются виртуальными и абстрактным я получаю ошибку, и это нормально, но я могу предотвратить создание класса? Или Delphi позволяет это по умолчанию? Спасибо за помощь

class abstract Это пережиток Delphi for .Net дней.
По неизвестным причинам нет (текущая) реализация за этим ключевым словом.

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

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

Обратите внимание , что вы должны не объявить конструктор override . Вместо того, чтобы объявить его virtual reintroduce (или просто , reintroduce если вы не хотите , чтобы виртуальные конструкторы).

Блог GunSmoker-а (переводы)

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

среда, 13 июля 2011 г.

Виртуальные методы и inherited

Это перевод Virtual methods and inherited. Автор: Hallvard Vassbotn.

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

Виртуальный метод объявляется в своём базовом классе с использованием директивы virtual : Базовый класс может иметь реализацию по умолчанию для виртуального метода, а может и не иметь. Если реализации по умолчанию нет, то вы помечаете метод как абстрактный (директива abstract ), вынуждая наследников класса создавать свою реализацию метода в обязательном порядке. Всё это — базовые вещи, которые знают все Delphi программисты. В зависимости от реализации (и документации!) базового класса, класс-наследник может решить вызывать унаследованный метод в самом начале, перед выполнением своих действий, либо в середине (довольно редко), либо после своих действий, в конце, либо же не вызывать вовсе. Существует два способа вызова унаследованного варианта метода, с тонкими отличиями: Этот код безусловно вызовет унаследованный метод Draw базового класса. Если метод в базовом классе — абстрактный, то этот вызов завершиться неудачей, возбуждая исключение EAbstractError во время выполнения (или Run-Time ошибку 210, если вы не используете исключения).

Альтернативный синтаксис вызова — просто написать inherited; , например: Этот код будет работать идентично предыдущему для случаев, когда базовый класс содержит не абстрактный метод. Этот код также автоматически создаётся средством автодополнения кода, когда вы реализуете замещение метода (override). Кроме того, он же используется IDE, при вставке обработчиков событий форм с визуальным наследованием.

Если же метод базового класса является абстрактным, либо же базовый класс вообще не содержит метода (для не виртуальных методов), то вызов inherited становится noop (No-Operation — пустым оператором). Компилятор не генерирует для него кода (и поэтому вы не можете поставить на него точку останова). Этот механизм является частью отличной версионной устойчивости языка Delphi.

Один подводный камень с синтаксисом inherited; — он не поддерживается для функций. Для функций вам нужно использовать явный синтаксис с указанием имени метода и его аргументов. К примеру: Это может выглядеть как чрезмерное ограничение дизайна языка Delphi, но я думаю, что это не случайно. Смысл этого, вероятно, в том, что если TMyClass.MethodC является абстрактным (или будет сделан абстрактным в будущем), то присваивание результата вызова в Result в классе-потомке будет удалено, что приведёт к неожиданному неопределенному значению. Это, конечно же, приведёт к скрытым багам в коде.

Однако, я думаю, что здесь есть небольшой пробел в синтаксисе унаследованного вызова. Во многих отношениях процедура, которая принимает out параметры (а в некоторых случаях и var параметры), ведёт себя как функция, возвращающая результат. Так, на мой взгляд, синтаксис inherited; должен быть запрещён при вызове методов с out (и, возможно, var ) параметрами. Сейчас это не так. Этот код означает, что если метод родительского класса является абстрактным (или просто отсутствует в случае не виртуального метода), то значение выходного параметра будет неопределённым. На мой взгляд, компилятор должен запрещать такие вызовы inherited; , требуя явного синтаксиса inherited MethodB(A); Но кота уже выпустили из мешка и уже слишком поздно что-то менять — блокировка подобных вызовов с ошибкой компиляции определённо поломает кучу кода, так что, вероятно, это тянет максимум на предупреждение (warning).

Блог GunSmoker-а

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

22 декабря 2008 г.

Новое ключевое слово static в Delphi

Недавно я переводил пост Почему методы класса должны быть помечены словом «static», чтобы их можно было использовать в качестве функции обратного вызова? Реймонда Чена. Там я оставил весь код «как есть» — на C++. Здесь я рассмотрю этот вопрос с точки зрения Delphi.

Как известно, в языке есть такие сущности как процедуры/функции и методы. Причём начинающие программисты часто путают эти два понятия.

Функция (в дальнейшем здесь будет также подразумеваться и процедура) — это код. Процедурная переменная — это указатель на код. Например: Метод — это тоже код, но код, связанный с классом. Указатель на метод — это ссылка на код + ссылка на конкретный объект. Например: Когда путают одно с другим компилятор чаще всего показывает такое сообщение: «Incompatible types: regular procedure and method pointer». Чаще всего или забывают писать «of object» в объявлении своих процедурных типов или пытаются передать в функцию (чаще всего как callback — т.е. функцию обратного вызова) метод класса вместо обычной функции (а самым упорным это иногда удаётся).

Что делает эти две сущности такими принципиально несовместимыми? Функция — это просто код. Она не имеет связи с данными, отличными от тех, что передаются в её параметры. Методы класса помимо работы с параметрами (как и обычная функция) ещё могут оперировать с данными объекта (вот оно: «код» vs «код + данные»), например: С функциями такое невозможно — обратите внимание, как вы манипулируете с P3 (он же: Self.P3) в методе. Собственно сам объект (это встроенная переменная Self) неявно передаётся в метод первым параметром. Поэтому, если метод объявлен как function(const P1, P2: Integer): Integer of object — с двумя параметрами, то, на самом деле, он трактуется как функция с тремя параметрами: function(Self: TSomeObj; const P1, P2: Integer): Integer . Именно это различие (на бинарном уровне) делает несовместимыми обычные функции и методы.

Соответственно, указатель на обычную функцию — это просто указатель (pointer), только что типизированный (это я про TDoSomethingFunc) — т.е. 4 байта. А вот указатель на метод — это уже запись или, если будет угодно, два указателя — один на код, второй — на данные, т.е. всего 8 байт.

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

Ещё в Delphi есть классовые методы. Это такие методы, которые можно вызывать не имея на руках объект. В этом случае вместо объекта в неявный параметр Self передаётся информация о классе. Т.е. в классовых методах вы не можете использовать информацию о конкретном объекте (например, читать/писать его поля), но можете использовать информацию о классе — например, вызывать конструктор класса. Также методы класса могут быть виртуальными. Заметим, что сигнатура функции, реализующей метод, всё ещё совпадает с сигнатурой обычного метода: неявный параметр (данные класса вместо Self) + все явные параметры метода.

Например: Теперь ещё один шажок и мы переходим к тому, о чём говорил Реймонд Чен. Классовый метод можно объявить статическим (только в новых версиях Delphi). В этом случае у него не будет неявного параметра. Разумеется, при этом он не может использовать информацию экземпляра и класса. Зато он и не отличается от обычной функции.

Рассматривая пример с потоком, вот что мы могли бы написать в старых Delphi без поддержки статических классовых методов: Теперь, с введением нового ключевого слова static, появилась возможность писать так: При этом Реймонд говорит о том, что если у Execute сделать модель вызова stdcall, то бинарные сигнатуры параметра CreateThread, методов ThreadProc и Execute совпадут — поэтому, мол, умный компилятор уменьшит код ThreadProc до простого jmp. Увы, но компилятор Delphi не настолько умён — в этом случае он генерирует полный вызов вместе с передачей параметра.

Потоки TStream, TFileStream, TMemoryStream (стр. 1 из 2)

Южно-Сахалинский институт экономики, права и информатики

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

по дисциплине: Языки программирования и методы трансляции

натему: Потоки: TStream, TFileStream, TMemoryStream

Задачей курсовой работы является реализация программы для работы с файловой структурой операционной системы Windows. Для реализации программы требуется использовать интегрированную среду разработки Delphi7.

Цель работы – изучить принципы программирования файловой структуры в операционной системе Windows и создать программу для работы с потоками TStream, TFileStream и TMemoryStream. В проекте предполагается реализовать основные операции над потоками, такие как чтение, запись, удаление и редактирование.

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

1. позволяет создавать потоки TFileStream, TMemoryStream,

2. сохранение данных в файлы,

3. открытие и редактирование файлов при помощи потоков,

4. использовать данные из файлов в программе.

Для разработки приложения используются следующие программные инструменты:

1. среда разработки Delphi 7

В основе иерархии классов потоков лежит класс Tstream. Он обеспечивает выполнение основных операций потока безотносительно к реальному носителю информации. Основными из них являются чтение и запись данных. Класс Tstream порожден непосредственно от класса TObject. Потоки также играют важную роль в чтении/записи компонентов из файлов ресурсов (DFM). Большая группа методов обеспечивает взаимодействие компонента и потока, чтение свойств компонента из ресурса и запись значений свойств в ресурс.

Илон Маск рекомендует:  Что такое код xmlrpc_encode_request

Таблица 1 — Свойства и методы класса Tstream

Объявление Описание
property Position: Longint; Определяет текущую позицию в потоке
property Size: Longint; Определяет размер потока в байтах
function CopyFrom( Source: TStream; Count: Longint) : Longint; Копирует из потока SourceCount байты, начиная с текущей позиции. Возвращает число скопированных байтов
function Read(var Buffer; Count: Longint) : Longint; virtual; abstract; Абстрактный класс, перекрываемый в наследниках. Считывает из потока Count байты в буфер Buffer. Возвращает число скопированных байтов
procedure Read3uffer (var Buffer; Count: Longint) ; Считывает из потока Count байты в буфер Buffer. Возвращает число скопированных байтов
function Seek (Off set: Longint; Origin: Word): Longint; virtual; abstract; Абстрактный класс, перекрываемый в наследниках. Смещает текущую позицию в реальном носителе данных на Offset байтов в зависимости от условия Origin
function Write (const Buffer; Count: Longint): Longint; virtual; abstract; Абстрактный класс, перекрываемый в наследниках. Записывает в поток Count байты из буфера Buffer. Возвращает число скопированных байтов
procedure WriteBuffer (const Buffer; Count: Longint); Записывает в поток Count байты из буфера Buffer. Возвращает число скопированных байтов
function ReadComponent (Instance: TComponent): TComponent; Передает данные из потока в компонент instance, заполняя его свойства значениями
function ReadComponentRes (Instance: TComponent) : TComponent; Считывает заголовок ресурса компонента Instance и значения его свойств из потока.
procedure ReadResHeader; Считывает заголовок ресурса компонента из потока
procedure WriteComponent (Instance: TComponent) ; Передает в поток значения свойств компонента Instance
procedure WriteComponentRes (const ResName: string; Instance: TComponent) ; Записывает в поток заголовок ресурса компонента Instance и значения его свойств

Итак, в основе операций считывания и записи данных в потоке лежат методы Read и Write. Именно они вызываются для реального выполнения операции внутри методов ReadBuffer и WriteBuffer, ReadComponent и WriteComponent. Так как класс TStream является абстрактным, то методы Read и write также являются абстрактными. В классах-наследниках они перекрываются, обеспечивая работу с конкретным физическим носителем данных.

Листинг 1 — создание, чтение и запись потока

Stream: TStream; //Объявлениепотока

Stream := TMemoryStream.Create (. ); //Созданиепотока

Stream.Read(. ); //Чтение данных из потока

Stream.Write(. ); //Запись данных в поток

Stream.Free; //Очистить поток

Группа методов обеспечивает чтение и запись из потока ресурса компонента. Они используются при создании компонента на основе данных о нем, сохраненных в формате файлов ресурсов. Для чтения ресурса используется метод ReadComponentRes, в котором последовательно вызываются: метод ReadResHeader — для считывания заголовка ресурса компонента из потока; метод ReadComponent — для считывания значений свойств компонента. Для записи ресурса в поток применяется метод writeComponentRes.

2. Поток T FileStream

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

Полное имя файла, который надо открыть, задается в параметре FileName. Этотпараметр — простаястрока:

constructor Create(const FileName: string; Mode: Word);

Параметр Mode определяет режим работы с файлом. Он составляется из флагов режима открытия: fmCreate — файл создается; fmOpenRead — файл открывается для чтения; fmopenwrite — файл открывается для записи; fmOpenReadWrite — файл открывается для чтения и записи.

И флагов режима совместного использования:

fmShareExciusive — файл недоступен для открытия другими приложениями;

fmShareDenyWrite — другие приложения могут читать данные из файла;

fmShareDenyRead — другие приложения могут писать данные в файл;

fmShareDenyNone — другие приложения могут производить с файлом любые операции. Создание потока аналогично примеру, рассмотренному в листинге 1.

Подробнее познакомимся с методами чтения, записи и внутренней структурой файла. Начнем со структуры. Когда вы открыли файл, позиция курсора устанавливается в самое начало и любая попытка чтения или записи будет происходить в эту позицию курсора. Если вам надо прочитать или записать в любую другую позицию, то надо передвинуть курсор. Для этого используется Метод Seek. «Точка отсчета» позиции зависит от значения параметра

Origin: soFromBeginning — смещение должно быть положительным и отсчитывается от начата потока;

soFromCurrent — смещение относительно текущей позиции в потоке;

soFromEnd — смещение должно быть отрицательным и отсчитывается от конца потока.

Не забывайте, что один байт — это один символ. Единственное исключение — файлы в формате Unicode. В них один символ занимает 2 байта.

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

Итак, если вам надо передвинуться на 10 символов от начала файла, можете написать следующий код:

Метод seek всегда возвращает смещение курсора от начала файла. Этим можно воспользоваться, чтобы узнать, где мы сейчас находимся, а можно и узнать общий размер файла. Если переместиться в конец файла, то функция вернет количество байт от начала до конца, т. е. полный размер файла.

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

Размер файла := Stream.Seek(0, soFromEnd);

Для чтения из файла нужно использовать метод Read. У этого метода два параметра:

1. Переменная, в которую будет записан результат чтения;

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

В листинге 2 рассмотрим пример чтения из файла с 20-й позиции

Листинг 2 – Чтения из файла, начиная с 20-й позиции.

Var Stream: TFileStream; //Переменная типа объект TFileStream. buf: array[0..10] of char; // Буфер для хранения прочитанных данных

begin // Далее открываем файл “Sample.wrk”. Stream:= TFileStream.Create(‘c:\Sample.wrk, fmOpenReadWrite); Stream.Seek(20, soFromBeginning); // Перемещениена20 символоввперед. Stream.Read(buf, 5); // Чтение 5 символов из установленной позиции. Stream.Free; // Очистка потока.


Метод Read возвращает количество реально прочитанных байт (символов).

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

1. При чтении был достигнут конец файла и дальнейшее чтение стало невозможным;

2. Ошибка на диске или любая другая проблема.

Для чтения применяется метод write. У него два параметра :

1. Переменная, содержимое которой нужно записать;

2. Число байт для записи.

Пользоваться этим методом можно точно так же как и методом для чтения.

3. Поток T MemoryStream

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

Для создания потока TMemoryStream используется конструктор Create без параметров:

Для чтения информации из файла с одновременным занесением ее в поток используется метод LoadFromFile:

Procedure LoadFromFile(const FileName: string);

файл поток класс метод

И аналогичный метод для записи содержимого потока в файл:

Procedure SaveToFile(const FileName: string);

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

Procedure LoadFromStream(Stream: TStream);

Procedure SaveToStream(Stream: TStream);

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

операция очистки содержимого с помощью метода Clear:

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

Правильная обработка освобождения ресурсов через try…finally в Delphi

Есть много разных вариантов как можно использовать конструкцию try. finally для освобождения ресурсов. Многие из них работают неверно в особых ситуациях. Рассмотрим несколько вариантов подробнее.

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

Прежде всего, установим ReportMemoryLeaksOnShutdown := True в dpr файле, для того чтобы отслеживать утечки памяти.

Изучаем отладчик, часть первая

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

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

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

Объем статьи получился неожиданно большим, поэтому я разбил ее на три части:

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

Собственно, приступим.

1.1. Применение точек остановки и модификация локальных переменных

Одним из наиболее часто используемых инструментов встроенного отладчика является точка остановки (BreakPoint – далее BP). После установки BP, программа будет работать до тех пор, пока не достигнет точки остановки, после чего ее работа будет прервана и управление будет передано отладчику.

Самым простым способом установки и снятия BP является горячая клавиша «F5» (или ее аналог в меню «Debug->Toggle breakpoint»). Есть и другие способы, но о них позже.

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

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

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

Давайте рассмотрим следующий пример.

Есть задача: написать код, который 5 раз увеличит значение изначально обниленой переменной на единицу и еще один раз на число 123, после чего выведет результат в виде 10-тичного и 16-тиричного значения. Ожидаемые значения будут следующими: 128 и 00000080.

Допустим, код будет написан с ошибкой:

Данный код будет выводить какие угодно значения, но только не те, какие мы хотели, потому что мы не произвели инициализацию переменной «А» нулем. А так как переменная «А» локальна, значит, она расположена на стеке, и мы никогда не сможем предугадать, какое значение она примет в начале данной процедуры. Но будем считать, что уже конец рабочего дня, мы действительно устали (глаз замылился) и просто забыли написать строчку с инициализацией переменной.

В итоге мы имеем код, который выводит неверные значения, и хотим быстро разобраться в причине подобного поведения. Ставим BP в теле процедуры и запускаем программу на выполнение:

Должно получится примерно так, как на картинке. BP установлен на строчке Inc(A). Слева внизу можно наблюдать значение всех локальных переменных процедуры FormCreate (окно называется «Local Variables»), а именно, переменной Self (она передается неявно и всегда присутствует в методах класса), параметра Sender, и непосредственно локальной переменной «А». Ее значение 19079581. Слева в центре в «WatchList» значение переменной «B».

Даже бегло взглянув на значения обеих переменных и выполненные три строчки кода, мы сможем понять, что значение переменной «А» не соответствует ожидаемому. Так как должно было выполнится два инкремента на единицу и еще одно увеличение на число 123, мы должны были увидеть значением переменной «А» число 125, а раз там другое значение, то это может означать только одно – изначальное значение переменной «А» было не верным.

Для проверки правильности нашего предположения, давайте изменим текущее значение переменной «A» на верное и продолжим выполнение программы, чтобы проверить – те ли результаты вернет процедура, которые ожидались.

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

Первый – «Evaluate/Modify», вызывается либо через меню, либо по горячей клавише «Ctrl+F7». Это очень простой инструмент с минимумом функционала, он наиболее часто используется.

Для изменения в нем значения переменной, достаточно указать новое значение в поле «New value» и нажать клавишу «Enter» или кнопку «Modify».
Второй инструмент – «Inspect», доступен так же либо через меню «Run», либо уже непосредственно из диалога «Evaluate/Modify». Это более продвинутый редактор параметров, о нем чуть позже.

После изменения значения переменной «А», обратите внимание на изменения в списке значений локальных переменных:

Переменная «А» приняла правильное значение, и теперь мы можем продолжить выполнение нашего приложения нажатием «F9» или через меню, выбрав пункт «Run». В результате такого вмешательства с помощью отладчика, процедура выдаст нам ожидаемые числа 128 и 00000080, и мы уже можем смело исправлять код процедуры, т. к. мы нашли в нём причину ошибки и проверили его исполнение с правильно заданным значением переменной «A».

Теперь вернемся к «Inspect». Помимо двух указанных способов его вызова, он так же вызывается двойным кликом на переменной в окне «Local Variables», либо через контекстное меню при правом клике на ней, либо по горячей клавише «Alt+F5».

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

При его вызове сначала вы увидите вот такой диалог:

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

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

Через «Evaluate/Modify» доступ к свойствам объекта несколько затруднен тем, что он не предоставляет информации непосредственно об исследуемом объекте. Например, для получения хэндла канваса формы, нам придется в нем набрать следующий текст: «(Sender as TForm1).Canvas.Handle» – что несколько не удобно, ведь мы можем и опечататься, да и просто банально забыть название того или иного свойства.

В случае с «Inspect» такой проблемы не будет.

К примеру, давайте откроем диалог «Inspect» не для переменной «А», а для переменной Self (которая, как я и говорил ранее, всегда неявно присутствует для всех методов объектов).

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

1.2. Трассировка (пошаговая отладка)

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

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

А умеет он следующее:

  1. Команда «Trace Into» («F7») – отладчик выполнит код текущей строчки кода и остановится на следующей. Если текущая строчка кода вызывает какую либо процедуру, то следующей строчкой будет первая строка вызываемой процедуры.
  2. Команда «Step Over» («F8») – аналогично первой команде, но вход в тело вызываемой процедуры не происходит.
  3. Команда «Trace to Next Source Line» («Shift+F7») – так же практически полный аналог первой команды, но используется в окне «CPU-View» (данный режим отладки не рассматривается в статье).
  4. Команда «Run to Cursor» («F4») – отладчик будет выполнять код программы до той строчки, на которой сейчас находится курсор (с условием, что в процессе выполнения не встретилось других ВР).
  5. Команда «Run Until Return» («Shift+F8») – отладчик будет выполнять код текущей процедуры до тех пор, пока не произойдет выход из нее. (Часто используется в качестве контрприема на случайно нажатую «F7» и так же с условием, что в процессе выполнения не встретилось других ВР).
  6. В старших версиях Delphi доступна команда «Set Next Statement», при помощи которой мы можем изменить ход выполнения программы, установив в качестве текущей любую строку кода. Так же эта возможность доступна в редакторе кода там, где можно перетащить стрелочку, указывающую на текущую активную строчку в новую позицию.

Подробного рассмотрения данные команды не требуют. Остановимся только на команда «Trace Into» («F7»).

Для примера возьмем такой код:

При выполнении трассировки, в тот момент, когда мы находимся на строчке S.Add(), у нас могут быть два варианта реакции отладчика:

  1. мы войдём внутрь метода TStringList.Add,
  2. мы туда не войдём.

Обусловлено данное поведение настройками вашего компилятора. Дело в том что в составе Delphi поставляется два набора DCU для системных модулей. Один с отладочной информацией, второй — без. Если у нас подключен второй модуль, то команда «Trace Into» («F7») в данном случае отработает как «Step Over» («F8»). Настраивается переключение между модулями в настройках компилятора:

И отвечает за данный функционал параметр «Use Debug DCUs».

1.3. Подробнее о настройках компилятора

Опции в закладке с настройками компилятора влияют непосредственно на то, какой код будет генерироваться при сборке вашего проекта. Очень важно не забывать, что при изменении любого из пунктов данной вкладки, требуется полная пересборка проекта («Project > Build») для того, чтобы изменения вступили в силу. Данные настройки непосредственно влияют на поведение вашего кода в различных ситуациях, а так же на состав информации, доступной вам при отладке проекта.

Рассмотрим их поподробнее:

Группа «Code generation»

Данный параметр влияет непосредственно на оптимизацию кода: при включенном параметре код будет сгенерирован максимально оптимальным способом с учетом как его размера, так и скорости исполнения. Это может привести к потере возможности доступа (даже на чтение) к некоторым локальным переменным, ибо из-за оптимизации кода они уже могут быть удалены из памяти в тот момент, когда мы прервались на BP.

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

Как видите, значения ранее доступных переменных Self и Sender, более не доступны. Так же из-за отключенного параметра «Use Debug DCUs» произошло кардинальное изменение в окне «Call Stack», ранее заполненного более подробной информацией о списке вызовов.
Более того, инструмент «Inspect» так же отказывается работать с объектом Self, выдавая следующую ошибку:

Параметры «Stack Frames» и «Pentiom-safe FDIV»

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

Параметр «Record field alignment»

Глобальная настройка выравнивания неупакованных записей, которая может быть изменена локально в пределах модуля директивой «<$Align x>» или «<$A x>»

Для примера рассмотрим следующий код:

Размер данной записи, который мы можем получить через SizeOf(T), будет для каждой из настроек выравнивания свой:

Группа «Syntax options»

Тут лучше вообще ничего не трогать. Ибо, если постараться, то можно сделать даже так, что стандартная VCL откажется собираться.

Единственно остановлюсь на параметре «Complete boolen eval», ибо периодически некоторые его включают. Он грозит ошибкой при выполнении следующего кода:

Так как, при включении данной настройки, булево выражение будет проверяться целиком, то произойдет ошибка при обращении к Value.Count, не смотря на то, что первая проверка определила, что параметр Value обнилен. А если вы включите (например) параметр «Extended syntax», то данный код у вас вообще не соберется, пожаловавшись на необъявленную переменную Result.

Группа «Runtime errors»

Параметр «Range checking»

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

В самом простом случае вам будет сгенерировано исключение при выполнении вот такого кода:

Здесь мы просто пытаемся обратится к элементу массива, и в принципе, при отключенной опции «Range checking», если мы не выйдем за границу выделенной памяти, данный код нам грозит только тем, что в заголовке формы появится некая непонятная строка.

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

Рассмотрим такой пример, оптимизацию отключим:

Как вы думаете, чему будет равно значение числа HazardVariable после выполнения данного кода? Нет, не 100. Оно будет равно 4. Так как мы ошиблись при выборе типа итератора и вместо TMyEnum2 написали TMyEnum1, произошел выход за диапазон границ массива и затерлись данные на стеке, изменив значения локальных переменных хранящихся на нём же.

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

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

Поэтому возьмите себе за правило – отладка приложения всегда должна происходить с включенной настройкой «Range checking»!

Так же данный параметр контролирует выход за границы допустимых значений при изменении значения переменных. Например, будет поднято исключение при попытке присвоения отрицательного значения беззнаковым типам наподобие Cardinal/DWORD, или при попытке присвоить значение большее, чем может содержать переменная данного типа, например, при присвоении 500 переменной типа Byte и т. п…

Параметр «I/O cheking»

Отвечает за проверку результатов ввода/вывода при работе с файлами в стиле Pascal.

Не уверен, что еще остался софт, использующий данный подход, но если вы вдруг все еще работаете с Append/Assign/Rewrite и т. п., то включайте данный параметр при отладке приложения.

Илон Маск рекомендует:  Что такое доменное имя

Параметр «Overflow cheking»

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

Чтобы было проще понять различия между данным параметром и «Range checking», рассмотрим следующий код:

Данный код не поднимет исключения при включенном параметре «Overflow cheking». Хоть здесь и присваиваются переменным не допустимые значения, но не производится математических операций над ними. Однако исключение будет поднято при включенном параметре «Range checking».

А теперь рассмотрим второй вариант кода:

Здесь уже не будет реакции от параметра «Range checking», но произойдет поднятие исключения EIntegerOverflow, за который отвечает «Overflow cheking», на строчках Inc(B) и C := C — 1 из-за того, что результат арифметической операции не может быть сохранен в соответствующей переменной.
Таким образом, при работе с переменными оба параметра взаимодополняют друг друга.


«Overflow cheking» не настолько критичен, как «Range checking», но всё же желательно держать его включенным при отладке приложения.

Небольшой нюанс: если вы вдруг реализуете криптографические алгоритмы, то в них, как правило, операция переполнения является штатной. В таких ситуациях выносите код в отдельный модуль и в начале модуля прописывайте директиву «<$OVERFLOWCHECKS OFF>» для отключения проверки переполнений в текущем модуле.

С данной вкладкой все очень просто. Все параметры, за исключением параметра «Assertions», никоим образом не влияют на финальный код вашего приложения. В зависимости от активности тех или иных параметров изменяется полнота отладочной информации в DCU файле для каждого модуля. На основе данной информации отладчик производит синхронизацию ассемблерного листинга программы с ее реальным кодом, реализованным программистом, распознает локальные переменные и т. п. При компиляции приложения данная отладочная информация не помещается в теле приложения.
Единственным исключением является параметр «Assertions» – он отвечает за работу процедуры Assert(). Если данный параметр отключен – Assert не выполняется, в противном случае – выполняется, причем его код так же будет помещен в тело приложения на этапе компиляции.

На этапе отладки приложения желательно держать все параметры из групп «Runtime errors» и «Debugging» включенными, и отключать их при финальной компиляции релизного приложения. В Delphi 7 и ниже это придется делать руками, но, начиная с Delphi 2005 и выше, появилась нормальная поддержка билдов проекта, в которой можно указывать данные комбинации флагов персонально для каждого типа сборки.

1.4. Окно стека вызовов («Call Stack»)

Если ВР является нашим основным инструментом при отладке приложения, то «Call Stack» второй по значимости.

Выглядит данное окно следующим образом:

Он содержит полное описание вызовов, которые были выполнены до того, как отладчик прервал выполнение программы на установленном ВР (или остановился из-за возникновения ошибки). Например, на скриншоте изображен стек вызовов, произошедших при нажатии кнопки на форме. Начался он с прихода сообщения WM_COMMAND (273) в процедуру TWinControl.DefaultHandler.

Имея на руках данный список, мы можем быстро переключаться между вызовами двойным кликом (или через меню «View Source»), просматривать список локальных переменных для каждого вызова («View Locals»), устанавливать ВР на любом вызове.

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

Например вот так будет выглядеть стек вызовов при возникновении ошибки EAbstractError:

В данном случае достаточно найти самый первый сверху вызов, код которого расположен не в системных модулях Delphi, чтобы с большой долей вероятности сказать, что ошибка именно в нём. Таким вызовом является Unit1.TForm1.Button1Click() — это обработчик кнопки Button1 в котором выполнялся следующий код:

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

  1. перейти в модуль, где объявлен вызов интересующей нас функции (в данном случае это windows.pas),
  2. найти её объявление (строка с синей точкой function MessageBox; external user32. ),
  3. установить на данной строке ВР и запустить программу.

Как только из любого места программы произойдет вызов MessageBox, сработает наш ВР и мы сможем – на основании данных «Call Stack» – выяснить точное место его вызова.

1.5. Работа с расширенными свойствами точек остановки

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

Делается это посредством диалога настроек свойств точки остановки. Вызывается он либо через свойства BP в коде приложения.

Либо в окне «Breakpoint list» так же через свойства выбранной BP.

Выглядит диалог настроек следующим образом:

Параметр «Condition» отвечает за условие срабатывания точки остановки.
Параметр «Pass count» указывает, сколько таких условий нужно пропустить, прежде чем ВР будет активирована, причем подсчёт количества срабатываний ведется от самого первого, с учетом значения параметра «Condition».

Рассмотрим абстрактный пример:

Допустим, ВР установлена на седьмой строчке (RandomValue := . ). Если программу просто запустить, то мы получим на руки ровно 100 срабатываний ВР. Для того чтобы данная ВР срабатывала каждый десятый вызов необходимо в свойстве «Pass count» выставить значение 10. В этом случае мы получим ровно десять срабатываний ВР, в тот момент кода итератор «I» будет кратен десяти.

Допустим, теперь мы хотим начать анализ после 75 итерации включительно, для этого выставим следующее условие в параметре «Condition»: I > 75. В этом случае данная ВР сработает всего два раза: в тот момент, когда итератор «I» будет равен 85, и второй раз, при значении 95.

Произошло это по следующим причинам:

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

Во втором случае увеличение счетчика сработок начало происходить только после выполнения изначального условия «Condition», т. е., пока итератор «I» был меньше или равен числу 75, отладчик считал, что условие не выполнено и продолжал выполнение программы. Как только первое условие выполнилось, началось увеличение количества срабатываний, которое стало равным значению параметра «Pass count» именно в тот момент, когда итератор «I» достиг значения 85.

Естественно, если мы хотим, чтобы ВР начала срабатывать сразу после превышения итератором «I» числа 75, то параметр «Pass count» необходимо выставить в ноль.

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

Теперь рассмотрим один небольшой нюанс.

Условия передачи управления отладчику, указанные в свойствах ВР, рассчитываются самим отладчиком. Т.е. грубо (взяв за основу вышеописанный пример), не смотря на то, что мы прервались на ВР всего лишь два раза, на самом деле прерывание произошло все 100 раз, просто мы не получали управление в те моменты, которые не соответствовали заданным нами условиям.

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

Можно проверить на следующем коде:

Установим ВР на той же седьмой строчке и укажем в параметре «Condition» значение I=9999. Даже на таком маленьком цикле нам придётся ждать срабатывания условия в районе 3-5 секунд. Конечно же, это не удобно. В таких случаях проще модифицировать код следующим образом:

… и поставить ВР на Beep, чем ждать столь продолжительное время. В этом случае мы получим управление на практически мгновенно.
(В релизной сборке проекта директива DEBUG будет отсутствовать и отладочный код не попадет в неё, но лучше, после отладки, все же не забывать удалять все эти отладочные Beep-ы. )

Подобные «тормоза» обусловлены тем, что всё взаимодействия отладчика с отлаживаемым приложением происходит через механизм структурной обработки исключений (SEH), более известный Delphi программистам через куцую обертку над ним в виде try..finally..except. Работа с SEH является одной из наиболее «тяжелых» операций для приложения. Дабы не быть голословным и показать его влияние на работу программы наглядно, рассмотрим такой код:

В функции Test1 и Test2 происходит инкремент переданного значения 100 миллионов раз.

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

Это, кстати тоже вам в «копилочку» – по возможности не вставляйте обработку исключений внутрь циклов, лучше выносите её за пределы…

Не рассмотренным у нас остался параметр «Group», он отвечает за включение BP в определенную группу точек остановки. Группа – понятие условное, на самом деле это некий идентификатор, произвольно задаваемый разработчиком, но удобен он тем, что к данным идентификаторам можно применить групповые операции управляя активностью всех BP, входящих в данную группу.

Групповые операции настраиваются в расширенных настройках ВР:

Отвечают за это параметры: «Enable group» – активирующий все ВР группы, и «Disable group» – отключающий все ВР входящие в группу.

Так же при групповых операциях часто применяется параметр «Break», который отвечает за действия отладчика при достижении ВР. Если данный параметр не активен, то прерывания выполнения программы при достижении данной ВР не происходит.
Важно – данный параметр не отключает саму ВР.

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

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

Перед компиляцией примера обязательно включите в настройках компилятора опцию «Overflow cheking» и отключите оптимизацию.

После запуска данного кода, произойдет исключение на шестнадцатой строчке

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

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

Сделаем это следующим образом:

  1. Поставим ВР на шестнадцатой строчке и назначим ей группу «level2BP».
  2. Отключим данную группу, чтобы установленная ВР не срабатывала раньше времени. Для этого в процедуре FormCreate поставим новую ВР на ShowMessage и в параметре «Disable group» укажем группу «level2BP». Чтобы не прерываться на новой ВР, в его настройках отключим параметр «Break».
  3. В функции Level1 устанавливаем ВР на строчке №25. Посчитаем, сколько раз выполнится данная ВР перед появлением ошибки.
  4. Выясняем, что было 9 прерываний (итератор I в этот момент равен восьми). Значит, нам нужно пропустить первые 8 прерываний, в которых ошибок не обнаружено, и на девятом включить ВР из группы «level2BP». Для этого заходим в свойства текущей ВР и выставляем в параметре «Condition» значение I=8, после чего исключаем его из обработки через отключение параметра «Break» и в настройках «Enable group» прописываем «level2BP».
  5. Перезапустив приложение, мы сразу прервемся в процедуре Level2, но не в момент самой ошибки – ошибка произойдет через несколько итераций. Несколько раз нажмем F9, считая количество итераций, и выясним, что это происходит в тот момент, когда итератор I был равен 5. В параметре «Condition» текущей ВР установим условие I=5, после чего можно смело перезапускать приложение.
  6. Первое же прерывание в отладчике произойдет непосредственно в месте возникновения ошибки, откуда и можно приступать к разбору причин ее возникновения.

Если из описания примера не все понятно — посмотрите ролик, демонстрирующий всю последовательность действий:
rouse.drkb.ru/blog/bp3.mp4 (17 Мб).
(я извиняюсь, но вставить ссылку так, чтобы ролик отображался прямо в теле статьи, у меня не получилось, поэтому только ссылка)

Таким образом, используя всего три ВР и групповые операции над ними, мы достаточно точно локализовали место ошибки и обеспечили удобство отладки кода.

Почему в примере не использовался параметр «Pass Count», а условия задавались через параметр «Condition»? Дело в том, что «Pass Count» просто отключает прерывание на ВР. Сама же ВР выполняется (т. к. условия её выполнения описаны в параметре «Condition») и раз она выполнилась, то выполняются и её групповые операции.

Осталось рассмотреть еще несколько параметров.

Параметр «Ignore subsequent exceptions» отключает реакцию отладчика на любое исключение, возникшее после выполнения ВР с включенным данным параметром.

Параметр «Handle subsequent exceptions» отменяет действие предыдущего параметра, возвращая отладчик в нормальный режим работы.

Чтобы посмотреть как это выглядит, создадим такой код:

На первом ShowMessage поставьте ВР, отключите его, сняв галку с параметра «Break», и включите параметр «Ignore subsequent exceptions».

На втором ShowMessage сделайте то же самое, только включите параметр «Handle subsequent exceptions».

Запустите приложение из отладчика и пощелкайте по кнопкам в следующем порядке:

  1. Button1
  2. Button2
  3. Button3
  4. Button4
  5. Button2
  6. Button3

Не смотря на то, что кнопки Button2 и Button3 генерируют исключение, на этапе 2 и 3 отладчик на них никак не прореагирует, мы дождемся от него реакции только на этапах 5 и 6 после того, как активируем нормальную обработку исключений нажатием кнопки Button4.

Осталось 2 параметра:

«Log message» – любая текстовая строчка, которая будет выводится в лог событий при достижении ВР.

«Eval expression» – при достижении ВР, отладчик вычисляет значение данного параметра и (в случае если включен флаг «Log result») выводит его в лог событий. Значение для вычисления может быть любым, хоть тот же «123 * 2».

1.6. Использование «Data breakpoint», «Watch List» и «Call Stack»

Все, что мы рассматривали ранее, относилось к так называемым «Source Breakpoint». Т. е. к точкам остановки, устанавливаемым непосредственно в коде приложения.

Но, помимо кода, мы работаем с данными (переменными, массивами, просто с участками выделенной памяти) и у отладчика есть возможность устанавливать BP на адреса, по которым эти данные расположены в памяти, при помощи «Data breakpoint».

Установка ВР на адрес памяти производится через «Watch List» (не во всех версиях Delphi) или в окне «Breakpoint List» при помощи «Add Breakpoint->Data Breakpoint», где, в появившемся диалоге, указываем требуемый адрес, размер контролируемой области или имя переменной. В случае указания имени переменной, отладчик попробует вычислить ее расположение в памяти и (если это возможно) установит ВР.

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

Что такое область видимости переменной – вы должны знать. Глобальные переменные доступны нам всегда, и, даже без запуска приложения, отладчик предоставляет нам возможность устанавливать «Data breakpoint» на изменения в таких переменных. Правда, в данном случае он рассчитывает адрес такой переменной на основании предыдущей сборки приложения, и не факт, что он совпадет с ее расположением при следующем запуске. Ситуация гораздо хуже с локальными переменными. Область видимости переменной – это не просто так введенное понятие, локальные переменные расположены на стеке, и, как только они выходят из области видимости, место, занимаемое ими ранее, используется под хранение совершенно других данных. Таким образом установить «Data breakpoint» на локальную переменную можно только в тот момент, пока она не вышла из области видимости.

Те, кто ранее работал с профессиональными отладчиками, вероятно узнают в «Data breakpoint» один из базовых инструментов анализа приложения – «Memory Breakpoint».

К сожалению, отладчик Delphi не позиционируется как профессиональное средство отладки сторонних приложений, поэтому столь полезный инструмент как «Memory Breakpoint» представлен в нем в обрезанном варианте, где от него оставлена только возможность контроля адреса памяти на запись.

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

Рассмотрим следующий код:

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

Нажав на «Break», мы окажемся где-то внутри модуля «system»:

Код, на котором мы прервались, ничего нам не может сказать о причине возникновения ошибки, но у нас есть окно «Call Stack», на основании которого мы можем сделать вывод, что ошибка произошла при вызове процедуры ShowCaption в главном модуле программы.
Если установить BP в данной процедуре и перезапустить программу, а затем, при срабатывании ВР, проверить значение переменной Caption, то окажется что данная переменная не доступна:

Это означает, что где-то произошло разрушение памяти и затерлись данные по адресу переменной Caption. Определить это место нам поможет «Data breakpoint».

  1. Дождемся инициализации переменной Caption, для этого установим ВР на строчке FT.Description := ‘Test Description’;.
  2. При срабатывании ВР, добавим переменную FP.Caption в «Watch List» и в свойствах этой переменной выберем «Break When Changed». Если данного пункта меню у вас нет (например, в Delphi 2010 он отсутствует), то установим «Data breakpoint» немного другим способом. В «Breakpoint List» выбираем «Add->Data Breakpoint», в появившемся диалоге указываем имя переменной FP.Caption и нажимаем ОК.
  3. Запускаем программу на выполнение.

После выполнения этих действий, программа остановится на строчке №68 – Inc(Value). Особенность «Data breakpoint» в том, что остановка происходит сразу после произошедших изменений, а не при попытке изменения контролируемой памяти, поэтому место, где происходит запись по адресу переменной FP.Caption, находится строчкой выше – это строка Value^ := ValueData[I].

Теперь, найдя проблемное место, мы можем исправить и саму ошибку. Она заключается в том, что длина строки ValueData, которую мы пишем в буфер Data, превышает размер буфера, из-за чего происходит перезапись памяти, в которой расположены переменные Caption и Description.

1.7. В заключение

На этом я заканчиваю краткий обзор возможностей интегрированного отладчика. Осталось несколько нерассмотренных нюансов, как то: настройка игнорируемых исключений, ВР при загрузке модуля и т.п., но они несущественны и крайне редко применяются на практике.
Так же нерассмотренным остался режим отладки в окне «CPU-View» и связанные с ним Address Breakpoint. Его я так же решил пропустить, т.к. читателям не знакомым с ассемблером мое объяснение не даст ничего, а более подкованные специалисты и без меня знают что такое CPU-View и как его правильно применять :)

Во второй части статьи, будет рассмотрена программная реализация отладчика. В ней будет показано, что именно происходит при установке BreakPoint, показана обратная сторона Data Breakpoint, не реализованная в отладчике Delphi, показано как в действительности производится трассировка (двумя методами, классический через TF флаг и на основе GUARD страниц), а так же рассмотрен механизм Hardware Breakpoint, тоже отсутствующий в интегрированном отладчике Delphi.

Отдельная благодарность сообществу форума «Мастера Дельфи» за помощь при подготовке статьи, а также персональное спасибо Андрею Васильеву aka Inovet, Тимуру Вильданову aka Palladin и Дмитрию aka Брат Птибурдукова за вычитку материала и ценные советы.

Александр (Rouse_) Багель
Москва, октябрь 2012

Возможные скрытые причины абстрактной ошибки в Delphi?

В проекте Delphi 7 мы установили FastMM. Вскоре после этого мы заметили, что одна из форм начала выдавать сообщение Abstract Error при закрытии. Я отлаживал это широко, и пока я не могу найти причину. Обычная причина этого сообщения об ошибке, похоже, не применяется здесь. Приложение не определяет абстрактные классы. Я также искал форму для возможного использования TStrings или что-то в этом роде. Самое главное, что мы не сделали (ну, как мы думаем, мы этого не сделали) внесли какие-либо изменения в эту форму. Он просто сломался.

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

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

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

Чтобы ответить на ваши вопросы: 1) Да, абстрактная ошибка также может быть вызвана повреждением памяти, и 2) Да, включение FastMM может сделать ошибки видимыми, которые обычно проходят незамеченными (но все равно должны быть исправлены).

Некоторые общие рекомендации для поиска ошибок памяти:

  • Попробуйте установить «FullDebugMode» в FastMM.
  • Удостоверьтесь, что все, что вы создаете, совпадает со свободным.
  • Убедитесь, что ничего не освобождается более одного раза.
  • Убедитесь, что объект не используется после его освобождения (или до его создания).
  • Включите подсказки и предупреждения (и исправьте их, когда они произойдут).

«Он просто сломался» — он, вероятно, всегда сломался, но теперь вы знаете.

Я видел проблемы при закрытии формы как части события кнопки. Форма уничтожается, а затем оставшаяся часть сообщений кнопки отправляется на более длинную существующую кнопку. Метод Release позволяет избежать этого путем (из памяти) отправки сообщения wm_close обратно в форму

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

Ответ на вопрос 1 «Существуют ли другие возможные причины этой ошибки, кроме попытки вызова невыполненного метода?»

Да. Вот что вызвало в моем случае абстрактную ошибку:

Это работало, когда отправитель был TButton, но вызывало ошибку (конечно), когда отправитель был чем-то другим (например, TAction). Это была, очевидно, моя вина. Я должен был использовать «как» вместо жесткого типа.

Ответ на вопрос 2: Да. Я тоже это видел. Нам должно быть очень ясно, что это не означает, что FastMM глючит. Ошибка была «неактивной». FastMM только активировал его.
На самом деле вы должны полагаться на FastMM еще больше, чтобы найти свою проблему. Для этого переключите FastMM в режим полной отладки. Это поможет вам:

Убедитесь, что объект не используется после того, как он был освобожден (или до него был создан)

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

Вы ДОЛЖНЫ также исправить ВСЕ предупреждения, которые показывает компилятор! Компилятор серьезно относится к этому. Это не вызвало бы предупреждение без уважительной причины. Исправьте это, и вы, вероятно, исправите свою проблему.

В этом конкретном случае я бы также заменил все .Free на FreeAndNil() .

Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL