Энциклопедия Turbo Pascal. Главы 9-11
ОГЛАВЛЕНИЕ
Глава 9. Инструментарий баз данных Turbo Pascal
Мощным средством, существующим в Турбо Паскале, является ин струментарий баз данных. Он содержит процедуры, которые поддержи вают операции в базах данных типа В-дерева, сортировки и установ ку терминалов оконечных пользователей. Они называются Turbo Access, Turbosort и GINST. В данной главе рассматривается каждая из них и делается ударение на процедурах баз данных. Примеры, данные в этой главе, отражают инструментарий баз данных, предназначенный для использования с Турбо Паскалем версии 3. Инструментарий баз данных, предназначенный для Турбо Паскаля версии 4 и последующих, существенно отличается от предназначенных для более ранних версий. Наиболее важное отличие заключается в том, что процедуры инструментария версии 4 содержатся в блоках, которые связываются с вашей программой, как это необходимо, вмес то включения с использованием директивы компилятора SI, как в ин струментарии версии 3.
Turbo Access
Процедуры баз данных TURBO ACCESS реализуют полную файловую структуру типа В-дерева. В-дерево названо по имени его изобретателя R.Bayer. Оно отличается от обычного двоичного дерева тем, что каждый корневой узел может иметь более двух детей, как показано на рис.9-1. Из-за организации В-дерева файлы, размещающиеся на диске, могут быть найдены очень быстро. Хотя реализация В-дерева может быть довольно трудной, вам не надо понимать, как функции используют процедуры инструментария - фирма Borland сделала за вас всю трудную работу.
-----¬
/ -----L----- -----\
/ | \
-----¬ -----¬ -----¬
/ L----- L----- L----
/ | / \ | \
-----¬ -----¬ -----¬ -----¬ -----¬ -----¬
L----- L----- L----- L----- L----- L-----
Рис.9-1. Пример В-дерева
Необходимые файлы
Прежде, чем вы сможете использовать любую процедуру инструментария, вы должны иметь в наличие следующие файлы:
Имя файла Функция
ACCESS.BOX Основной файл данных и индексов для инициализации и установки
ADDKEY.BOX Добавляет ключи к индексным файлам
DELKEY.BOX Удаляет ключи из индексных файлов
GETKEY.BOX Находит ключи
Отметим, что инструментарий баз данных поставляется с двумя версиями базовых процедур ввода/вывода: одна для версии 2 Турбо Паскаля, другая для версии 3 Турбо Паскаля. Они называются ACCESS.BOX и ACCESS3.BOX, соответственно. Вы должны скопировать соответствующий файл в ACCESS.BOX для того, чтобы включить соответствующие процедуры, как показано в примерах в данной главе. С этого момента будем полагать, что процедуры ввода/вывода баз данных находятся в файле ACCESS.BOX.
В дополнение к этим файлам вам еще нужна программа SETCONST. PAS, которая прилагается к инструментарию для вычисления нескольких констант, необходимых для процедур В-дерева.
Файлы данных и индексные файлы
В Turbo Access файл данных используется для хранения записей, которые содержат запоминаемую вами информацию. Каждый файл данных, созданный и поддерживаемый Turbo Access, может содержать до 65 536 записей. Однако, в данное число входит и первая запись, которая зарезервирована для использования Turbo Access. Таким образом вы в действительности можете запомнить 65535 записей. Хотя длина каждой записи может быть до 65536 байт, это не рекомендуется. Наименьшая допустимая длина записи - 8 байт. Все переменные файлов данных в Turbo Access должны быть декларированы как тип DataFile, который задан в ACCESS.BOX.
Индексный файл содержит ключи, которые используются для идентификации специфических записей и номеров записей, связанных с ключем. Каждый индексный файл может содержать до 65535 ключей. Индексный файл обеспечивает быстрый способ определения местоположения специфических записей в файле данных. Важно понять, что все ключи должны быть строками. Все переменные индексных файлов в Turbo Access должны быть декларированы как IndexFile.
Константы В-дерева в Turbo Access
Шесть специальных констант должно быть задано любой программой, которая использует систему Turbo Access. Эти константы управляют такими спецификациями, как высота дерева и число узлов, подсоединенных к каждому корню. Имеются следующие константы:
MaxDataRecSize
MaxKeyLen
PageSize
Order
PageStackSize
MaxHeight
Значение данных констант лучше всего вычислять с помощью программы SETCONST.PAS. Используя данную программу вы должны ввести длину записи данных, которые вы будете запоминать, и длину ключа, используемого для индексирования каждой записи. Мы рассмотрим использование SETCONST.PAS далее в данной главе.
Зарезервированные имена переменных и коды ошибок
Turbo Access использует глобальные переменные, имена которых начинаются с буквы "ТА". Хотя вы также можете объявить переменные, которые начинаются с "ТА", но это не рекомендуется. Это может привести к сообщениям об ошибках во время компиляции, указывающим на дублирование имен идентификаторов. Если ошибка ввода/вывода происходит, когда вы используете процедуры Turbo
Access, отображается один из кодов, показанных в таблице 9-1.
Таблица 9-Коды ошибок ввода/вывода Turbo Access
--------------------------------------------------------------
Код Смысл
--------------------------------------------------------------
1 Неожидаемая длина записи
2 Несуществующий файл
3 Директория переполнена
4 Файл не открыт
5 Файл не открыт для ввода
6 Файл не открыт для вывода
7 Преждевременный EOF(конец файла)
8 Ошибка записи
9 Ошибка формата числа
10 Преждевременный конец файла
11 Размер файла превысил 65535 байт
12 Преждевременный конец файла
13 Ошибка записи
14 Попытка поиска за концом файла
15 Файл не открыт
16 Недопустимая операция
17 Недопустимое использование непосредственного режима
18 Недопустимое использование присваивания стандартным
файлам
144 Неожидаемая длина записи
145 Попытка поиска за концом файла
153 Преждевременный конец файла
240 Ошибка записи
243 Слишком много открытых файлов
---------------------------------------------------------------
Процедуры для работы с базами данных
AddRec
procedure AddRec(var dfile: DataFile, var RecNum,
var buffer);
Процедура AddRec добавляет записи, содержащиеся в buffer, к файлу данных dfile и возвращает номер записи, в которой осуществлено запоминание, в RecNum переменная buffer не имеет типа, но она должна содержать правильную запись. При успешном выполнении ОК содержит TRUE; в противном случае она равна FALSE. RecNum часто используется в последующем вызове AddKey.
ClearKey
Процедура ClearKey используется для сброса указателя индекса индексного файла в начало файла. Она объявляется следующим образом:
procedure ClearKey(var ifile: IndexFile);
CloseFile и CloseIndex
Данные процедуры используются для закрытия файла данных и индексного файла, соответственно. Они объявляются следующим образом:
procedure CloseFile(var dfile: DataFile);
procedure CloseIndex(var ifile: IndexFile);
Вы должны закрыть все файлы, которые были открыты процедурами Turbo Access. Если этого не делать, то могут быть потеряны данные или разрушен индексный файл.
DeleteKey
Данная процедура удаляет ключ из индексного файла. Она объявляется следующим образом:
procedure DeleteKey(var ifile: Indexfile, var RecNum: integer, var key);
Если в файле допускаются дублированные ключи, то RecNum должна содержать номер записи данных, связанной с ключем, который должен быть удален. Если операция завершилась успешно, то ОК равно TRUE, в противном случае - FALSE.
DeleteRec
Процедура DeleteRec удаляет указанную запись из файла данных. Она объявляется следующим образом:
procedure DeleteRec(var dfile. DataFile,var RecNum:integer);
Данная процедура осуществляет удаление записи с номером RecNum. Если операция выполняется успешно, ОК равно TRUE, в противном случае - FALSE. Уничтожаемая запись удаляется из файла логически, но она остается в файле физически и помещается в связанный список уничтоженных записей для возможного последующего использования. Важно, чтобы вы никогда не пытались уничтожить уже уничтоженную запись. Делая так, можно разрушить связанный список уничтоженных записей.
FileLen
Функция FileLen возвращает число записей в файле данных.
Она объявляется следующим образом:
function FileLen(var dfile: DataFile): integer;
Помните, что все файлы Turbo Access используют первую запись для внутренних целей так, что число записей, действительно содержащих ваши данные, меньше на 1 возвращаемого данной функцией.
FindKey
Данная процедура определяет местоположение заданного ключа в индексном файле и возвращает соответствующий номер записи файла данных. Она объявляется следующим образом:
procedure FindKey(var ifile: IndexFile,var RecNum:integer, var key);
Если key является ключем в заданном файле, то RecNum устанавливается на запись, связанную с ключем в файле данных. Если операция выполнилась успешно, то ОК устанавливается в значение TRUE, в противном случае - в FALSE.
GetRec
Процедура GetRee осуществляет считывание записи информации с заданным номером из файла данных. Она объявляется следующим образом:
procedure GetRec(var dfile: DataFile, var RecNum: integer, var buffer);
Запись с номером RecNum считывается в buffer, который должен по размеру соответствовать записи.
InitIndex
Данная процедура использует для инициализации таблицы, используемой Turbo Access для установки индексных файлов. Она не имеет параметров. Данная процедура должна вызываться однажды в начале вашей программы до вызова других процедур Turbo Access.
MakeFile MakeIndex
Данные процедуры используются для создания файлов данных и индексных файлов Turbo Access. Они объявляются следующим образом:
procedure MakeFile(var dfile: DataFile, fname: string[14], RecLen: integer);
procedure MakeIndex(var ifile: IndexFile, fname:string[14], KeyLen,DupKeys: integer);
Параметр RecLen в MakeFile должен содержать значение длины данных, которые будут запоминаться. Лучший способ сделать это - использование SizeOf вместо подсчета байт. Параметр KeyLen в MakeIndex должен содержать значение длины ключа. Если DopKey установлен в 0, то дублирование ключей не допускается, а если в 1, то допускается. Успешное выполнение операции устанавливает ОК в TRUE, в противном случае - в FALSE.
NextKey
Данная процедура возвращает соответствующий номер записи файла данных, связанный со следующим ключем в индексном файле. Она объявляется следующим образом:
procedure NextKey(var ifile: IndexFile, var RecNum: integer, var key);
После обращения к процедуре RecNum будет содержать номер записи в файле данных, соответствующей следующему ключу в индексном файле. Переменной key будет присвоено значение следующего ключа. При успешном обращении к NextKey ОК устанавливается в TRUE, в противном случае - в FALSE.
OpenFile и OpenIndex
Данные процедуры используются для открытия существующих файла данных и индексного файла, соответственно. Они объявляются следующим образом:
procedure OpenFile(var dfile: DataFile, fname: string[14], RecLen: integer);
procedure OpenIndex(var ifile:IndexFile,fname: string[14], KeyLen, DupKeys: integer);
Параметр RecLen в OpenFile должен быть установлен в значение длины данных, которые будут запоминаться. Лучший способ сделать это состоит в том, чтобы применить SizeOf вместо подсчета байт. Параметр RecLen в OpenIndex должен быть установлен в значение длины ключа. Если параметр DupKey установлен в 0, дублирование ключей не допускается, а если 1, то разрешается. При успешном выполнении данных процедур ОК устанавливается в TRUE, в противном случае - в FALSE.
PrevKey
Данная процедура используется для возврата номера записи в файле данных, связанной с предыдущим ключем в индексном файле. Она объявляется следующим образом:
procedure PrevKey(var ifile: IndexFile, var RecNum: integer, var key);
После обращения к процедуре параметр RecNum будет содержать номер записи в файле данных, соответствующей предыдущему ключу в индексном файле. Переменной key будет присвоено значение предыдущего ключа. При успешном выполнении данной процедуры ОК устанавливается в TRUE, в противном случае - в FALSE.
PutRec
Данная процедура помещает запись информации с заданным номером в файл данных. Она объявляется следующим образом:
procedure PutRec(var dfile: DataFile, var RecNum:integer, var buffer);
Информация из buffer помещается в запись с номером RecNum. Переменная buffer должна иметь размер соответствующий длине записи.
SearchKey
Данная процедура используется для поиска в индексном файле первого ключа, который не меньше ключа, заданного в вызове процедуры. Она объявляется следующим образом:
procedurte SearchKey(var ifile: IndexFile, var RecNum: integer, var key);
При успешном выполнении процедура возвращает номер записи в файле данных, связанной с ключем в RecNum и устанавливает ОК в TRUE. В противном случае ОК устанавливается в FALSE.
UsedRec
Данная процедура возвращает число записей в файле данных, содержащих истинные данные. Уничтоженные записи не подсчитываются. Процедура объявляется следующим образом:
function UsedRecs(var dfile: DataFile): integer;
Пример простого почтового списка
В качестве примера использования Turbo Access будет заново с применением его возможностей написана программа ведения простого почтового списка, разработанная в главе 2, которая использовала связанные ссылки. Во-первых, нам необходимо найти размер записи, используемой для хранения адресной информации, а затем вы примените программу SETCONST для вычисления соответствующих значений шести констант, требующихся для процедур Turbo Access.
Так как Turbo Access использует первые два байта в каждой записи в качестве флага индикации удаления, необходимо добавить поле целой переменной к исходной адресной записи. Кроме того, поля "предыдущий" и "следующий" больше не используются. Результирующая запись выглядит следующим образом:
type
address = record
status: integer; { используется Turbo Access }
name: string[30];
street: string[40];
city: string[20];
state: string[2];
zip: string[9];
end;
Длина в байтах поля adress, которая найдена, используя SizeOf, равна 108. Вам нужно будет данное число при выполнении программы SETCONST.PAS.
Программа SETCONST.PAS определяет значения констант, необходимых для Turbo Access. При запуске программы появляется экран, аналогичный показанному на рис.9-2, со значениями, принимаемыми по умолчанию. Руководство пользователя по инструментарию баз данных говорит, что для большинства применений вам необходимо изменить только длину записи и длину ключа, чтобы подстроиться под ваши особенности. Необходимо также изменить число записей, запоминаемых в базе данных, если оно превышает 10000. Размер страницы и размер стека страниц изменять не надо. Длина записи adress равна 108, так что сначала надо ввести это значение. В данном примере поле name будет использоваться в качестве ключа, поэтому значение 30 вводится как длина ключа. Клавишу RETURN нажать для оставшихся полей. Когда информация введена, SETCONT.PAS отображает экран, показанный на рис.9 -3.
При выходе из SETCONST.PAS вы можете автоматически создать соответствующую декларацию const. Декларация для программы
** 1 - рабочий лист задания констант Turbo Access, версия
1.10А *.
размер записи данных (байт) 200 20.
длина строки ключа (символов) 1.
размер базы данных (записей) 1000.
размер страницы (ключей) 2.
размер стека страниц (страниц) 1.
плотность (процент используемых элементов в средней странице)
50% 75% 100%
общее количество страниц индексного файл.
память, используемая для стека страниц (байт.
размер страницы индексного файла (байт.
размер индексного файла (байт.
размер файла данных (байт)
порядо.
максимальная высот.
среднее количество просмотров, необходимое для нахождения ключ.
среднее количество просмотров, удовлетворяемое стеком страни.
среднее количество просмотров на диске для нахождения ключа
нажмите ESC для завершения программы
Рис.9-2. Начальный экран SETCONST.PAS** Turbo Access constant determination worksheet, Version 1.10A
Data record size (bytes)
108 Key string Iength (characters)
30 Size of the database (records)
10000 Page size (keys)
24 Page stack size (pages)
10
Density (Percent of Items in use per average Page)50% 75% 100%
Total Index file pages
834 556 417 Memory used for page stack (bytes)
8430 8430 8430 Index file page size (bytes)
843 843 843 Index file size (bytes)
Data file size (bytes)
Order 12 12 12 MaxHeight
4 4 3 Average searches needed to find a key
3.71 3.19 2.90 Average searches satisfied by page stack
1.75 1.50 1.38 Average disk searches needed to find a key
1.96 1.69 1.52
ESC to end program
Рис.9-3. Экран SETCONST.PAS с вычисленными значениями: почтового списка показаны ниже. Комментарии даны автором.
Const
{данные константы сгенерированы программой SETCONST.PAS предоставляемой инструментарием баз данных.}MaxDataRecSize = 108;
MaxKeyLen = 30;
PageSize = 24;
Order = 12;
PageStackSize = 10;
MaxHeight = 4;
данные константы сгенерированы программой SETCONST.PAS, предоставляемой инструментарием баз данных.
С данной информацией и включенными необходимыми файлами Turbo Access первая часть программы почтового списка выглядит следующим образом:
program db_example;
Const
{данные константы сгенерированы программой SETCONST.PAS предоставляемой инструментарием баз данных.}MaxDataRecSize = 108;
MaxKeyLen = 30;
PageSize = 24;
Order = 12;
PageStackSize = 10;
MaxHeight = 4;
type
address = record
status: integer; {используется Turbo Access }
name: string[30];
street: string[40];
city: string[20];
state: string[2];
zip: string[9];
end;
{следующие файлы содержат процедуры баз данных}
{$i access.box} {основные процедуры баз данных}
{$i addkey.box} {добавить элементы }
{$i delkey.box} {удалить элементы }
{$i getkey.box} {поиск по дереву }
var
dbfile: DataFile;
ifile: IndexFile;
done: boolean;
В основном тексте программы, показанном далее, сначала инициализируется таблица индексов с помощью процедуры InitIndex. Далее либо открываются, либо создаются соответствующие файлы данных и индексный. Основной цикл программы аналогичен тому, который разработан в главе 2, и позволяет пользователю выбрать различные опции. При завершении файлы данных и индексный закрываются.
begin
InitIndex;
OpenFile(dbfile, 'mail.lst', SizeOf(address));
if not OK then
begin
WriteLn('creating new data file');
MakeFile(dbfile, 'mail.lst', SizeOf(address));
end;
OpenIndex(ifile, 'mail.ndx', 30, 0);
if not OK then
begin
WriteLn('creating new index file');
MakeIndex(ifile, 'mail.ndx', 30, 0);
end;
done:=false;
repeat
case MenuSelect of
'1': Enter;
'2': Remove;
'3': ListAll;
'4': Search;
'5': Update;
'6': done:=true;
end;
until done;
CloseFile(dbfile);
CloseIndex(ifile);
end.
Отметим, что больше не необходимо явно загружать и сохранять почтовый список: процедуры Turbo Access объявляют файлы автоматически.
Процедура Enter, показанная далее, вводит адресную информацию, запоминает ее в файле данных и помещает ключ в индексный файл. Отметим, что поле status каждой записи установлено в 0. По соглашению Turbo Access использует 0 для обозначения активной записи, а не нулевое значение для обозначения уничтоженных элементов.
temp: string[30];
info: address;
begin
done:=FALSE;
repeat
Write('Enter name:');
Read(info.name); WriteLn;
if Length(info.name)=0 then done:=TRUE
else
begin
Write('Enter street: ');
Read(info.street); WriteLn;
Write('Enter city: ');
Read(info.city); WriteLn;
Write('Enter state: ');
Read(info.state); WriteLn;
Write('Enter zip: ');
Read(info.zip); WriteLn;
info.status:=0; {сделать активной }
FindKey(ifile, recnum, info.name);
if not OK then {убедитесь, что нет дублированных
ключей }
begin
AddRec(dbfile, recnum, info);
AddKey(ifile, recnum, info.name);
end else WriteLn('Duplicate key ignored');
end;
until done;
end; {Enter}
Как вы видите, данная процедура осуществляет проверку на дублирование ключей. Так как дублированные имена не допускаются, процедура, во-первых, проверяет, соответствует ли новый ключ какому-либо уже существующему в файле. Если это так, то он игнорируется. Порядок вызова AddRec и AddKey критичен, так как переменная RecNum должна быть сначала установлена процедурой AddRec и затем запомнена процедурой AddKey (Помните, что номер записи файла данных связан с ключем в индексном файле и используется для последующего нахождения данных).
Процедура ListAll рассчитывает все содержимое почтового списка:
procedure ListAll;
{добавить адрес к списку}
procedure Enter;
var
done: boolean;
recnum: integer;
var
info: address;
len, recnum: integer;
begin
len: = filelen(dofile) -1;
for recnum:=1 to len do
begin
GetRec(dbfile, recnum, info);
{display if not deleter}
if info.status = 0 then display(info);
end;
end; {ListAll}
Процедура FileLen возвращает число записей активных или уничтоженных в файле данных, включая первую запись, которая зарезервирована для использования Turbo Access. Следовательно, действительное число пользовательских записей на единицу меньше. Кроме того, из-за того, что некоторые записи могут быть уничтожены, необходимо проверять поле status до отображения информации.
Поиск определенного адреса включает в себя поиск ключа в индексном файле с помощью процедуры FindKey. Когда ключ найден, возвращается соответствующий номер записи в файле данных и он используется процедурой GetRec для получения нужной информации. Процедура Search, показанная далее реализует данный подход:
{найти заданный элемент}
procedure Search;
var
name: string[30];
recnum: integer;
info: address;
begin
Write('Enter name: ');
ReadLn(name);
{найти ключ,если он существует}
FindKey(ifile, recnum, name);
if OK then { если найден }
begin
GetRec(dbfile, recnum, info);
{display if not deleter}
if info.status = 0 then Display(info);
end else WriteLn('not found');
end; {Search}
Наконец, модификация существующей записи предполагает, что вы должны сначала найти запись, считать ее, модифицировать и записать обратно в файл данных. Процедура Update иллюстрирует простой метод обновления, в котором пользователь должен ввести заново всю информацию. Более сложные подходы требуют перевода только изменяемых полей.
{ изменение адреса в списке, исключая поле имени }
procedure Update;
var
done: boolean;
recnum: integer;
temp: string[30];
info: address;
begin
Write('Введите имя: ');
Read(info.name); WriteLn;
FindKey(ifile, recnum, info.name);
if OK then
begin
Write('Введите улицу: )';
Read(info.street); WriteLn;
Write('Введите город: ');
Read(info.city); WriteLn;
Write('Введите штат: ');
Read(info.state); WriteLn;
Write('Введите индекс: ');
Read(info.zip); WriteLn;
info.status:=0; {сделать активной}
PutRec(dbfile, recnum, info);
end else WriteLn('ключ не найден');
end; {Update}
Программа, использующая Turbo Access для работы с файлами
Целиком программа, использующая Turbo Access для работы с файлами, выглядит следующим образом:
program db_example;
Const
{данные константы сгенерированы программой SETCONST.PAS.
предоставляемой инструментарием баз данных. }
MaxDataRecSize = 108;
MaxKeyLen = 30;
PageSize = 24;
Order = 12;
PageStackSize = 10;
MaxHeight = 4;
type
address = record
status: integer; {используется Turbo Access }
name: string[30];
street: string[40];
city: string[20];
state: string[2];
zip: string[9];
end;
{следующие файлы содержат процедуры баз данных}
{$i access.box} {основные процедуры баз данных}
{$i addkey.box} {добавить элементы }
{$i delkey.box} {удалить элементы }
{$i getkey.box} {поиск по дереву }
var
dbfile: DataFile;
ifile: IndexFile;
done: boolean;
function MenuSelect:char; {возврат пользовательского
выбора }
var
ch:char;
begin
WriteLn('1. ');
WriteLn('2. Удалить имя ');
WriteLn('3. Отобразить список');
WriteLn('4. Обновление ');
WriteLn('5. Поиск по имени ');
WriteLn('6. Выход ');
repeat
WriteLn;
Write('Введите ваш выбор:');
Read(ch); ch:=UpCase(ch); WriteLn;
until (ch>='1') and (ch<='6');
MenuSelect:=ch;
end; {MenuSelect}
{добавить адрес к списку}
procedure Enter;
var
done: boolean;
recnum: integer;
temp: string[30];
info: address;
begin
done:=FALSE;
repeat
Write('Введите имя: ');
Read(info.name); WriteLn;
if Length(info.name)=0 then done:=TRUE
else
begin
Write('Введите улицу: ');
Read(info.street); WriteLn;
Write('Введите город: ');
Read(info.city); WriteLn;
Write('Введите штат: ');
Read(info.state); WriteLn;
Write('Введите индекс: ');
Read(info.zip); WriteLn;
info.status:=0; {сделать активной}
FindKey(ifile, recnum, info.name);
if not OK then {убедитесь, что нет дублированных
ключей }
begin
AddRec(dbfile, recnum, info);
AddKey(ifile, recnum, info.name);
end else WriteLn('Дублированный ключ игнорирован');
end;
until done;
end; {Enter}
{ изменение адреса в списке, исключая поле имени }
procedure Update;
var
done: boolean;
recnum: integer;
temp: string[30];
info: address;
begin
Write('Введите имя: ');
Read(info.name); WriteLn;
FindKey(ifile, recnum, info.name);
if OK then
begin
Write('Введите улицу: ');
Read(info.street); WriteLn;
Write('Введите город: ');
Read(info.city); WriteLn;
Write('Введите штат: ');
Read(info.state); WriteLn;
Write('Введите индекс: ');
Read(info.zip); WriteLn;
info.status:=0; {сделать активной}
PutRec(dbfile, recnum, info);
end else WriteLn('ключ не найден');
end; {Update}
{удалить адрес из списка }
procedure Remove;
var
recnum: integer;
name: string[30];
info: address;
begin
Write('Введите имя для удаления : ');
Read(name); WriteLn;
FindKey(ifile, recnum, name);
if OK then
begin
DeleteRec(dbfile, recnum);
DeleteKey(ifile, recnum, name);
end else WriteLn('Не найдено');
end; {Remove}
procedure Display(info: address);
begin
WriteLn(info.name);
WriteLn(info.street);
WriteLn(info.city);
WriteLn(info.state);
WriteLn(info.zip); WriteLn;
end; {Display}
procedure ListAll;
var
info: address;
len, recnum: integer;
begin
len := fileLen(dbfile) -1;
for recnum:=1 to len do
begin
GetRec(dbfile, recnum, info);
{отобразить, если не уничтожен}
if info.status = 0 then display(info);
end;
end; {ListAll}
{Найти заданный элемент }
procedure Search;
var
name: string[30];
recnum: integer;
info: address;
begin
Write('Введите имя: ');
ReadLn(name);
{найти ключ, если существует}
FindKey(ifile, recnum, name);
if OK then
begin
GetRec(dbfile, recnum, info);
{отобразить, если не уничтожен}
if info.status = 0 then Display(info);
end else WriteLn('не найден');
end; {Search}
begin
InitIndex;
OpenFile(dbfile, 'mail.lst', SizeOf(address));
if not OK then
begin
WriteLn('Cоздание нового файла');
MakeFile(dbfile, 'mail.lst', SizeOf(address));
end;
OpenIndex(ifile, 'mail.ndx', 30, 0);
if not OK then
begin
WriteLn('Cоздание нового файла ');
MakeIndex(ifile, 'mail.ndx', 30, 0);
end;
done:=false;
repeat
case MenuSelect of
'1': Enter;
'2': Remove;
'3': ListAll;
'4': Search;
'5': Update;
'6': done:=true;
end;
until done;
CloseFile(dbfile);
CloseIndex(ifile);
end.
Пример программы инвентаризации
Для демонстрации того, как легко создать новые прикладные программы при наличии базового набора процедур, рассмотрим программу инвентаризации. Запись, используемая для хранения информации, выглядит следующим образом
type
inv = record
status: integer;
name: string[30];
descript := string[40];
guantity: integer;
cost: real;
end;
Длина ее, найденная с помощью SizeOf, равна 83. Используя данную длину и длину ключа, равную 30, программа SETCONST.PAS создает определение констант
Const
MaxDataRecSize = 82;
MaxKeyLen = 30;
PageSize = 24;
Order = 12;
PageStackSize = 10;
MaxHeight = 4;
Другие изменения, необходимые для преобразования процедур ведения почтового списка в процедуры инвентаризации, заключаются только в изменениях предложений печати. Целиком программа инвентаризации выглядит следующим образом:
program inventory;
Const
{ данные константы генерируются программой SETCONST.PA.
предоставляемой инструментарием баз данных }
MaxDataRecSize = 82;
MaxKeyLen = 30;
PageSize = 24;
Order = 12;
PageStackSize = 10;
MaxHeight = 4;
type
inv = record
status: integer;
name: string[30];
descript: string[40];
guantity: integer;
cost: real;
end;
{следующие файлы содержат процедуры баз данных}
{$i access.box} {основные процедуры баз данных}
{$i addkey.box} {добавить элементы }
{$i delkey.box} {удалить элементы }
{$i getkey.box} {поиск по дереву }
var
dbfile: DataFile;
ifile: IndexFile;
done: boolean;
function MenuSelect:char; {возврат пользовательского
выбора }
var
ch:char;
begin
WriteLn('1. Введите элемент ');
WriteLn('2. Удалить элемент ');
WriteLn('3. Отобразить инвентарный список');
WriteLn('4. Поиск элементов ');
WriteLn('5. Обновление ');
WriteLn('6. Выход ');
repeat
WriteLn;
Write('Введите ваш выбор: ');
Read(ch); ch:=UpCase(ch); WriteLn;
until (ch>='1') and (ch<='6');
MenuSelect:=ch;
end; {MenuSelect}
{добавить элемент к списку}
procedure Enter;
var
done: boolean;
recnum: integer;
temp: string[30];
info: inv;
begin
done:=FALSE;
repeat
Write('Введите имя элемента: ');
Read(info.name); WriteLn;
if Length(info.name)=0 then dont:=TRUE
else
begin
Write('Введите описание: ');
Read(info.descript); WriteLn;
Write('Введите количество: ');
Read(info.guantity); WriteLn;
Write('Введите стоимость: ');
Read(info.cost); WriteLn;
info.status:=0; { сделать активной }
FindKey(ifile, recnum, info.name);
if not OK then
begin
AddRec(dbfile, recnum, info);
AddKey(ifile, recnum, info.name};
end else WriteLn('дублированный ключ игнорирован');
end;
until done;
end; {Enter}
{изменение элемента в списке с сохранением поля имени}
procedure Update;
var
done: boolean;
recnum: integer;
temp: string[30];
info: inv;
begin
Write('Enter item name: ');
Read(info.name); WriteLn;
FindKey(ifile, recnum, info.name);
if OK then
begin
Write('Введите описание: ');
Read(info.descript); WriteLn;
Write('Введите количество: ');
Read(info.guantity); WriteLn;
Write('Введите стоимость: ');
Read(info.cost); WriteLn;
info.status:=0;
info.status:=0; {сделать активной}
PutRec(dbfile, recnum, info);
end else WriteLn('ключ не найден');
end; {Update}
{удалить элемент из инвентарного списка}
procedure Remove;
var
recnum: integer;
name: string[30];
begin
Write('Введите имя уничтожаемого элемента: ');
Read(name); WriteLn;
FindKey(ifile, recnum, name);
if OK then
begin
DeleteRec(dbfile, recnum);
DeleteKey(ifile, recnum, name);
end else WriteLn('Не найдено');
end; {Remove}
procedure Display(info: inv);
begin
WriteLn('Item name: ',info.name);
WriteLn('Description: ',info.descript);
WriteLn('Quantity on hand: ',info.quantity);
WriteLn('Initial cost: ',info.cost:10:2);
WriteLn;
end; {Display}
procedure ListAll;
var
info: inv;
len, recnum: integer;
begin
len := filelen(dbfile) -1;
for recnum:=1 to len do
begin
Getrec(dbfile, recnum, info);
if info.status = 0 then display(info);
end;
end; {ListAll}
{поиск элемента}
procedure Search;
var
name: string[30];
recnum: integer;
info: inv;
begin
Write('Введите имя элемента: ');
ReadLn(name);
{найти ключ, если он существует}
FindKey(ifile, recnum, name);
if OK then {если найден}
begin
GetRec(dbfile, recnum, info);
if info.status = 0 then Display(info);
end else WriteLn('не найден');
end; {Search}
begin
InitIndex;
OpenFile(dbfile, 'inv.lst', SizeOf(inv));
if not OK then
begin
WriteLn('Cоздание нового файла');
MakeFile(dbfile, 'inv.lst', SizeOf(inv));
end;
OpenIndex(ifile, 'inv.ndx', 30, 0);
if not OK then
begin
WriteLn('Cоздание нового файла');
MakeIndex(ifile, 'inv.ndx', 30, 0);
end;
done:=false;
repeat
case MenuSelect of
'1': Enter;
'2': Remove;
'3': ListAll;
'4': Search;
'5': Update;
'6': done:=true;
end;
until done;
CloseFile(dbfile);
CloseIndex(ifile);
end.
Программа ведения почтового списка и данная программа имеют один базовый скелет. Он может быть модифицирован для различных ситуаций использования баз данных.
TurboSort
Инструментарий баз данных предоставляет функцию TurboSort, которая является разновидностью QuickSort. Она может быть использована для сортировки любого типа данных, если их длина равна по меньшей мере двум байтам. QuickSort используется, так как это самая быстрая сортировка в большинстве ситуаций, как вы видели в Главе 1. TusboSort находится в файле SORT.BOX, который должен быть включен в каждую программу, использующую ее. TurboSort объявляется следующим образом
function TurboSort(ItemSize: integer): integer;
ItemSize - это размер данных, которые будут сортироваться; он должен быть вычислен с помощью SizeOf. Значение, возвращаемое TurboSort, интерпретируется, как показано в таблице 9-3. TurboSort может сортировать до 32767 элементов. Хотя функция будет пытаться сортировать их в оперативной памяти для скорости, она будет использовать временный файл на диске, когда это необходимо.
Таблица 9-3 Коды возврата TurboSort
-------------------------------------------------------------
Значение Смысл
-------------------------------------------------------------
0 Сортировка выполнена успешно
3 В компьютере нет достаточной памяти
8 Длина элемента меньше 2
9 Более 32767 входных элементов для сортировки
10 Ошибка записи на диск
11 Ошибка чтения с диска
12 Нет возможности создать временный файл на диске
--------------------------------------------------------------
InP, OutP и Less
TusboSort имеет три фазы работы: ввод данных, которые будут сортироваться, сортировка данных и вывод отсортированных данных. Для того, чтобы помочь TurboSort выполнить свою работу, вы должны создать три процедуры, называемые InP, OutP, Less. Данные процедуры декларируются как forward с помощью SORT.BOX, но вы должны создать действительную реализацию.
Процедура InP используется для передачи TurboSort данных, которые должны сортироваться, по одному элементу за раз. Действительная передача выполняется, используя SortRelease, которая также определена в SORT.BOX. Она объявляется следующим образом
procedure SortRelease(item);
Так как тип параметра item не задан, могут сортироваться данные любого типа. Простая процедура InP, которая считывает десять целых, введенных с клавиатуры, и передает их TurboSort для сортировки, показана ниже:
procedure InP;
var
f: integer;
begin
for i:=1 to 10 do ReadLn(data[i]);
for i:=1 to 10 do SortRelease(data[i]);
end; {InP}
Процедура OutP используется для поэлементного чтения отсортированных данных из TurboSort с помощью SortReturn, определенной в SORT.BOX. SortReturn объявляется следующим образом:
procedure SortReturn(item);
Так как тип параметра item не задан, то могут быть возвращены данные любого типа. Процедура OutP может не обладать информацией о том, сколько элементов данных будет возвращаться, поэтому функция SorEOS может быть использована для проверки на конец данных. Следующая простая процедура OutP может быть использована с целыми числами, генерируемыми процедурой InP, показанной ранее:
procedure OutP;
var
data: integer;
begin
repeat
SortReturn(data);
write(data,' ');
until SortEOS;
end; {OutP}
Функция Less является наиболее важной из трех рассматриваемых, так как она вызывается каждый раз, когда сравниваются два элемента. Функция Less возвращает TRUE, если первый аргумент меньше второго. Less декларирована в SORT.BOX, как имеющая два параметра, называемые Х и У, которые вы должны перекрыть с двумя логическими переменными, имеющими тот же самый тип, что и сортированные данные. Перекрытие реализуется с помощью команды alsolute. Например, данная версия Less может быть использована для сравнения двух целых переменных:
function Less;
var
first: char absolute X;
second: char absolute Y;
begin
Less := first < second;
end; {Less}
В качестве примера связи этих процедур рассмотрим короткую программу, которая считывает десять целых числе, сортирует их и отображает отсортированный список:
program simple_sort;
var
data: array [1..10] of integer;
result: integer;
{$i sort.box} {read in the sort routines}
procedure InP;
var
f: integer;
begin
for i:=1 to 10 do ReadLn(dara[i]);
for i:=1 to 10 do SortRelease(data[i]);
end; {InP}
function Less;
var
first: char absolute X;
second: char absolute Y;
begin
Less := first < second;
end; {Less}
procedure OutP;
var
data: integer;
begin
repeat
SortReturn(data);
write(data, ' ');
until SortEOS;
end; {OutP}
begin
result:=TurboSort(sizeOf(integer));
writeLn('sort result; ', result);
end.
GINST
Программа GINST, предоставляемая инструментарием баз данных, позволяет вам создать программу подготовки терминалов, которая дает возможность пользователям устанавливать свои программы на Турбо Паскале на своих компьютерах. Существует много типов компьютеров, терминалов и прочего; программа GINST позволяет вам создать программы на Турбо Паскале, которые могут быть установлены в различных условиях. Как таковая данная программа не будет рассматриваться. Однако, интересующиеся читатели могут обратиться к руководству пользователя инструментария баз данных.
Глава 10. Графический инструментарий Turbo Pascal
Графический инструментарий Турбо Паскаля является необязательным добавляемым для расширения Турбо Паскаля средством, которое представляет большой и разнообразный набор графических процедур. Эти процедуры делятся на пять основных категорий:
- базовые графические элементы точки, линии, квадраты, круги;
- тексты;
- окна;
- гистограммы;
- графики.
Количество различных процедур, функций и опций делает невозможным рассмотрение всего графического инструментария в данной главе (Руководство по графическому инструментарию имеет объем 256 страниц). Однако, будет дан обзор средств каждой категории с примерами.
Примеры, данные в этой главе, отражают графический инструментарий, предназначенный для версии 3 Турбо Паскаля. Графический инструментарий, предназначенный для версии 4 и последующих, значительно отличается от предназначенного для более ранних версий. Наиболее существенное отличие инструментария, предназначенного для версии 4, состоит в том, что процедуры содержаться в блоках, которые связываются с вашей программой, а не включаются в нее с помощью директивы компилятора SI, как в версии 3 инструментария.
Требования к аппаратному обеспечению
Для персонального компьютера IBM PC и совместимых с ним процедуры графического инструментария требуют, чтобы у вас в системе был один из следующих графических аппаратов:
- CGA (цветной графический адаптер);
- EGA (расширенный графический адаптер);
- монохромный графический адаптер Hercules. Вы можете настроить вашу копию графического инструментария в соответствии с инструкцией в руководстве пользователя графического инструментария для конкретного типа графического адаптера вашей системы. Кроме того, для функционирования инструментария в системе необходима память по крайней мере 192 Кбайта.
Системы координат
Все графические процедуры в инструментарии используют две отдельные координатные системы. Первая называется абсолютной координатной системой, а вторая - планетной координатной системой.
Абсолютная координатная система определяется используемым вами графическим адаптером. Она представляется числом пикселей в горизонтальном и вертикальном направлениях (Пиксель - это наименьшая адресуемая точка на экране). Например, CGA в режиме 6 имеет 640 пикселов в ширину и 200 в высоту. Процедуры инструментария используют координатную систему Х-Y, где ось Х представляет горизонтальное направление, а ось Y - вертикальное. По соглашению верхний левый угол экрана имеет координаты (0,0) и в случае контроллера CGA левый нижний угол имеет координаты (639,200). Хотя вы можете использовать абсолютную координатную систему, это делается редко из-за больших преимуществ планетной координатной системы.
Планетная координатная система задается процедурой DefineWold, которая используется для определения начальной и конечной точек координатной системы. Например, строка
DefineWord(1, 0, 0, 1000, 1000);
определяет координатную систему для планеты номер 1. Она делает 0,0 левым верхним углом и 1000,1000 правым нижним. Когда это сделано все графические процедуры в инструментарии будут настроены на координатную систему. Это позволяет создавать графические программы без учета того, какое графическое оборудование будет использоваться, таким образом давая большую независимость от аппаратуры. Та же самая программа может быть скомпилирована для работы либо с контроллером EGA в режиме 640х350, либо с CGA с разрешением 640х200 без изменений.
Для использования планетной координатной системы требуется трехступенчатый процесс. Во-первых, вы должны осуществить задание параметров планеты с помощью DefineWold. Во-вторых, вы должны выбрать одну из предварительно заданных планет с помощью SelectWold. Наконец, вы должны выбрать окно в планете с помощью SelectWindom. Процедура объявляется следующим образом:
procedure DefineWorld(WorlidNum:integer,StartX,StartY,EndX,EndY: real);
procedure SelectWorid(WoridNum: integer);
procedure SelectWindom(WindomNum: integer);
WorldNum - это номер планеты, а WindomNum - номер окна. StartX и StartY определяют координаты верхнего левого угла, а EndX и EndY - правого нижнего угла. Окно, которое вы выбрали, должно иметь тот же номер, что и номер планеты.
Следующий фрагмент кода определяет две планеты и выбирает первую из них в качестве текущей среды:
DefineWorld(1, 0, 0, 1000, 1000);
DefineWorld(2, 0, 0, 2000, 2000);
SelectWorld(1);
SelectWorld(2);
После данной последовательности процедуры инструментария будет работать в координатном пространстве 1000х1000.
Следующим преимуществом планетной координатной системы является то, что она позволяет увеличивать масштаб изображения. Это реализуется уменьшением координат планеты при неизменности всех других параметров. Примеры этого будут даны позднее.
Требования по инициализации
Файлы typed.sys, graphix.sys, kernel.sys должны быть включены в любую программу, которая использует графический инструментарий. Порядок включения важен и должен быть следующим:
{Si typedef.sys}
{Si graphix.sys}
{Si kernel.sys}
Прежде, чем можно будет использовать какую-либо процедуру инструментария, должна быть вызвана процедура InitGraphic для инициализации графической системы. После завершения использования графики вы должны вызвать процедуру LeaveGraphic для сброса экрана в принимаемый по умолчанию текстовый режим.
Базовые графические элементы
Графический инструментарий представляет процедуры реализации базовых графических элементов, которые на первый взгляд дублируют аналогичные процедуры, имеющиеся в Турбо Паскале. Однако, это не так. Процедуры инструментария могут работать в планетной координатной системе, тогда как соответствующие процедуры Турбо Паскаля не могут. Процедуры реализации базовых графических процедур описаны в таблице 10-1.
Таблица 10-Процедуры реализации базовых графических элементов
Имя Функция
DrawPoint Нарисовать точку в заданном месте
DrawLine Нарисовать линию в заданном месте
DrawSguare Нарисовать квадрат в заданном месте
DrawCircle Нарисовать круг в заданном месте
DrawCircleSegment Нарисовать круг в заданном месте
DrawCircleDirect Нарисовать круг, используя абсолютные
координаты
SetAspect Установить коэффициент взгляда для
процедур реализации круга
GetAspect Возврат к текущему коэффициенту взгляда
Следующая простая программа выбирает планету и окно и рисует круги, квадраты и линию. Результаты показаны на рис.10-1.
program simple_graphics;
{Si typedef.sys}
{Si graphix.sys}
{Si kernel.sys}
var
radius: real;
i:integer;
begin
InitGraphic;
DefineWorld(1, 0, 0, 1000, 1000);
SelectWorld(1);
SelectWindom(1);
DrawBorder; {установить границу вокруг окна }
SetAspect(1);
radius: = 0.05;
for i: = 1 to 10 do
begin
DrawCircle(500,500,radius);
radius: = radius + 0.2;
end;
repeat until KeyPressed;
ReadLn;
DrawSguare(100, 100, 900, 900, false);
DrawSguare(400, 400, 600, 600, false);
repeat until KeyPressed;
ReadLn;
DrawLine(0, 0, 1000, 1000);
repeat until KeyPressed;
LeaveGraphic;
end.
Процедура SetAspect используется для установки коэффициента взгляда, который определяет способ рисования круга. Любое значение, отличное от 1, будет порождать элипс вместо круга. Процедура DrawBorder помещает границу вокруг активного окна.
--------------------------------------------------¬
¦ --------------------------------------------¬ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ L-------------------------------------------- ¦
L-------------------------------------------------.
Рис.10-1. Круги, квадраты и линии в области 1000х1000
Чтобы понять влияние планетных координат, изменим предложение DefineWold и запустим программу снова. Окончательное отображение будет выглядеть, как показано на рис.10-2. Отметим, что не все круги влезли в область 2000х2000. Когда что-нибудь не влезает в область, процедуры инструментария автоматически "отстригают" его по краю.
---------------------------------------------------¬
¦ ---------------------¬ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ ¦ ¦ ¦
¦ L--------------------- ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
L--------------------------------------------------.
Рис.10-2. Круги, квадраты и линия в области 2000х2000
Графические текстовые процедуры
Графический инструментарий представляет два метода отображения текста на экране графического дисплея. Первый метод основывается на использовании стандартных процедур ввода/вывода Write и WriteLn, которые отображают машинно зависимый набор символов. Этот набор определяется аппаратным обеспечением компьютера и содержит типы символов, которые вы обычно используете. Однако, инструментарий также позволяет вам отображать машинно независимые символы, используя процедуры DrawText и DrawTextW, которые отображают буквы переменного размера на графическом экране или в окне. Машинно независимые процедуры наиболее интересны.
Машинно-независимый набор символов
Каждый машинно-независимый символ конструируется, используя матрицу 4х6 пикселов. Например, буква "Е" конструируется, как это показано на рис.10-3. Так как машинно-независимый набор символов создается с помощью графического инструментария, то можно изменять размер букв, используя масштабирующий коэффициент.
----------------------------------------
---T--T--T--¬
+--+--+--+--+
+--+--+--+--+
+--+--+--+--+
+--+--+--+--+
+--+--+--+--+
L--+--+--+--
---------------------------------------.
Рис.10-3. Построение буквы "Е" в матрице 4х6 пикселов
DrawText и DrawTextW
Процедуры DrawText и DrawTextW объявляются следующим образом:
procedure DrawText(X, Y, Scale: integer, Msg: WrkString);
procedure DrawTextW(X, Y, Scale: integer, Msg: WrkString);
Данные процедуры выводят сообщение Msg, начиная с координат X, Y в масштабе, заданном параметром Scale. WrkString декларируется инструментарием, как строка максимальной длины. Однако, вы можете использовать любой тип, который пожелаете.
Простая программа, показанная далее, отображает примеры первых шести размеров шрифта.
program text_graphics;
{Si typedef.sys}
{Si graphix.sys}
{Si kernel.sys}
var
i:integer;
begin
InitGraphic;
DefineWorld(1, 0, 0, 1000, 1000);
SelectWorld(1);
SelectWindom(1);
DrawBorder;
for i: = 1 to 6 do
begin
DrawTextW(10, i*140, i, 'Это тест')
end;
repeat until KeyPressed;
LeaveGraphic;
end.
Главным преимуществом применения машинно-независимых символов является то, что их размер может быть изменен для удовлетворения специфическим нуждам вашей программы.
Окна
Графический инструментарий позволяет вам создавать и обрабатывать одно или несколько окон. Каждое окно может быть связано со своей отдельной планетной координатной системой. Кроме того, окно может иметь заголовок и быть заключенным в границы. Две основные процедуры используются для работы с окнами: DefincWindow и SelectWindow. Они декларируются следующим образом:
procedure DefineWindow(WindomNum, X1, Y1, X2, Y2: integer);
procedure SelectWindow(WindomNum: integer);
WindoNum - это номер окна. В процедуре DefineWindow параметры X1, Y1 определяют местоположение верхнего левого угла, а X2, Y2 правого нижнего угла. Одним необычным аспектом процедуры DefineWindow является измерение координаты Х единицами, равными 8 пикселам. Следовательно, предложение
Defin_Window(1, 0, 0, 10, 10);
задает окно, которое имеет 10 пикселов высоты и 80 пикселов ширины (такой подход используется, так как все окна должны быть выравнены в памяти отображения на границу байта).
Для связи планеты с окном необходимо придерживаться следующей последовательности:
1. выбрать планету;
2. выбрать окно.
Для отображения сообщения в заголовке вы должны сначала связать заголовок с окном, а затем заголовок в соответствие "включено". Для реализации этого используйте процедуры DefineHeader и SetHeaderOn, которые декларируют следующим образом:
procedure DefineHeader(WindowNum: integer, Msg: WrkString);
procedure SetHeaderOn;
Вызов DrawBorder помещает границу вокруг активного окна. Данная процедура не имеет параметров.
Следующая программа иллюстрирует правильный порядок вызова различных процедур работы с окном для того, чтобы активизировать окно с границей и заголовком. Ее вывод показан на рис.10-5.
program One_Window;
{Si typedef.sys}
{Si graphix.sys}
{Si kernel.sys}
var
i:integer;
begin
InitGraphic;
DefineWorld(1, 0, 0, 1000, 1000);
DefineWindow(1, 20, 20, 40, 100);
DefineHeader(1, 'Заголовок');
SetHeaderOn;
SekectWorld(1);
SekectWindow(1);
DrawBorder;
repeat until KeyPressed;
LeaveGraphic;
end.
-------------------------------------------------
-----------------¬
L----------------
-----------------¬
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
L----------------
--------------------------------------------------.
Рис.10-5. Простое окно с границей и заголовком
Увеличение масштаба изображения
Созданием окон с различными координатами вы можете добиться эффекта увеличения масштаба графического отображения. Рассмотрим следующее предложение для рисования линии:
DrawLine(0, 0, 100, 100);
Если активная планета, в которой рисуется данная линия, задана следующим образом
DefineWorld(1, 0, 0, 100, 100);
то линия будет идти диагонально из одного угла в другой. Однако, если планета будет задана, как
DefineWorld(1, 0, 0, 200, 200);
то та же самая линия будет проходить половину дистанции из одного угла в другой. Графические объекты будут всегда заполнять экран в пропорции к системе координат планеты. В большей планете объект будет выглядеть меньше, а в меньшей будет выглядеть больше.
Следующая программа создает эффект увеличения масштаба изображения в углу квадрата. Вывод программы показан на рис.10-6.
------------------------------------------------------
--------------¬
L-------------
--------------¬
¦---¬ ¦
¦¦ ¦ +------------¬
¦L--- +------------
¦
+------------¬
¦ ¦ ------¬ ¦
¦ ¦ ¦ ¦ +------------¬
L-------------+ ¦ ¦ +------------
¦ L------ +------------¬
¦ ¦ ----------+
¦ ¦ ¦ ¦
L------------+ ¦ ¦
¦ ¦
¦ ¦ ¦
¦ ¦ ¦
L--+----------
Рис.10-6. Эффект увеличения масштаба изображения за счет использования окна
program Windows;
{Si typedef.sys}
{Si graphix.sys}
{Si kernel.sys}
{Si windows.sys}
var
i: integer;
procedure SetUpWindow;
begin
DefineWindow(1, 0, 0, 20, 100);
DefineHeader(1, 'Sguare');
DefineWorld(1, 0, 0, 400, 400);
SelectWorld(1);
SelectWindow(1);
SetHeaderOn;
SetBackground(0);
DrawBorder;
DefineWindow(2, 20, 40, 40, 140);
DefineHeader(2, 'Zoom in a little');
DefineWorld(2, 0, 0, 200, 200);
SelectWorld(2);
SekectWindow(2);
SetHeaderOn;
SetBackground(0);
DrawBorder;
DefineWindow(3, 40, 80, 60, 180);
DefineHeader(3, 'Ah...much clearer');
DefineWorld(3, 0, 0, 100, 100);
SekectWorld(3);
SelectWindow(3);
SetHeaderOn;
SetBackground(0);
DrawBorder;
end;
begin
InitGraphic;
SetUpWindow;
{появиться для увеличения в углу}
for i: = 1 to 3 do
begin
SelectWorld(1);
SelectWindow(1);
DrawSguare(10, 10, 120, 120, false);
DrawLine(10, 10, 20, 20);
end;
repeat until KeyPressed;
LeaveGraphic;
end.
Гистограммы и круговые диаграммы
Графический инструментарий предоставляет процедуры, позволяющие вам нарисовать гистограммы и круговые диаграммы. Существуют две процедуры для реализации круговых диаграмм: DrawPolarPie и DrawCartPie. Они отличаются способом задания местоположения и радиуса круговой диаграммы: в полярных или декартовых координатах. Легче пользоваться процедурой DrawPolarPie. Она задается следующим образом
procedure DrawPolarPie(X, Y, Radius, Theta, Inner, Outer:real; Info: PieArray; Num, Option,TextSize: integer);
X,Y - координаты центра, radius - радиус круговой диаграммы и Theta - угол (в градусах) первого сегмента. Параметр Inner и Outer определяют длину линии метки указателя. Info - это матрица типа PieArray, которая определяется следующим образом:
type
PieType = record
Area: real;
Text: wrkstring;
end;
PieArray = array[1..MaxPiesGlb] of PieType;
Параметр Num должен определять число сегментов, а Option устанавливается в соответствии со следующей таблицей:
Значение Option Смысл
0 Нет меток
1 Только текстовые метки
2 Метка из текста и значений
3 Метки только из значений
Наконец, параметр TextSize определяет размер машинно-независимых символов, используемых в метках. Программисты, которые используют процедуру DrawPolarPie, должны также включить следующие файлы в указанном порядке:
{Si circsegm.hgh}
{Si pie.hgh}
Для использования DrawPolarPie вы должны сначала загрузить 0 значения сегментов в Info.Area и метки, связанные с этими сегментами в Info.Text. Наконец, вы присваиваете соответствующие значения остальным параметрам и рисуете круговую диаграмму. Процедура, следующая далее, рисует программу, показанную на рис.10-7.
procedure PlotPie;
var
Radius, Theta, InRad, OuterRad: real;
Mode, Size: integer;
Products: PieArray;
begin
DefineWindow(1, 0, 0, 50, 100);
DefineHeader(1, 'Exports in Billions');
DefineWorld(1, 0, 0, 1000, 1000);
SelectWorld(1);
SelectWindow(1);
SetHeaderOn;
DrawBorder;
Products[1].Text: = 'Wheat S';
Products[2].Text: = 'Corn S';
Products[3].Text: = 'Manu. Goods S';
Products[4].Text: = 'Electronics S';
Products[5].Text: = 'Misc. S';
Products[1].Area: = 15;
Products[2].Area: = 12.4;
Products[3].Area: = 7.34;
Products[4].Area: = -24; {вытолкнуть}
Products[5].Area: = 16;
Radius: = 125;
Theta: = 60;
SetAspect(1.0);
InRad: = 0.85;
DuterRad: = 1.5;
Mode: = 2; {установить рисование обеих меток}
Size: = 1; {установить малый размер текста}
DrawPolarPie(500, 500, Radius, Theta, InRad,
OuterRad, Products, 5, Mode, Size);
end; {PlotPie}
Для того, чтобы нарисовать гистограмму, требуется использовать процедуру DrawHistogram, которая декларируется следующим образом:
procedure DrawHictogram(Info: PlotArray; Num: integer;
Hatch: Boolean; HatchStyle:integer);
Параметр Info содержит значение, связанное с каждой полосой.
------------------------------------------------------
----------------------------¬
L---------------------------
----------------------------¬
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
L---------------------------
--------------------¬
L-------------------
--------------------¬
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
¦ ¦
L--------------------
Рис.10-7. Круговая диаграмма и гистограмма
Это двумерная матрица, где для любого Info[i,1] зарезервирован для внутреннего использования, а Info[i,2] должен содержать значение полосы для i. Если параметр Hatch равен TRUE, то каждая полоса заштриховывается. Параметр HatchStyle определяет частоту штрихования; значение 1 задает самую частую штриховку. Нельзя рисовать метки непосредственно используя процедуру DrawHistogram.
Вы должны включить следующие файлы в порядке, показанном далее, в любую программу, которая использует DrawHistogram:
{Si hatch.hgh}
{Si histogrm.hgh}
Процедура, показанная далее, иллюстрирует использование DrawHistogram. Она порождает гистограмму, показанную на рис.10-7.
procedure PlotBar;
var
Products: PlotArray;
begin
DefineWindow(2, 40, 110, 70, 180);
DefineHeader(2, 'Exports in Billions');
DefineWorld(2, 0, 0, 30, 35);
SelectWorld(2);
SelectWindow(2);
SetHeaderOn;
SetBackground(0);
DrawBorder;
Products[1,2]: = 15;
Products[2,2]: = 12.4;
Products[3,2]: = 7.34;
Products[4,2]: = 24;
Products[5,2]: = 16;
DrawHistogram(Products, 5, true, 5);
DrawTextW(1, 2, 1, 'Wheat Corn Manu. Elec. Misc.');
DrawTextW(1, 7, 1, 'S15 S12.4 S7.34 S24 S16');
end; {PlotBar}
Отметим, что метки естественным образом включаются с помощью процедуры DrawTextW.
Для вашего удобства гистограмма и круговая диаграмма показаны далее в одной программе:
program Presentation_graphics;
{Si typedef.sys}
{Si graphix.sys}
{Si kernel.sys}
{Si circsegm.hgh}
{Si pie.hgh}
{Si hatch.hgh}
{Si histogrm.hgh}
procedure PlotPie;
var
Radius, Theta, InRad, OuterRad: real;
Mode, Size: integer;
Products: PieArray;
begin
ClearScreen;
SetColorWhite;
DefineWindom(1, 0, 0, 50, 100);
DefineHeader(1, 'Exports in Billions');
DefineWorld(1, 0, 0, 1000, 1000);
SelectWorld(1);
SelectWindom(1);
SetHeaderOn;
SetBackground(0);
DrawBorder;
Products[1].Text:='Wheat S';
Products[2].Text:='Corn S';
Products[3].Text:='Manu. Goods S';
Products[4].Text:='Electronics S';
Products[5].Text:='Mics. S';
Products[1].Area:=15;
Products[2].Area:=12.4;
Products[3].Area:=7.34;
Products[4].Area:=-24; {вытолкнуть}
Products[5].Area:=16;
Radius: = 125;
Theta: = 60;
SetAspect(1.0);
InRad: = 0.85;
OuterRad: = 1.5;
Mode:=2; {установить рисование обеих меток}
Size:=1; { установить малый размер текста }
DrawPolarPie(500, 500, Radius, Thete, InRad,
OuterRad,Products, 5,Mode, Size);
end; {PlotPie}
procedure PlotBar;
var
Products: PlotArray;
begin
DefineWindow(2, 40, 110, 70, 180);
DefineHeader(2, 'Exports in Billions');
DefineWorld(2, 0, 0, 30, 35); 1
SelectWorld(2);
SelectWindow(2);
SetHeaderOn;
SetBackground(0);
DrawBorder;
Products[1,2]: = 15;
Products[2,2]: = 12.4;
Products[3,2]: = 7.34;
Products[4,2]: = 24;
Products[5,2]: = 16;
DrawHistogram(Products, 5, true, 5);
DrawTextW(1, 2, 1, 'Wheat Corn Manu. Elet. Mias.');
DarwTextW(1, 7, 1, S15 S12.4 S7.34 S24 S16');
end; {PlotBar}
begin
InitGraphic;
PlotPie;
PlotBar;
repeat untei KeyPressed;
LeaveGraphic;
end.
Рисование графиков
Графический инструментарий содержит прекрасный набор процедур рисования графиков и сглаживания. Далее будут рассмотрены две процедуры DrawPoly и Spline.
Процедура DrawPoly используется для рисования на экране любой произвольной кривой по ее конечным точкам. Она объявляется следующим образом:
procedure DrawPole(Info: PlotArray; Start, End, Code, Scale, Line: integer);
Info - это матрица рисования, которая содержит координаты каждой рисуемой точки. Start и End - это матрица индексов первой и последней точки. Параметр Code определяет символ, который будет использоваться для рисования точки. Он имеет следующие значения:
Код Смысл
0 Используется линия между точками
1 +
2 х
Так как точки генерируются с помощью генератора случайных чисел Random, каждый прогон процедуры порождает особую кривую.
Глава 11. Эффективность, мобильность и отладка
Способность писать программы, которые эффективно используют системные ресурсы, переносимы на другие компьютеры и свободны от ошибок - это признак профессионального программиста. В данной главе рассматриваются некоторые методы, с помощью которых достигается эффективность, мобильность и отладка программ.
Эффективность
В применении к программам термин эффективность относится либо к использованию ресурсов системы, либо скорости выполнения, либо к тому и другому. К ресурсам системы относится оперативная память, дисковое пространство, устройства, то есть то, что может выделяться и использоваться. Суждение о том, является программа эффективной или нет, субъективно, оно зависит от ситуации. Рассмотрим программу, которая при выполнении использует 147 Кбайт оперативной памяти, 2 Мбайта дискового пространства и затрачивает в среднем 70 минут. Если это короткая программа, выполняющаяся на персональном компьютере Apple 2, то по всей видимости не очень эффективна. Однако, если это программа, выполняющаяся на суперкомпьютере Cray, то вероятно, она эффективна.
С другой стороны, когда вы добиваетесь эффективности по какому-либо одному параметру, часто при этом ухудшаются другие показатели. Например, достижение более быстрого выполнения программы ведет к ее увеличению, если вы используете линейную программу вместо вызовов функций для увеличения скорости. Кроме того, достижение более эффективного использования дискового пространства за счет упаковки данных замедляет доступ к диску.
В свете этих проблем вы можете поставить вопрос. а как эффективность вообще может обсуждаться. В действительности существует несколько методик программирования, которые всегда дают эффективные программы или по крайней мере более эффективные, чем в других случаях. Существует также ряд методов, которые делают программу как быстрее, так и меньше.
Предотвращение дублировния кода
Даже самые лучшие программисты пишут иногда избыточные коды. Избыточный код не ссылается на код, который может быть выделен в подпрограмму; даже не очень опытные программисты понимают это. Кроме того, к избыточности относится и не необходимое дублирование аналогичных предложений внутри процедуры. Чтобы получить лучшее представление о том, что такое избыточный код, рассмотрим следующий фрагмент:
Read(a);
Read(y);
if a<10 then WriteLn('Недопустимый ввод');
if Length(y)=0 then WriteLn('Недопустимый ввод');
В данном случае предложение WriteLn('Недопустимый ввод') встречается дважды. Однако, это не необходимо, так как фрагмент может быть переписан следующим образом
Read(a);
Read(y);
if (a<10 or (Length(y)=0) then WriteLn('Недопустимый ввод');
В таком варианте код не только короче, но и будет в действительности выполняться быстрее, так как выполняется только одно предложение if/then вместо двух.
Данный пример возможно не встретится в реальной программе, так как избыточные предложения являются соседними и их легко обнаружить. Однако, так как избыточные предложения часто разнесены в программе, то такие коды встречаются в большинстве программ.
Избыточность иногда является следствием методов, выбранных для кодирования процедур. Например, далее представлены два метода кодирования функции, которая осуществляет поиск заданного слова в матрице строк:
type
str80 = string[80];
StrArray = array [1..100] of str80;
function StrSearch1(str: StrArray; word: str80): boolean;
{ правильный, неизбыточный код }
var
t: integer;
begin
StrSearch1 := FALSE;
for t := 1 to 100 do
if str[t]=word then StrSearch1 := TRUE;
end;
Function StrSeach2(str: StrArray; word: str80): boolean;
{неправильный, избыточный код }
var
t: integer;
begin
t :=1;
StrSearch := FALSE;
if str[t]=word then StrSearch2 := TRUE
else
begin
t := 2;
while(t<=100) do
begin
if str[t]=word then StrSearch2 := TRUE;
t := t+1;
end;
end;
end;
При втором методе не только дублируются предложения сравнения if/then, но также имеются два предложения присваивания (t:=1 и t:=2). Первая версия работает быстрее и требует значительно меньше памяти.
Коротко говоря, избыточный код может быть следствием либо неряшливости при программировании, либо неверного выбора метода реализации процедуры.
Использование процедур и функций
Всегда помните, что использование процедур и функций с локальными переменными составляет основу структурного программирования. Процедуры и функции являются строительными блоками программ на Турбо Паскале и составляют самое главное положительное качество данного языка. Вам следует знать несколько особенностей функций Турбо Паскаля, которые влияют на размер и скорость выполнения вашего кода.
Во-первых, Турбо Паскаль является стеково-ориентированным языком: все локальные переменные и параметры используют стек для промежуточного запоминания. При вызове функции адрес возврата вызвавшей процедуры также помещается в стек. Это позволяет программе осуществить возврат в точку, из которой был вызов. Когда функция возвращает управление, данный адрес и все локальные переменные и параметры должны быть удалены из стека. Процесс заталкивания данной информации в стек называется последовательностью вызова, а процесс выталкивания информации из стека последовательностью возврата. Эти последовательности требуют определенного времени и иногда довольно большого.
Чтобы понять, как вызов функции может замедлить вашу программу, рассмотрим два примера:
Версия 1 Версия 2
for x:=1 to 100 do for x:=1 to 100
t:=compute(x); t:=Abs(Sin(q)/100/3.1416);
function compute(q: integer): real;
var
t:real;
begin
compute:=Abs(Sin(q)/100/3.1416);
end;
Хотя каждый цикл выполняет одну и ту же функцию, версия 2 гораздо быстрее, так как использование непосредственного кода устраняет задержки, связанные с последовательностями вызова и возврата. Для понимания того, сколько времени тратится, рассмотрим следующий код на псевдоассемблере, который демонстрирует в теории последовательности вызова и возврата для функции compute:
; последовательность вызова
move A, x ; поместить значение х в аккумулятор
push A
call compute ; инструкция вызова помещает адрес
; возврата в стек
; последовательность возврата
; значение возврата функции должно быть помещено в
; регистр - мы используем В
move B, stack-1 ; доставить значение во временное t
return ; возврат к вызвавшей процедуре
; вызвавшая процедура затем выполняет следующие действия
Использование функции compute внутри цикла ведет к тому, что последовательности вызова и возврата будут выполнены 100 раз. Если вы хотите написать быстрый код, то использование данной функции внутри цикла - это не самый лучший подход.
Теперь вы можете подумать, что следует писать программы, которые состоят из нескольких больших процедур и которые, следовательно, будут работать быстрее. В большинстве случаев, однако, небольшие различия во времени выполнения не важны, а вот потеря структуры будет ощутимой. Но это другая проблема. Замена вызовов подпрограмм, которые используются различными процедурами, на непосредстевнные коды сделает вашу программу очень большой, так как один и тот же код будет дублироваться большее количество раз. Помните, что подпрограммы были выдуманы главным образом как средство повышения эффективности использования памяти. Положение таково, что убыстрение программы ведет к ее увеличению, а уменьшение программы ведет к ее замедлению. Использовать непосредственный код вместо вызовов функций следует только тогда, когда скорость имеет абсолютный приоритет. В противном случае рекомендуется повсеместное применение процедур и функций.
Предложение Case против цепочки if/then/else
Следующие фрагменты кодов функционально эквивалентны. Однако, один является более эффективным, чем другой. Можете ли вы сказать какой?
case ch of if ch='a' then f1(ch)
'a': f1(ch); else if ch='b' then f2(ch)
'b': f2(ch); else if ch='c' then f3(ch)
'c': f3(ch); else if ch='d' then f4(ch)
'd': f4(ch);
end;
Левый фрагмент кода гораздо более эффективен, чем первый, так как в общем случае предложение case порождает более компактный и быстрый объектный код, нежели серия предложений if/then/else.
Цепочка if/then/else важна, так как она позволяет вам выполнить переходы по множеству ветвей с анализом данных различных типов, что не может быть сделано с помощью предложения case. Однако, если вы используете скалярные данные целые, действтительные числа, символьные данные и перечисления, то следует применять предложение case.
Мобильность программ
Довольно частое явление - это перенос программы, написанной на одной машине, на другую, которая отличается процессором, операционной системой или тем и другим. Данный процесс может быть и очень простым и крайне трудным в зависимости от того, как была написана программа. Программа мобильна, если она может быть легко перенесена. Программа переносится трудно, если она содержит много машинно-зависимых вещей: фрагментов кодов, которые работают только со специфической операционной системой или процессором. Турбо Паскаль допускает перенесение кода между всеми его версиями,но все *таки требует внимания к деталям и часто при этом теряется эффективность из-за отличий в операционных системах.
Перенесение кода, написанного с использованием какого-либо компилятора Паскаля, в Турбо Паскаль может вызвать проблемы из-за применений различных наборов расширений. Обратная задача также проблематична: если были использованы расширения Турбо Паскаля, написанный код должен быть модифицирован при использовании другого компилятора.
В данном разделе рассматривается несколько специфических областей и даются некоторые решения. Вы также увидите, как писать программы на языке Паскаль, чтобы они были мобильными.
Применение констант
Возможно самый простой способ сделать программу мобильной состоит в том, чтобы ввести каждое системно- и процессорно-зависимое "магическое" число в декларацию констант. У данным "магическим числам" относятся размер записи прямого доступа, специальные команды экрана и клавиатуры, информация по распределению памяти и другие данные, которые могут сильно измениться при перенесении программ. Если вы выделите эти числа в декларацию констант они станут понятными для человека, осуществляющего перенесение, и облегчит их редактирование.
Например, далее приводятся две декларации матриц и две процедуры, которые обращаются к ним. В первом варианте размерности матриц являются жестко заданными, а во втором _ помещаются в декларацию const.
{первая версия}
var
count: array[1..100] of integer;
procedure f1;
var
t: integer;
begin
for t := 1 to 100 do count[t] := t;
end;
{вторая версия}
const
MAX = 100;
var
count: array[1..MAX] of integer;
procedure f2;
var
t: integer;
begin
for t := 1 to MAX do coun[t] := t;
end;
Вторая версия лучше в том случае, если вы хотите переносить данную программу на машину, которая, например, допускает больший размер матрицы. В данном случае необходимо изменить только МАХ и все ссылки на МАХ будут автоматически скорректированы. Данную версию не только легко модифицировать, но она также позволяет избежать большого количества ошибок редактирования. Помните, что в реальной программе будет возможно много ссылок на МАХ, поэтому выигрыш в мобильности часто вполне весомый.
Зависимость от операционной системы
Практически все коммерческие программы имеют коды, которые являются специфическими по отношению к операционной системе. Например, программа может использовать экранную память персонального компьютера IBM PC для быстрого переключения экрана или специальные графические команды, применимые в данной операционной системе. Некоторые особенности, связанные с операционной системой, необходимы для быстрых коммерческих программ. Однако, нет причин делать код более зависимым, чем это необходимо.
Когда вы должны использовать системные вызовы для доступа к операционной системе, лучше сделать это в одной главной процедуре так, чтобы только ее надо было изменять при переводе в другую операционную систему, оставив все остальные без изменения. Например, если системный вывод необходим для очистки экрана и помещения курсора в точку с координатами X,Y, то вы могли бы создать главную процедуру, аналогичную OpSysCall, показанной далее:
{интерфейс с операционной системой}
procedure OpSysCall(op, x, y: integer);
begin
case op if
1: ClearScreen;
2: ClearEOL;
3: CotoXY(x,y);
end;
Хотя эти вызовы стандартны для всех версий Турбо Паскаля, если вы будете переносить программу на другой компилятор Паскаля, то вы поймете ценность такого метода взаимодействия. Должен быть изменен только код, который формирует действительные функции, зависимые от операционной системы, оставляя нетронутым общий интерфейс.
Расширение Турбо Паскаля
Если вы осуществляете перенос из Паскаля в Турбо Паскаль, расширения и улучшения Турбо Паскаля могут сделать работу легче и даже повысить эффективность программы. Например, расширение string значительно облегчает работу со строками.
Однако, если вы переносите код из Турбо Паскаля в другой Паскаль, то вы почти наверняка должны удалить все расширения и улучшения из вашей программы. Наихудшей задачей будет замена всех переменных типа string на матрицы символов. Но это только первый шаг, вы также потеряете все процедуры, которые поддерживают строки, такие как Copy, Concat и Pos. Если ваша программа использует такие процедуры, то в новой версии вы должны создать их сами.
В общем случае, если вы до написания программы знаете, что будете переносить ее в другой Паскаль, то следует избегать применения расширений и улучшений Турбо Паскаля.
Отладка
Перефразируя Томаса Эдисона можно сказать, что программирование - это на 10% вдохновение и на 90% отладка. Хорошие программисты являются хорошими отладчиками. Если вы и имеете большой опыт отладки, вам надо следить за различными типами мелочей, которые имеют место при использовании Турбо Паскаля.
Проблема указателей
Наиболее часто ошибки в Турбо Паскале связаны с неправильным употреблением указателей. Проблемы указателей делятся на две основные категории: недоразумения с действиями над указателями и случайное применение недопустимых указателей. Для разрешения проблем первого типа вы должны разобраться с указателями в языке Паскаль; для разрешения проблем второго типа вы должны всегда проверять допустимость указателей перед их применением.
Следующая программа иллюстрирует типичную ошибку с указателем, которую допустил программист:
program WRONL; {данная программа имеет ошибку}
type
pntr = ^obiect;
obiect = record
x: char;
y: integer;
name:string[80];
end;
var
p: pntr;
begin
p^.name := 'tom';
p^.x := 'g';
p^.y := 100;
end.
Данная программа может аварийно завершиться, так как указатель р не принимает значение, используя New. Указатель р содержит неизвестное произвольное число, которое может указывать куда угодно в памяти компьютера. Это совсем не то, что вы хотели. Для исправления этой программы вы должны добавить строку
New(p);
до первого использования р.
"Дикий" указатель крайне трудно выследить. Если вы осуществляете присваивание указателю значение переменной, которая не содержит недопустимый адрес, ваша программа некоторое время может работать правильно, а крах наступит после. Статистически, чем меньше ваша программа, тем больше вероятность того, что она будет работать правильно даже со сбившимся указателем, так как используется меньше памяти. По мере роста вашей програмы ошибка становится более вероятной.
Во-вторых, наиболее коварные проблемы могут возникнуть, когда используются указатели: вы можете выйти за пределы памяти в течение вызова New во время выполнения. Это вызывает ошибку во время выполнения и оно прекращается. К счастью в Турбо Паскале есть специальная встроенная функция MemAvail так, что вы можете избежать этих проблем. MemAvail возвращает число свободных байт слева от неупорядоченного массива. В версиях, предшествующих 4.0, MemAvail возвращает число параграфов свободной памяти. Следовательно, чтобы сделать программу полностью правильной, проверяйте наличие свободной памяти перед ее выполнением. Чтобы сделать это, вы должны знать число байт, необходимое для каждого типа данных, которые вы размещаете. Однако, это число может изменяться в зависимости от процессора и операционной системы, поэтому используйте функцию SizeOf, которая возвращает число байт, требующихся для переменной. Измененная программа выглядит следующим образом:
program RIOHL; {это верная программа}
type
pntr = ^object;
object = record
x: char;
y: integer;
name:string[80];
end;
var
p: pntr;
begin
if MaxAvail>=SizeOf(object) then
begin {свободная память}
New(p);
p^.name := 'tom';
p^.x := 'g';
p^y := 100;
end;
end.
Одним из указаний на проблему с указателем является то, что ошибка ведет к неустойчивости в поведении. Ваша программа может работать один раз правильно, а другой раз неправильно. Иногда другие переменные будут содержать мусор без видимых причин. Если такое происходит, проверьте ваши указатели.
Хотя указатели являются вещью хлопотной, они также являются одним из самых мощных и полезных аспектов языка Паскаль. Приложите усилия заранее, чтобы научиться правильно ими пользоваться.
Переопределение встроенных процедур и функций
Хотя Турбо Паскаль не допускает переопределения ключевых слов, которые входят в язык, он позволяет вам переопределять слова, которые отсылают к стандартным процедурам и функциям. Программисты иногда думают, что это интересно по многим причинам; однако, это может привести только к неприятностям. Далее дается пример проблемы, вызванной переопределением встроенной процедуры:
program WRONG; {данная программа не вернa}
var
t: integer;
units_processed: integer;
procedure WriteLn(t: integer);
begin
write(t, ' ', 'байт свободной памяти в неупорядоченном
массиве');
end;
begin
{вычислить объем свободной памяти в неупорядоченном
массиве}
WriteLn(NemAvail);
.
.
.
WriteLn(units_processed);
end.
Как показано в данном примере, программист переопределяет стандартную процедуру WriteLn, но забыв, что программа также использовала данную процедуру для печати числа обработанных элементов. Вместо этого сообщение о свободной памяти будет отображаться дважды, так как переопределение WriteLn заменено встроенным определением.
Хотя в предыдущем примере заметить ошибку легко, самые главные проблемы возникают тогда, когда встроенные функции и процедуры переопределены, но не используются непосредственно в программе в данном месте. Позже, когда программа будет модифицирована, перераспределенная функция ил процедура рассматривается, как если бы она была встроенной, как показано в предыдущем примере:
{проверить окно, оставшееся в глобальной матрице count }
function MemAvail: boolean;
var
t: integer;
begin
MemAvail := FALSE;
for t := 1 to MAX do if count[t]=0 then MemAvail:=TRUE;
end;
Переопределение MemAvail хорошо, пока выполняется данный пример. Проблема возникает, если матрица count превращается позднее из глобальной переменной в динамическую переменную, которая выделяется из неупорядоченного массива. В данном случае, если вы пытаетесь использовать MemAvail для просмотра того, осталось ли достаточно свободного пространства, ваша программа выйдет из строя.
Единственный способ избежать таких проблем заключается в том, чтобы никогда не иметь процедур и функций, написанных вами, с именами как у встроенных функций. Если вы не уверены, присоедините ваши инициалы в начало имени, например, HSMemAvail вместо MemAvail.
Неожиданные синтаксические ошибки
Иногда вы будете встречаться с синтаксическими ошибками, которые трудно понять или даже распознать, как ошибки. В частности, необнаруживаемая ошибка произойдет, когда вы пытаетесь скомпилировать данный код:
program Errors; {данная программа не компилируется}
var
s: string[80];
procedure F1(x: string[80]);
begin
WriteLn(s);
end;
begin
ReadLn(s);
F1(s);
end.
Если вы попытаетесь скомпилировать эту программу, то увидите следующее сообщение об ошибке:
Error 89: ")" expected
/ошибка 89: ожидалась ")"/
После нажатия клавиши ESC, вы обнаружите, что Турбо Паскаль указывает на строку
procedure F1(x:string[80]);
с курсором в позиции, показанной стрелкой. Это недостаток Турбо Паскаля? Нет. Турбо Паскаль не может использовать тип string в вызовах процедур и функций. Вы должны явно декларировать свой тип и применить его. В данном примере вы, во-первых, декларируете тип, называемый str80 с помощью следующего предложения:
type
str80 = string[80];
Затем вы используете заново созданный тип str80, как тип параметра для функции F1. Правильная программа выглядит следующим образом:
program CorrectecError; {данная программа будет
скомпилирована}
type
str80 = string[80];
var
s := str80;
procedure F1(x: str80);
begin
WriteLn(x);
end;
begin
ReadLn(s);
F1(s);
end.
Другая, вносящая путаницу, синтаксическая ошибка порождается следующей программой:
program Error; {данная программа не будет скомпилирована}
procedure F2;
var
t: integer;
begin
for t := 1 to 10 do WriteLn('hi there');
end
begin
F2;
end.
Ошибка здесь состоит в том, что точка с запятой отсутствует после end в процедуре F2. Однако, Турбо Паскаль укажет на ошибку в следующей строке. В данной простой программе легко обнаружить ошибку. Однако, в некоторых ситуациях вам пришлось бы потрудиться, чтобы найти то место, где пропущена точка с запятой.
Ошибки if/then/if/else
Даже очень опытные программисты могут допустить ошибку в конструкции if/then/if/else. Например, уверены ли вы, что следующий код работает правильно
if count<100 then
if count>50 then F1
else F2;
Не шутите с соответствующим форматированием. Предложение else не ассоциируется с первым if, а только со вторым if. Помните, что else всегда ассоциируется с ближайшим if. В данном примере вместо выполнения F2, когда count больше 100, Турбо Паскаль не делает ничего. Кроме того, F2 будет выполняться только, если count меньше 100 и - если меньше 50. Вы увидите это когда код правильно отформатирован:
if count<100 then
if count>50 then F1
else F2;
Если вы хотите просто выполнить F2, когда count больше 100, вам надо было бы использовать конструкцию begin/end, как показано далее:
if count<100 then
begin
if count>50 then F1;
end
else F2;
Забывание о параметрах var в процедурах и функциях
Иногда в пылу программирования легко забыть, что, если процедура или функция изменяет свои аргументы, они должны быть определены, как параметры типа var. Забывание этого может вызвать причудливые результаты и потребовать часов на отладку. Например, обсудим неправильную программу:
program Error;{ данная программа не верна }
var
t: integer;
procedure F1(x: integer);
begin
Write('Введите значение: ');
ReadLn(x);
end;
begin
F1(t); {получить значение t}
writeLn('t имеет значение: ', t);
end.
Данная программа не работает, так как значение назначается только локальной переменной х, а затем F1 возвращает управление и t не модифицируется. Чтобы сделать эту программу работающей, вы должны объявить х внутри F1, как параметр var. Это будет означать, что переменная t будет модифицирована. Правильная программа выглядит следующим образом:
program Fixed; {данная программа верна}
var
t: integer;
procedure F1(var x: integer);
begin
Write('Введите значение: ');
ReadLn(x);
end;
begin
F1(t); {получить значение t}
writeLn('t имеет значение: ', t);
end.
Хотя эту простую программу легко исправить, когда такая ошибка произойдет в большой программе, она может оказаться одной из наиболее трудных для нахождения.
Общие соображения по отладке
Каждый может иметь свой подход к программированию и отладке. Однако, доказано, что одни методы лучше других. В случае отладки возрастающее тестирование считается наиболее дешевым и эффективным по времени методом, даже если кажется, что оно замедляет процесс разработки.
Инкрементное тестирование - это простой процесс, всегда имеющий рабочий код. Как только можно будет запустить кусок вашей программы, вам следует сделать это, проверяя эту секцию полностью. По мере расширения вашей программы продолжайте проверять новые секции. При таком методе вы можете быть уверены, что любая возможная ошибка находится в малой области кода.
Теория инкрементного тестирования основывается в основном на вероятности и областях. Каждый раз, как вы добавляете длину, вы удваиваете области. Следовательно, по мере роста вашей программы образуется и рост областей, в которых вы должны осуществлять поиск ошибки. При отладке вы как программист хотите иметь дело с как можно меньшей областью. С помощью данного метода вы можете отделить область, которая уже проверена от всего кода и, следовательно, уменьшить область, которая может содержать ошибки.