Работа с файлами

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

Работа с файлами в Паскале. Общие сведения

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

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

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

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

f: file of real;

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

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

При определении переменной файлового типа также в программе появляется скрытый (неявный) текущий указатель файла. Его назначение – указывать на конкретный элемент файла (обеспечивать доступ к нему).

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

Операции с файловыми переменными включают:

Создание файла и запись данных в него

var
    f: file of char;
    c: char;
    i, n: byte;
 
begin
    assign (f, 'c:\file.txt');
    rewrite (f);
 
    write ('Количество символов: ');
    readln (n);
 
    for i:=1 to n do begin
        write ('Введите символ: ');
        readln (c);
        write (f, c);
    end;
 
    close (f);
 
end.

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

Процедура rewrite открывает файл в режиме записи, т.е. мы можем вводить данные в файл с помощью процедуры вывода из программы write. Если указанный файл отсутствует на диске, то он будет создан. Если файл существует и содержит данные, то все они будут удалены и заменены в дальнейшем новыми (перезаписаны).

Запись данных в файл – результат выполнения процедуры write (f, c), где f – файловая переменная, а с – выводимый из программы, но вводимый в файл символ.

В конце требуется закрыть файл и «освободить» переменную f. Это делается с помощью процедуры close.

Чтение данных из файла

var
    f: file of char;
    c: char;
 
begin
    assign (f, 'c:\file.txt');
    reset (f);
 
    while not eof (f) do begin
        read (f, c);
        writeln (c);
    end;
 
    close (f);
 
readln
end.

Процедура reset открывает файл для чтения. Т.е. мы можем в дальнейшем в программе извлекать данные из файла с помощью процедуры read.

Данные извлекаются «порциями» базового типа. В данном примере – это char (символы).

Чтение данных из файла продолжается до тех пор, пока не будет достигнут конец файла. Функция eof проверяет достигнут ли конец файла, переданного ей в качестве аргумента и, если достигнут, возвращает true. Выражение not eof (f) проверяет обратное – то, что конец файла еще не достигнут.

Функция IOResult

var
    f: file of char;
    c: char;
    r: integer;
 
begin
    assign (f, 'c:\file1.txt');
 
    {$I-}
    reset (f);
    {$I+}
 
    r := ioresult;
 
    if r <> 0 then
        writeln ('Такого файла нет')
    else
        while not eof (f) do begin
            read (f, c);
            writeln (c);
        end;
 
    close (f);
 
readln
end.	

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

Чтобы избежать преждевременного выхода из программы, можно отключить автоматический контроль ошибок ({$I-}) и воспользоваться функцией IOResult.

Функция IOResult возвращает ноль лишь в том случае, если файл существует.

Редактирование файла

var
    f: file of char;
    c: char;
    n: integer;
 
begin
    assign (f, 'c:\file.txt');
    reset (f);
 
    write ('Номер элемента: ');
    readln (n);
 
    seek (f, n);
 
    write ('Новый символ: ');
    readln (c);
    write (f, c);
 
    close (f);
 
readln
end.	

В языке программирования Pascal для редактирования файлов предназначена процедура seek. В качестве аргументов она принимает файловую переменную и номер заменяемого элемента. В это место помещается текущий указатель файла. Далее с помощью write производится запись в файл.

Предварительно файл необходимо открыть для чтения (reset).

Текстовые файлы

