[ Миронченко ] Императивное и объектно-ориентированное програмирование на Turbo Pascal и Delphi
.pdf271
Остальные функции вы можете найти в справочной системе, написав фразу
file management routines TSearchRec определяется так:
type
TSearchRec = record Time: Integer;
Size: Integer; //размер файла в байтах
Attr: Integer; //атрибуты файла
Name: TFileName; //название файла с расширением, но без пути доступа
ExcludeAttr: Integer;
FindHandle: THandle;
FindData: TWin32FindData; // вспомогательная инф.: время создания, полное имя и т.д.
end;
Вследующем примере мы напишем программу, которая будет искать все файлы
сзаданным расширением в подкаталогах.
Пример 7: Поиск файлов в директории.
1 : program Kap15b04;
2 :
3 : {$APPTYPE CONSOLE}
4 :
5 : uses
6 : SysUtils,
7 : Dialogs;
8 :
9 : const
10:n=10000;
11:type
12:MeinTSearchRec = record
13:TSR:TSearchRec;
14:VollName:string; {имя файла вместе с полным путем к нему}
15:end;
16:DateienInfoMas = array [1..n] of MeinTSearchRec;
17:
18:{Заносит в массив M все файлы в директории Dir (включая все
19:поддиректории) с расширением Erweiterung. В TotQuant записывает
20:общее количество таких файлов.}
21:procedure AlleDateienZuMas(var M:DateienInfoMas;var TotQuant:integer;var Dir,Erweiterung:string);
22:var
23:sr:TSearchRec;
24:s:string;
25:begin
26:{$I-} //ошибки ввода-вывода надо проверять самому с помощью
IOResult
27:ChDir(Dir); //устанавливаем новый текущий каталог
28:
29: if IOResult<>0 then //Если каталог Dir не существует
272
30:begin
31:TotQuant:=0;
32:exit;
33:end;
34:// Ищем pas-файлы в текущей директории
35:if FindFirst('*.'+Erweiterung,faAnyFile,sr)=0 then
36:repeat
37:inc(TotQuant);
38:M[TotQuant].TSR:=sr;
39:M[TotQuant].VollName:=Dir+'\'+sr.Name;
40:until FindNext(sr)<>0;
41: |
FindClose(sr); |
//освобождаем память, выд. под FindFirst |
42: |
|
|
43://Ищем подкаталоги
44:if FindFirst('*.*',faDirectory,sr)=0 then
45:repeat
46:if (sr.Name<>'.') and (sr.Name<>'..') and (sr.Attr=faDirectory)then //Если подкаталог
47:begin
48: s:=Dir+'\'+sr.Name;//Записываем в s путь к подкаталогу
49: alleDateienZuMas(M,TotQuant,s,Erweiterung); //пробегаем файлы подкаталога
50: ChDir(Dir); // снова возвращаемся к исходному каталогу
51:end;
52:until FindNext(sr)<>0;
53:FindClose(sr); //освобождаем память, выд. под FindFirst
54:end;
55:
56:{Выдает общий объем всех файлов, содержащихся в M}
57:function NehmTotUmfang(var M:DateienInfoMas;TotQuant:integer):integer;
58:var
59:i,s:integer;
60:begin
61:s:=0;
62:for i:=1 to ToTQuant do
63:s:=s+M[i].TSR.Size;
64:result:=s;
65:end;
66:
67:{Печатаем все M[i] в поле для вывода}
68:procedure SchrDateien(var M:DateienInfoMas;TotQuant:integer);
69:var
70:i:integer;
71:begin
72:for i:=1 to TotQuant do
73:writeln(M[i].VollName);
74:writeln('Quantitat = ',TotQuant);
75:writeln('Umfang (in Bytes) =',NehmTotUmfang(M,TotQuant));
76:end;
77:
78: var
273
79:DatMas:dateienInfoMas;
80:TotQuant:integer;{Количество записей в массиве}
81:Weg,Erw:string;
82:
83:begin
84:Weg:=InputBox('Поиск файлов в поддиректории','Введите путь к файлам','');
85:Erw:=InputBox('','Введите расширение (например - pas)','');
86:AlleDateienZuMas(DatMas,TotQuant,Weg,Erw);
87:SchrDateien(DatMas,TotQuant);
88:readln;
89:end.
Разберем процедуру AlleDateienZuMas. В 26-й строке используется директива компилятора {$I-}. Она означает, что ошибки ввода-вывода, которые будут происходить при использовании файлов, придется обрабатывать самостоятельно, используя функцию IOResult. По умолчанию используется {$I+}, при которой генерируются при ошибках генерируются исключения, которые можно затем обрабатывать. Этот способ больше отвечает объектно-ориентированному подходу, но мы еще не знаем ни ООП, ни исключений, поэтому будем использовать IOResult.
IoResult возвращает 0, если ошибок не было. В программе, если указанная директория не существует, то поиск прекращается, а количество найденных файлов устанавливается равным 0. Сам алгоритм рекурсивный – чтобы найти все файлы в текущем каталоге, надо просмотреть все файлы в подкаталогах.
15.7. Файлы, связанные с проектом
Когда мы работали с ТР, основная программа и программный код модулей были записані в файлах с расширением .pas, откомпилированные модули – в файлах с расширением tpu и еще, естественно, мы могли создавать .exe файл, который и был готовым приложением.
ВDelphi в состав приложения входит значительно большее количество файлов:
•Основной файл проекта имеет расширение dpr (Delphi Project).
•Программный код модулей находится в файлах, имеющих расширение pas. После компиляции этих файлов создаются файлы соответствующие файлы с машинным кодом, но расширение у них не tpu, как в ТР, а dcu (Delphi compiled unit).
•Если модуль содержит описание формы, то помимо pas-файла с исходным кодом создается файл с расширением dfm, в котором находится описание формы (в текстовом или двоичном виде).
•Кроме того, приложение содержит файлы ресурсов (напр. значок программы), которые присоединяются компилятором на этапе компоновки проекта к исполняемому файлу. Они имеют расширение res.
•Файлы настройки содержат параметры компилятора (с расширением cfg), проекта (с расширением bdsproj или dof в Delphi 2005 или Delphi 6,7 соответственно).
После компиляции проекта создается exe-файл, содержащий код готового
приложения.
274
15.8. Динамически подключаемые библиотеки (DLL)
Модули, на которые разбивается программа, подключаются к основной программе статически – т.е. при компоновке программы. С помощью DLL можно подключать части программы во время ее работы. Кроме того, используя DLL, можно использовать подпрограммы, написанные на других языках программирования. Использование DLL поддерживается операционной системой Windows (для этого в Win32 API есть специальные функции). Сама Windows тоже состоит из ряда DLL.
Описание библиотек отличается от описания основной программы лишь тем, что начинается файл с исходным кодом библиотеки с ключевого слова Library, а также в библиотеке есть раздел exports, в котором перечисляются подпрограммы, которые экспортируются библиотекой (т.е. подпрограммы, которые могут использоваться в программах, которые подключают DLL).
Пример 8: Пишем простую библиотеку. library MeineDLL;
function Summe(var A:array of integer):integer; stdcall; var
i:integer; begin
result:=0;
for i:=0 to high(A) do result:=result+A[i];
end;
procedure ZufalligMas(var A:array of integer;k:integer); stdcall; var
i:integer; begin
for i:=0 to High(A) do A[i]:=random(k);
end;
procedure SchreibMas(var A:array of integer); stdcall; var
i:integer; begin
for i:=0 to High(A) do write(A[i],' ');
end;
Exports
Summe index 1 name 'Summe',
ZufalligMas index 2 name 'ZufalligMas', SchreibMas index 3 name 'SchreibMas';
begin end.
275
Называется библиотека MeineDLL, следовательно, после компиляции появится файл MeineDLL.dll, который и будет подключаться к программам.
Все функции в библиотеке – вполне обычные, но объявлены с директивой stdcall для того, чтобы не возникало проблем при использовании библиотеки в программах, написанных на других языках программирования.
В конце библиотеки находится раздел exports, в котором «на экспорт» идут все 3 подпрограммы, написанные в библиотеке. После названия процедуры следует индекс, который позволяет при подключении библиотеки быстро находить адрес подпрограммы. Name – это внешнее имя подпрограммы (оно может отличаться от внутреннего, т.е. имени, которое ей дано внутри библиотеки).
15.9. Подключение библиотеки
Можно подключать функции из библиотеки просто с помощью директивы external: после полного названия подпрограммы ставится директива external и название библиотеки. Такое подключение иногда называется статическим т.к. функции после начала работы программы экспортируются из библиотеки и затем присутствуют в программе до ее завершения. Однако все равно подпрограммы подключаются не на этапе компоновки, а после запуска приложения.
Пример 9: Статическое подключение библиотеки. program StatischeDLL;
{$APPTYPE CONSOLE} const
n=5; var
A:Array [1..n] of integer;
function Summe(var A:array of integer):integer; stdcall; External 'MeineDLL';
procedure ZufalligMas(var A:array of integer;k:integer); stdcall; external 'MeineDLL';
procedure SchreibMas(var A:array of integer); stdcall;external 'MeineDLL';
begin randomize;
ZufalligMas(A,45);
SchreibMas(A);
writeln;
writeln(Summe(A));
readln;
end.
Как видите, были подключены 3 подпрограммы из библиотеки и успешно использованы в программе.
Для того чтобы «динамически» подключать библиотеки, т.е. самим решать, когда подключать библиотеку, а когда выгружать ее из памяти, надо использовать API-
276
функции LoadLibrary, GetProcAddress, FreeLibrary, которые описаны в модуле Windows.
Мы разберемся в том, как они работают на примере.
Пример 10: Динамическое подключение библиотеки. program DynDLL;
{$APPTYPE CONSOLE} uses
Windows; const
n=5; type
Mas=Array [1..n] of integer;
SummFunk = function (var A:array of integer):integer; stdcall; FullProz = procedure (var A:array of integer;k:integer); stdcall; SchrProz = procedure (var A:array of integer); stdcall;
var A:Mas;
H:LongWord;
Summe:SummFunk;
ZufalligMas:FullProz;
SchreibMas:SchrProz;
begin H:=LoadLibrary('MeineDLL.dll'); if H=0 then
begin
writeln('Нет такой библиотеки'); readln;
exit;
end;
@Summe:=GetProcAddress(H,PChar(1));
@ZufalligMas:=GetProcAddress(H,'ZufalligMas');
@SchreibMas:=GetProcAddress(H,'SchreibMas');
randomize;
ZufalligMas(A,45);
SchreibMas(A);
writeln;
writeln('Summe ',Summe(A)); FreeLibrary(H);
readln;
end.
В программе создаются все необходимые процедурные типы и переменные этих типов. В исполняемой части программы сначала загружается библиотека, затем в переменные функциональных типов загружаются все соответствующие функции и процедуры из библиотеки. Перед завершением работы программы, следует выгрузить библиотеку из памяти.
При динамическом подключении подпрограмм вы можете много раз загружать и выгружать различные библиотеки из памяти, что очень удобно.
277
15.10.Использование модулей в DLL
ВDLL можно использовать модули, как и в обычной программе, но отличие в том, что внутри модуля как в разделе implementation, так и в разделе interface можно экспортировать подпрограммы (как и в самой библиотеке, с помощью exports). При создании библиотеки все модули и сама библиотека компилируются в один файл с расширением dll, и при подключении этой библиотеки из основной программы можно подключать все необходимые функции.
Инициализирующая и завершающая часть модулей, на которые ссылается библиотека будут выполнены только в случае, если на них ссылается основная программа (прямо, или косвенно, через какой-то из своих модулей).
15.11.Что нам дает DLL?
1.Не надо постоянно хранить в памяти всю программу, - можно подключать и убирать куски кода, которые уже не нужны.
2.Программа может изменять саму себя во время своей работы: часть программы, которая находится в памяти, может изменять исходный код некоторых библиотек, или создавать новые библиотеки, затем вызывать компилятор, который сможет создать из исходного кода библиотеку и затем подключать уже измененный код.
3.Привлекательна возможность написания программы на разных языках.
4.Но: библиотека может экспортировать только подпрограммы. Ни типы, ни переменные она экспортировать не в состоянии. Следовательно, классы из DLL переносить нельзя.
5.При использовании DLL следует учитывать, что если ваша подпрограмма использует модули в разных подпрограммах, то компилятор подключает к программе 1 экземпляр модуля, а если вы 2 раза подключите одну DLL в разных местах программы, то к программе будет подсоединено 2 копии одной и той же библиотеки.
278
Глава 16: Объектно-ориентированное программирование
16.1.Модульный подход и ООП
Вмодульном программировании программа состояла в общем случае из нескольких модулей, в которых сосредоточены подпрограммы, типы данных, глобальные переменные, и т.д. Разбиение программы на модули, как вы помните, - это не просто разделение одного большого файла с кодом на несколько, - модули обладают определенной самостоятельностью, т.к. у них есть внутренние подпрограммы, константы, типы …, которые невидимы в программе, которая подключает данный модуль. Но модульность принципиально не изменила подход процедурного программирования, который можно кратко выразить так: «натравить процедуру на матрицу». Локальность подпрограмм в модулях во многом напоминает локальность одной подпрограммы относительно другой.
Процедурный подход часто не соответствует нашему пониманию мира, который мы моделируем в программе: мы говорим, что машина едет, а не то, что есть функция «ехать», которая применяется к машине. Скорее у машины есть способность перемещаться, которая является ее неотъемлемой частью, иначе она – просто металлолом, а не средство передвижения. Но в таком случае мы должны каким-то образом объединить данные – саму машину (ее детали) с функциями машины – перемещаться, расходовать бензин и т.д. Именно объединение данных и функций положено в основу объектно-ориентированного программирования (ООП), которое мы подробно рассмотрим в этой главе.
Введем основные определения:
•Тип данных, в котором объединены данные и функции, называется классом.
•Экземпляр класса называется объектом этого класса.
•Подпрограммы, которые находятся внутри класса, называются методами класса.
•Данные, которые находятся в классе, называются полями.
Позже мы рассмотрим еще одни элементы класса – свойства.
•Объектно-ориентированное программирование - методология программирования, основанная на использовании классов.
•Объединение в рамках одного типа полей и методов называется инкапсуляцией.
16.2. Как объединить данные с подпрограммами средствами процедурного программирования
Вы знаете, что программа хранится в памяти во время выполнения, следовательно, и сами подпрограммы, которые являются частью программы, тоже обладают определенным адресом (адрес подпрограммы = адрес ее начала) в оперативной памяти ПК. Кроме того, вы умеете пользоваться процедурными и функциональными типами, которые, по сути, являются ссылками на начало подпрограммы. Значит, чтобы объединить подпрограммы с данными, можно создать запись, в которой вместо каждой из функций находилась бы ссылка на некоторую подпрограмму. Безусловно, сама запись стала бы очень громоздкой, однако цели мы бы добились. Однако, для создания переменной такой записи, надо самостоятельно настроить все ссылки на подпрограммы, т.е. в каждую из них записать адрес нужной
279
подпрограммы. Т.к. подпрограмм-членов такой записи может быть много, то логично написать дополнительную подпрограмму-настройщицу, которая бы выполняла эту работу. Естественно, она также должна стать членом записи.
Далее будем называть запись, содержащую подпрограммы, связанные с нею, чудо-записью.
На рис. 16.1 вы видите 2 переменные типа Чудо-Запись. В каждой из них 4 поля, 2 из которых – ссылки на некоторые функции (заранее заданной сигнатуры).
Согласно нашему построению, переменные, типом которых является чудозапись, могут менять свои функциональные свойства, т.к. чудо-запись содержит ссылки на подпрограммы, и при желании мы можем переставить их на другие подпрограммы
(см. рис 16.1).
|
ПодПр1 |
ЧудоЗапись1 |
ЧудоЗапись2 |
14 |
ПодПр2 |
132 |
|
2.25 |
34.99 |
||
|
ПодПр3
Рис 16.1 Чудо-записи, версия 1
Теперь договоримся, что набор подпрограмм у переменных, типом которых является чудо-запись, не будет изменяться на протяжении существования этих переменных. В таком случае можно будет сделать чудо-запись гораздо более компактной: для каждого типа «чудо-запись» сформировать где-то в памяти массив, который состоял бы из указателей на подпрограммы, а в самой чудо-записи будет храниться лишь указатель на этот массив. Создавать массивы для каждого типа «чудозапись» надо в начале программы. Таким образом, чудо-записи будут отличаться по от обычных записей лишь на размер одного указателя. Заметим, что каждая подпрограмма из массива должна принимать ссылку на переменную типа «чудо-запись» (кроме тех подпрограмм, которые не используют поля чудо-записи14).
На рисунке 16.2. вы видите 3 переменные (ЧЗ_1, ЧЗ_2, ЧЗ_3), типом которых является некоторая чудо-запись. При этом все ссылки на подпрограммы находятся в массиве ЧЗ (все ссылки уже указывают на подпрограммы). А у элементов типа чудозаписи хранится в одном из полей указатель на этот массив.
Массив с общими для переменных типа чудо-запись подпрограммами мы назовем дескриптором чудо-записи.
Значит, в принципе реализовать принцип инкапсуляции мы можем без особых трудностей на основе двух слоев указателей (если учесть, что и подпрограммы должны принимать указатели на записи, то – трех слоев). Другие принципы ООП, которые нам еще предстоит изучить, реализовать несколько сложнее.
14 В будущем мы назовем такие функции «методами класса»
280
ЧЗ
ПодПр3 |
|
ПодПр1 |
ПодПр4 |
|
ПодПр2 |
|
|
|
ЧЗ_1 |
ЧЗ_2 |
ЧЗ_3 |
234 |
453 |
176 |
342.65 |
32.45 |
4432 |
Рис 16.2 Чудо-записи, версия 2
16.3. Объявление класса. Способы доступа к элементам класса
Просто объединить данные и методы – уже хорошо, т.к. это позволяет мыслить в терминах классов и объектов. Для того чтобы сделать класс чем-то более целостным, можно ограничивать доступ к элементам класса. Это помогает обеспечить, например, непротиворечивость данных: доступ к полям будет обеспечиваться специальными функциями, которые будут проверять правильность введенных значений.
В Delphi есть 3 основных режима доступа к элементам класса – public (открытые), private (закрытые), protected (защищенные).
•private – элементы класса могут использоваться лишь внутри методов класса либо методов, которые расположены в том же модуле, где объявлено private-поле.
•public – элементы могут использоваться где угодно (так же, как и поля записи). Режим доступа protected мы обсудим позже, когда изучим наследование.
Метод класса стоит сделать закрытым, если он является лишь подметодом других методов класса и отдельно от них использоваться не должен.
Способ доступа контролируется компилятором, поэтому если вы попытаетесь присвоить значения закрытым полям какого-то объекта вне области видимости этого поля, то компилятор выдаст сообщение об ошибке.
Объявляется класс в Delphi следующим образом:
type
KlassesName = class private
…{описания private-полей и private-методов} public
…{описания public-полей и public-методов} end;
Вобъявлении:
KlassesName – название класса, который мы хотим создать;
class – зарезервированное слово, говорящее о том, что мы будем объявлять класс.