Демидов Основы программирования в примерах на языке ПАСЦАЛ 2010
.pdfошибка считается обработанной, после чего можно продолжать операции ввода-вывода.
Основным уязвимым местом в программе при работе с файлами является момент открытия. В частности, ошибка может возникнуть при открытии несуществующего файла. Рассмотрим программу:
var
f: Text; begin
write('Enter file path: '); readln(path);
assign(f, path); {$I-}
reset(f); {$I+}
if IOResult <> 0 then
writeln('Error while opening file '+path); exit;
end;
// штатная работа с файлом
…
end.
В данной программе при ошибке открытия файла будет выдано сообщение об ошибке и программа завершится.
Задача слияния словарей
В двух текстовых файлах записаны слова – по одному в строке. При этом слова упорядочены по алфавиту. Необходимо получить третий файл, в котором содержатся слова из первого и второго файла без повторений, сохранив алфавитный порядок слов.
Данная задача схожа с задачей слияния массивов. Однако имеются некоторые отличия. Во-первых, количество слов в файлах заранее неизвестно, файлы вообще могут быть пусты. Во-вторых, чтение элементов массивов и их сравнение могут осуществляться в одном выражении, а при работе с файлами требуется сначала считать строки, а лишь затем сравнивать. Поэтому удобнее организовать один сложный цикл, который будет выполняться до тех пор, пока хотя бы один файл не считан до конца:
var
f, g, h: Text; s1, s2: string;
121
fstRead, sndRead: boolean; begin
assign(f, '…'); assign(g, '…'); assign(h, '…');
reset(f); |
// первый входной открываем на чтение |
reset(g); |
// второй входной открываем на чтение |
rewrite(h); |
// выходной открываем на запись |
fstRead := false; // строка из первого файла не считана sndRead := false; // строка из второго файла не считана
// основной цикл слияния двух файлов while not EOF(f) or not EOF(g) do begin
// безопасное считывание
if not fstRead and not EOF(f) then begin readln(f, s1);
fstRead := true; end;
if not sndRead and not EOF(g) then begin readln(g, s2);
sndRead := true; end;
// слияние
if not fstRead then begin
// первый файл кончился, а второй еще нет writeln(h, s2);
sndRead := false; end
else if not sndRead then begin
// второй файл кончился, а первый еще нет writeln(h, s1);
fstRead := false; end
else begin
// оба файла еще не кончились if s1 < s2 then begin
writeln(h, s1); fstRead := false;
end
else if s1>s2 then begin writeln(h, s2); sndRead := false;
end
else begin
// при s1=s2 строка записывается только один раз
122
writeln(h, s1); fstRead := false; sndRead := false;
end; end;
end;
close(f);
close(g);
close(h);
end.
Основная идея этого решения заключается в разделении процессов чтения строк из файлов и слияния. Для этого вводятся две дополнительные булевы переменные – fstRead и sndRead, каждая из которых указывает на то, что из соответствующего файла была считана строка. При слиянии эти переменные играют важнейшую роль.
123
Глава 12. Записи и типизированные файлы
Записи и массивы записей
Представьте, что в программе требуется описать некоторый объект реального мира, например запись в телефонной книжке. Это возможно только с помощью совокупности переменных. Для работы с этой совокупностью, как с единым целым, в языке Паскаль вводится тип-запись, в Си – тип-структура. Для объявления типазаписи следует использовать ключевое слово record и далее описать составляющие запись переменные, которые называются полями записи. Далее можно объявлять переменные нового сложного типа. Например:
type
phone_rec = record fio: string[60]; phone: string[20]; e_mail: string[50];
end;
var
pr1, pr2: phone_rec;
Здесь в типе phone_rec объявлено три поля fio, phone, e_mail строковых типов.
Для обращения к конкретному полю записи необходимо после имени переменной-записи через '.' указать имя поля:
begin
…
pr1.fio := 'Ivanov Ivan'; pr1.phone := '89169110203'; pr1.e_mail := 'ivanov91@mail.ru';
…
writeln('fio: ' + pr1.fio); writeln('phone: ' + pr1.phone); writeln('e-mail: ' + pr1.e_mail);
…
end;
Кроме того, для обращения к полю записи может использоваться оператор with <имя записи> do. Тогда при обращении к полю имя записи можно не указывать:
124
begin
…
with pr1 do begin writeln('fio: ' + fio); writeln('phone: ' + phone); writeln('e-mail: ' + e_mail);
end;
…
end;
При этом текст программы немного сокращается, однако программисту следует позаботиться об исключении конфликтов имен переменных.
Для записей определена операция присваивания, при этом копируются все значения полей записи. Однако для записей не определены операции сравнения. Для сравнения записей необходимо попарно сравнить значения одноименных полей.
function Equal(pr1, pr2: phone_rec): boolean;
begin |
= pr1.fio) and |
|
Equal := (pr2.fio |
||
(pr2.phone |
= |
pr1.phone) and |
(pr2.e_mail |
= |
pr1.e_mail); |
end. |
|
|
Пример: описать телефонную книжку на 200 номеров. Это можно сделать с помощью массива однотипных записей. В следующей программе на экран выводится содержимое телефонной книги, хранящейся в массиве:
type
phone_book = array [1..200] of phone_rec; var
pb: phone_book; pr: phone_rec; i: integer;
begin
…
for i:=1 to 200 do begin pr := pb[i]; writeln('№', i);
writeln('Name: ' + pr.fio); writeln('Phone: ' + pr.phone); writeln('E-mail: ' + pr.e_mail); writeln;
125
end; end.
Вообще, записи и массивы можно комбинировать для создания сколь угодно сложных структур данных, вложенных друг в друга. Например:
type
passport_rec = record series: string[4]; number: string[6]; when: DateTime; whom: string[100];
end;
person = record fio: string;
birthdate: DateTime; passport: passport_rec;
end; var
p: person; begin
p.fio := 'Smith'; p.passport.series := '4700';
…
with p.passport do writeln(series, ' ', number); end.
Записи с вариантной частью
В примере с телефонной книгой одна запись содержала три поля. Однако часто в разных случаях уместен разный набор полей. Так, формат библиографической записи различается в зависимости от типа издания:
для книги следует указывать автора, название, год издания, издательство;
для журнала – название, год и месяц издания, номер, издательство;
для газеты – название, дату выхода (день, месяц, год),
номер, издательство.
Конечно, можно определить универсальную структуру данных, содержащую все возможные поля, и заполнять поля по мере надобности. Например:
126
type
biblio = record bibtype: char; author: string[40]; name: string[40]; year: integer; month: byte;
day: byte; number: integer;
publisher: string[40]; end;
Но такая структура неэкономна с точки зрения расхода памяти. На одну запись расходуется 1+40+40+4+1+1+4+40 = 131 байт. При этом общими для всех трех типов изданий являются название, год, издательство, а остальная информация может различаться или вовсе отсутствовать. Для создания более компактных структур данных в языке Паскаль реализована возможность определения записей с вариантной частью.
В таких записях после постоянной части, которая может и отсутствовать, указывается вариантная часть с помощью ключевого слова case. Общий вид записи с вариантной частью:
type
<имя типа> = record <поле>: <тип>;
…
<поле>: <тип>;
case <тег>: <перечислимый тип> of <список констант>:
(
<поле>: <тип>;
…
<поле>: <тип>; );
…
<список констант>:
(
<поле>: <тип>;
…
<поле>: <тип>; );
end;
Финальное слово end закрывает и запись, и вариантную часть.
127
Каждый список констант соответствует одному варианту, состоящему из одного и более полей. Содержимое списков констант для компилятора не важно, однако списки не должны пересекаться между собой. Для полей вариантов выделяется один и тот же участок памяти, т.е. варианты являются его различными представлениями и как бы наложены друг на друга. Таким образом, общий размер записи равен сумме размера постоянной части и размера самого ёмкого варианта.
Тег в вариантной части, вообще говоря, необязателен, однако его перечислимый тип следует указывать всегда. Если же тег указан, то он считается дополнительным полем постоянной части и может использоваться для анализа того, какой вариант из вариантной части уместен. В вариантной части (так же как и в постоянной) не должно быть одноименных полей. Это ограничение гарантирует однозначное распознавание варианта и последующее вычисление относительной позиции поля в памяти.
С помощью вариантной части запись biblio можно сделать более компактной:
type
biblio = record name: string[40]; year: integer;
publisher: string[40]; case bibtype: char of 'b': (author: string[40]); 'j': (
jmonth: byte; jnumber: integer );
'n': (
nmonth: byte; day: byte; nnumber: integer );
end;
В данном случае размер записи равен (40+4+40+1) + (40) = 125. Схема распределения памяти для структуры с вариантной частью показана на рис. 9.
128
name |
year |
publisher |
bibtype |
|
author (40) |
|
|||
(40) |
(4) |
(40) |
(1) |
jmonth (1) |
|
jnumber (4) |
|
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nmonth |
|
day |
number |
|
|
|
|
|
|
(1) |
(1) |
(4) |
|
|
|
|
|
|
|
|
|
|
|
|
|
Рис. 9. Схема распределения памяти для структуры с вариантной частью
Обращение к полям вариантной записи ничем не отличается от обращения к полям обычной записи. Однако программист должен сам определять, с каким из вариантов вариантной части работать в данный момент. Например, с помощью тега вариантной части:
procedure Print(bib: biblio); begin
with bib do begin writeln(name); writeln(year); writeln(publisher) case bibtype of
'b': writeln(author);
'j': writeln(jmonth, ' ', jnumber);
'n': writeln(nmonth, ' ', day, ' ', nnumber); end;
end; end;
Поскольку все варианты накладываются на один и тот же участок в памяти, то можно, например, записать в этот участок фамилию автора книги author, а затем обратиться к номеру журнала jnumber. При этом байты со 2 по 5, занимаемые author, будут расценены как число типа integer.
Типизированные файлы
Понятно, что для долгосрочного хранения совокупности записей, например телефонной книги, оперативная память непригодна, так как при завершении работы программы все данные будут потеряны. Для долгосрочного пользования телефонной книгой следует использовать механизм работы с файлами. Телефонная книга представляет собой совокупность записей типа phone_rec, т.е. последовательность однотипных компонентов, поэтому разумно использовать типизированные файлы.
129
Для работы с типизированным файлом следует объявить файловую переменную, указав тип компонентов файла с помощью ключевых слов file of:
var
pb: file of phone_rec;
Или же ввести новый файловый тип phone_book, а затем объявить файловую переменную:
type
phone_book = file of phone_rec; var
pb: phone_book;
В отличие от массива, на число компонентов файла не накладывается ограничений. Компоненты типизированного файла всегда нумеруются с 0.
Для открытия типизированных файлов используют знакомые процедуры rewrite и reset. Процедура rewrite открывает типизированный файл в режиме перезаписи, а процедура reset – в режиме чтения и записи. Следует напомнить, что текстовые файлы открываются процедурой reset только на чтение.
Для выполнения операций чтения и записи используются процедуры Read и Write. Однако, в отличие от текстовых файлов, процедуры работают с целыми компонентами файла: Read считывает компонент целиком, а Write записывает компонент целиком. В следующей программе на экран выводится содержимое телефонной книги, хранящейся в файле 'c:\dev\phones.bin' (предполагается, что файл уже существует):
var
pb: phone_book; pr: phone_rec; i: integer;
begin
Assign(pb, 'c:\dev\phones.bin'); Reset(pb);
while not EOF(pb) do begin
Read(pb, pr); // чтение одного компонента writeln('Name: ' + pr.fio); writeln('Phone: ' + pr.phone); writeln('E-mail: ' + pr.e_mail);
130