Текстовые файлы состоят из символьных строк переменной длины. Каждая строка завершается специальной комбинацией, называемой «конец строки». Комбинация «конец строки» состоит из двух символов: «перевод каретки» (ASCII-код #13) и «перевод строки» (#10). Завершается текстовый файл символом «конец файла» (#26).

Описание текстового файла осуществляется объявлением переменной типа Text:

var файловая_переменная: Text;

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

Отличие операторов Read и ReadLn при чтении из текстовых файлов состоит в том, что оператор ReadLn, поместив значение в последнюю переменную списка ввода, переходит на начало следующей строки, не считывая оставшиеся в строке данные. С другой стоны, оператор Read остается готовым считывать данные со следующей позиции текущей строки. Так, предположим, что в текстовом файле f имеются две строки:

1 -2
4

В этом случае два оператора Read(f,m); Read(f,n); поместят в целочисленные переменные m и n соответственно значений 1 и -2, а два оператора ReadLn(f,m); ReadLn(f,n); считают значения 1 и 4.

Пример. В текстовом файле f.txt через пробел и записаны целые числа. Переписать в файл f1.txt из файла f.txt все числа, за исключением максимальных (предполагается, что их может быть несколько).

var f,f1: Text;
    a,max: LongInt;
    flag: Boolean;
begin
  Assign(f,'f.txt');
  Reset(f);
  while not Eof(f) do begin
    Read(f,a);
    if a>max then
      max := a;
  end;
  Assign(f1,'f1.txt');
  Rewrite(f1);
  Reset(f);
  while not Eof(f) do begin
    Read(f,a);
    if a<>max then
      WriteLn(f1,a);
  end;
  Close(f);
  Close(f1);
end. 

В примере файл f.txt прочитывается два раза. Первый раз для определения максимального числа, второй раз — для считывания чисел и их записи во второй файл. Данный алгоритм используется, если максимальных чисел в файле несколько.

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

Типизированные файлы

Более характерным для Pascal являются типизированные файлы, или файлы произвольного доступа. Основным свойством этих файлов является то, что их структура данных представляет собой последовательность компонентов одного типа. Описывают подобный файл словосочетанием file of с последующим указанием типа компонентов файла, число которых (длина файла) не фиксируется:

var имя_файла: file of тип_компонентов

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

var 
	FileInt: file of Integer

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

Чтобы можно было работать с типизированным файлом, необходимо, как и для текстовых файлов, сначала связать имя файловой переменной с внешним именем файла (оператор Assign). Затем нужно открыть его (используются операторы Reset и Rewrite, но не Append). Операторы Reset и Rewrite открывают файл и для чтения, и для записи (а не только для чтения или только для записи, как при использовании текстовых файлов). Отличие их в том, что оператор Reset открывает только существующий файл (если такого файла нет, будет сгенерирована ошибка времени выполнения). С другой стороны, оператор Rewrite создает новый файл (если файл с таким именем уже имеется, то он будет уничтожен и создан заново). При открытии файла с ним связывается текущий указатель файла, который позиционируется на его первый элемент. Оперировать можно только тем элементом файла, на который ссылается указатель файла. При чтении или записи элемента файла происходит автоматическое перемещение указателя на следующий элемент. Чтение из типизированного файла производится оператором Read (но не ReadLn), а запись в него — оператором Write (но не WriteLn). Однако следует помнить, что в списке вывода оператора Write могут быть только переменные. Типы элементов файла и типы переменных в списках ввода-вывода должны быть согласуемы по присваиванию. Элементами типизированных файлов могут быть числовые, символьные, булевы, строковые значения, массивы, записи, но не файлы или структуры с файловыми элементами.

Узнать количество элементов типизированного файла (размер файла) можно с помощью функции FileSize, для которой используется следующий синтаксис:

FileSize(имя_файла)

Например, если переменная k имеет тип LongInt, а f – файловая переменная типизированного файла, то оператор k := FileSize(f), записывает в переменную k размер файла f.

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

FilePos(имя_файла)

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

Seek(имя_файла, номер_элемента)

Второй параметр (тип LongInt) задает номер элемента (отсчет от 0), на который должен переместиться указатель файла. Рассмотрим несколько примеров.

Перейти к пятому (фактически шестому) элементу файла f:

Seek(f, 5);

Перейти к предыдущему элементу:

Seek(f, FilePos(f)-1);

Перейти в конец файла:

Seek(f, FilePos(f)-1);

Как и для текстовых файлов, можно использовать функцию Eof(имя_файла), которая возвращает значение True, если текущий указатель расположен на признаке конца файла (т. е. при выполнения равенства FilePos(имя_файла) = FileSize(имя_файла)).

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

Текстовые файлы могут быть созданы текстовым редактором. Однако типизированные файлы создаются в результате работы какой-либо программы.

Пример записи данных в типизированный файл:

type
    t_subscriber = record
      surname: string[20];
      tel: LongInt;
    end;
 
var
    subscriber: t_subscriber;
    f: file of t_subscriber;
    i: Integer;
 
begin
  Assign(f,'notebook.dat');
  Rewrite(f);
  for i:=1 to 5 do begin
    with subscriber do begin
      Write('Surname: ');
      ReadLn(surname);
      Write('Phone: ');
      ReadLn(tel);
    end;
    Write(f, subscriber);
  end;
  Close(f);
end.

Пример последовательного доступа к типизированному файлу:

type
    t_subscriber = record
      surname: string[20];
      tel: LongInt;
    end;
 
var
    subscriber: t_subscriber;
    f: file of t_subscriber;
    s: string[7];
 
begin
  Assign(f,'notebook.dat');
  Reset(f);
  while not Eof(f) do begin
    Read(f, subscriber);
    with subscriber do begin
      str(tel,s);
      if Copy(s,1,2) = '33' then
        tel := tel+4000000;
    end;
    Seek(f,FilePos(f)-1); // возврат указателя назад
    Write(f,subscriber);
  end;
  Close(f);
end. 

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

Нетипизированные файлы

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

var имя_файла: file;

Например, возможно такое описание:

var FileOneType: file;

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

Открываются файлы без типа теми же операторами Reset и Rewrite, но в этом случае имеется второй параметр — размер записи (элемента файла), заданный в байтах. Предварительно нужно с помощью оператора Assign связать внутреннее имя файла с внешним:

Assign(FileOneType, 'f.dat');   Reset(fileOneType, 1);

Второй параметр операторов Reset и Rewrite может быть опущен, что означает задание размера записи в 128 байт. Наибольшая скорость обмена данными обеспечивается при длине записи, кратной 512 байт (размеру сектора на диске).

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

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

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

BlockRead(имя_файла, переменная_буфер, количество_записей)

Третий параметр — это количество записей, читаемых за один раз (тип Word).

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

Запись данных в нетипизированный файл производится только после его открытия с помощью процедуры Rewrite. Для записи данных используется процедура BlockWrite, которая имеет те же три (или четыре) параметра, что и BlockRead. При этом в переменную_буфер нужно предварительно поместить записи. Количество этих записей должно совпадать со значением третьего параметра процедуры BlockWrite, а размер — со вторым параметром процедуры Rewrite. В четвертом параметре процедуры BlockWrite (если он имеется) возвращается количество фактически помещенных в фал записей. Если на диске нет свободного места, то после выполнения процедуры BlockWrite значения третьего и четвертого параметров будут отличаться.

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

Работа с нетипизированными файлами на примере копирования файла:

const
  CountRead = 512; // кол-во читаемых записей.
   // При длине записи в 1 байт - размер буфера.
 
var
  file_in, file_out: file;
  name1, name2: string;
  num_read, num_write: word; // фактич. считанные/записанные записи
  buf: array[1..CountRead] of char; // буфер
 
begin
  Write('File 1: ');
  Readln(name1);
  Write('File 2: ');
  Readln(name2);
  Assign(file_in, name1);
{$I-}
  Reset(file_in,1);
{$I+}
  if IOResult <> 0 then begin
    WriteLn('Файл-оригинал не найден'); Halt
  end;
  Assign(file_out, name2);
  Rewrite(file_out, 1); // длина записи 1 байт
  repeat
    BlockRead(file_in, buf, CountRead, num_read);
    BlockWrite(file_out, buf, CountRead, num_write);
  until (num_read = 0) or (num_write <> num_read);
  writeln('Copying is completed');
  if num_read <> num_write then
    writeln('Not enough space');
  close(file_out); close(file_in);
end. 

Операции для работы с файловой системой

В Pascal существует несколько процедур для работы с файловой структурой.

Процедура Rename служит для переименования файла или каталога. Синтаксис процедуры следующий:

Rename(файловая_переменная, новое_имя)

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

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

Erase(файловая_переменная)

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

Четыре процедуры (ChDir, MkDir, RmDir и GetDir) в Pascal обеспечивают работу с каталогами.

Первые три процедуры используют один и тот же синтаксис:

ChDir(каталог)
MkDir(каталог)
RmDir(каталог)

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

Процедура ChDir изменяет текущий каталог на указанный, процедура MkDir создает новый каталог с указанным именем, а процедура RmDir уничтожает каталог при условии, что он пустой.

Процедура GetDir позволяет определить имя текущего каталога на указанном диске. Синтаксис процедуры таков:

GetDir(диск, каталог)

Здесь параметр диск представляет собой выражение типа Word, задающее номер диска (0 – активный диск, 1 – диск A, 2 – диск B и т. д.). Параметр каталог – это переменная типа string, которая служит для возврата пути к текущему каталогу на диске, номер которого указан в качестве первого параметра процедуры. Пример использования описанных выше процедур представлен в программе ниже.

var
  f: text;
  s: string;
 
begin
  Assign(f,'a.txt'); // связываемся с файлом
  Rename(f,'b.txt'); // переименовываем его
 
  MkDir('foto'); // создаем каталог
  ChDir('foto'); // переходим в него
  GetDir(0,s);  // полное имя текущего каталога записываем в s
  writeln(s);
 
  ChDir('..'); // поднимаемся на уровень вверх
  rewrite(f); // открываем файл на запись
  write(f,s); // записываем туда строку
  close(f);
  readln; // пока не нажата клавиша, вы можете видеть каталог
  RmDir('foto'); // удаляем каталог
 
end.