Основы работы с файлами.
[+] Введение:
Трудно переоценить умение работать с файлами, ведь это одна из наиболее часто встречающихся в программировании задач. По большому счёту можно утверждать, что вся работа компьютера сводится к манипуляции с файлами, а точнее данными, содержащимися в них. Практически любая создаваемая программа должна взаимодействовать с файловой системой компьютера и осуществлять базовые операции ввода-вывода. Кроме того, в системе Windows умение работать с файлами весьма пригодится при обращении к именованным каналам, почтовым слотам и сокетам.
[+] Классификация типов файлов:
Традиционно под файлом понимается именованная совокупность данных на внешнем носителе. Однако, в Delphi этот термин трактуется как любое внешнее устройство, по своему назначению являющееся источником или приёмником информации, например, дисплей, клавиатура и т.д. Такое устройство называется логическим, если учитывается только его основная функция, а не физические характеристики. Для того, чтобы указать, откуда считываются данные и куда данные должны записываться, в Delphi вводится понятие файла.
Программисты, работающие на языке Delphi
все существующие типы файлов разделяют на три направления:
1. Текстовые файлы
2. Типизированные файлы
3. Двоичные (нетипизированные) файлы.
Текстовый файл специализируется на хранении текстовой информации, представленной в символах ASCII. Как правило, такие файлы снабжаются специфичным только для них расширением *.txt и могут быть открыты любым текстовым редактором.
В отличие от узкоспециализированного текстового файла, типизированный файл предназначен для работы с данными, определяемыми программистом. В качестве них могут выступать как любые целые, вещественные, символьные, логические и строковые типы данных, так и записи, состоящие из только что перечисленных типов. Синтаксис языка не запрещает создание типизированных файлов-массивов, хотя в общем-то файл сам по себе является массивом.
Самый универсальный формат файла - двоичный. Это файлы графики, аудио- и видеофайлы, электронные таблицы, HTML-файлы. Текстовые и типизированные файлы представляют собой частный случай двоичного файла. Нетипизированные файлы очень часто используются при решении задач, в которых нет необходимости анализа содержимого этих файлов. Типом файла определяются особенности применения методов, реализующих базовые операции ввода-вывода.
[+] Описание и обработка файлов:
Программирование на уровне абстракции, называемой файлом, сводится к тому, что до начала операций ввода-вывода конкретному внешнему файлу должна быть поставлена в соответствие переменная файлового типа. После этого необходимо открыть файл для чтения, записи или чтения и записи информации совместно. Только после этих действий может быть выполнена требуемая операция.
Объявление типизированных и нетипизированных файлов может быть дано в следующей форме:
type <имя файловой переменной> = file [of <тип>]
Здесь <тип> - это тип каждой из записей типизированного файла. Если зарезервированное слово of и параметр <тип> опущены, объявляемый файл является нетипизированным. При этом под записями подразумеваются компоненты файла, которые могут иметь любой, но один и тот же тип, за исключением файлового или любого структурированного типа, содержащего элементы типа файл. Каждая запись файла имеет номер, причём считается, что первой записи присвоен нулевой номер. Не следует смешивать записи файлов с данными комбинированных типов (record), которые также обычно называются записями.
Как типизированные, так и нетипизированные файлы в основном используются в режиме последовательного доступа, когда ввод и вывод записей ведутся последовательно, в порядке возрастания их номеров. Следует учитывать, что с этими файлами можно работать и в режиме последовательного доступа, когда возможно выборочное обращение к конкретным записям, которые задаются их номерами.
В Delphi определён также стандартный файловый тип, обозначаемый при объявлении служебным словом text:
type <тип> = text;
Текстовый файл представляет собой последовательность строк переменной длины. Каждая строка завершается признаком конца строки EOLN. Текстовые файлы могут использоваться как для ввода, так и для вывода информации, но только в режиме последовательного доступа. Примеры объявления файловых типов и переменных:
type A = file of char;
// Файл типа Char
B = file of
integer;
// Файл типа Integer
C = file
of real;
// Файл типа Real
D = file
of TPoint; //
Файл для работы с записями типа TPoint
E = text;
// Текстовый файл
var f: textfile;
g: file of integer;
h: file of char;
k: file of real;
m: file of TPoint;
n: file;
// Двоичный файл
Файловые переменные имеют специфическое применение. Над ними нельзя выполнять никаких операций. Можно использовать их лишь для операций с файлами (чтения, записи, удаления и т.д.). Кроме того, через файловую переменную можно получить информацию о конкретном файле (имя, тип и т.д.).
Базовые операции ввода-вывода в первую очередь нацелены на решение задач чтения из файла и записи в файл требуемых данных. При реализации этих операций любой язык программирования требует от нас особой внимательности и осторожности. Во-первых, следует учитывать, что перечень применяемых методов ввода-вывода определяется типом файла, с которым планируется работа. Во-вторых, необходимо соблюдать строго определённую последовательность вызова методов. В-третьих, в своём коде программист должен предусмотреть и тщательно реализовать систему обработки потенциальных ошибок.
Для работы с файлами всех трёх типов существуют
общие правила:
1. Файл является некоторой
переменной, следовательно, ему следует присвоить имя. С другой
стороны, операционная система использует имена файлов на различных
носителях. Между именами файлов в Delphi и
именами файлов, присеваемых операционной системой, нужно установить
связь. Для этого имеется стандартная процедура Assign,
вызов которой осуществляется следующим образом:
Assign (<имя>, <файл>);
2. Файл f следует открыть
для выполнения планируемых операций с помощью одной из трёх
процедур: Reset(f), Rewrite(f) или
Append(f), входящих в стандартный модуль
System. Существуюший файл открывается для считывания
с помощью процедуры Reset, а для создания
нового файла и подготовки его к записи применяется процедура
Rewrite. Типизированные нетекстовые файлы всегда
открываются с помощью процедур Rewrite и
Reset, причём последняя делает файл доступным
не только для считывания, но и для записи или дозаписи. Для
дозаписывания текстового файла необходимо его открыть с
использованием процедуры Append(f). С помощью
процедуры Rewrite открывается новый пустой
файл, и ему приписывается имя, заданное процедурой Assign.
Если файл с таким именем уже существует, то он
уничтожается.
3. С помощью
стандартных процедур Read(f, <список ввода>),
Readln(f, <список ввода>)
или Write(f, <список вывода>),
Writeln(f, <список вывода>)
можно соответственно считать данные из файла f
или записать данные в файл. В этих операторах
f - это файловая переменная; <список
ввода-вывода> - это перечисленные через
запятую имена переменных при вводе и перечисленные через запятую
выражения, значения которых выводятся при выводе. Процедуры
Readln, Writeln предназначены для работы со строками
и могут использоваться без указания списка ввода-вывода.
Типизированные файлы строятся из записей фиксированного размера,
который задаётся неявным образом при объявлении типа файла. Поэтому
такие файлы, открытые процедурой Reset, могут
быть модифицированы путём попеременного обращения к процедурам
Read и Write.
4.После завершения работы
с файлом с помощью стандартной процедуры CloseFile(f)
файл закрывается. При этом дописывается метка конца
файла EOF. Закрытие файла с помощью процедуры
CloseFile позволяет обеспечить сохранность
информации. При попытке закрыть с помощью CloseFile
файл, который не был до этого
открыт или который уже закрыт, возникает ошибочная ситуация.
[+] Текстовые файлы:
После связывания файловой переменной с именем
файла, в зависимости от поставленных задач можно пойти по одному из
трёх путей:
1. Создание нового текстового
файла.
2. Открытие существующего
файла в режиме только для чтения.
3. Открытие существующего
файла с возможностью добавления новых строк.
Для реализации этих задач предусмотрено три процедуры:
procedure Rewrite (var f: File [; RecSize]);
Ключевой параметр процедуры - файловая переменная F. В результате выполнения метода формируется пустой файл с именем, определённым в процедуре инициализации AssignFile(). Если вдруг имя вновь создаваемого файла совпадёт с именем существующего, то старый файл стирается без всякого предупреждения. Второй необязательный параметр определяет, какими порциями будут осуществляться операции записи данных в файл. По умолчанию назначается размер, равный 128 байт. При работе с текстовыми файлами этот параметр не используется.
Для открытия уже существующего файла процедура Rewrite() не нужна. Вместо неё используется метод инициализации чтения:
procedure Reset (var F [: File; RecSize: Word]);
Параметры процедуры идентичны аргументам предыдущего метода. После открытия внутренний указатель файла позиционируется на самом первом его байте. При использовании метода Reset() надо иметь в виду, что режим открытия файла этой процедурой зависит от значения глобальной переменной FileMod. По умолчанию в ней содержится значение 2, что соответствует режиму, допускающему как чтение, так и запись. Если планируется работа с файлом только в режиме чтения, то перед вызовом Reset() присвойте переменной FileMod значение 0. Соответственно, если файл открывается только для записи, установите переменную в значение 1.
При работе с текстовыми файлами метод Reset() откроет файл в режиме только для чтения независимо от значения переменной FileMod.
Текстовый файл может открываться в режиме добавления данных в конец файла. Для этого вызывается процедура:
procedure Append (var F: Text);
После выполнения процедуры внутренний указатель файла позиционируется на самом последнем байте файла.
Также существует ряд дополнительных методов. Очистка буфера текстового файла осуществляется методом:
procedure Flush (var F: text);
Вызов этого метода целесообразен в том случае, когда файл был открыт для записи. Функция гарантирует, что все данные из буфера будут сохранены в файл на диске. Эта процедура вызывается по окончании операций записи данных в файл.
Функция Eof - проверяет находится ли внутренний указатель файла на самой последней его строке. Этот метод обычно применяется при организации чтения данных из файла:
function Eof [ (var F: Text) ]: Boolean;
Метод SeekEof - отличается от обычного Eof тем, что умеет распознавать пустые строки (заполненные символами пробела) и исключать их из процесса чтения:
function SeekEof [ (var F: Text) ]: Boolean;
Функции Eoln и SeekEoln - проверка на нахождение указателя в конце строки текстового файла и соответственно аналогичный метод, но игнорирующий пробелы:
function Eoln [ (var F: Text) ]: Boolean;
function SeekEoln [ (var F: Text) ]: Boolean;
Пример программы, создающей новый файл в корне диска program Project1; Пример программы, осуществляющей вывод содержимого
текстового файла с последующим отображением прочитанной информации
на экран компьютера (скачать
здесь): program Project1;
{$APPTYPE CONSOLE}
uses SysUtils;
var
i: integer;
begin
AssignFile(F, 'C:\NewFile.txt');
try
Rewrite(F);
for i:=0 to 9 do Write(F, 'A');
finally
CloseFile(F);
end;
end.
{$APPTYPE CONSOLE}
uses SysUtils;
var
s: string;
begin
AssignFile(F, 'C:\NewFile.txt');
Reset(F);
try
while SeekEof(f)=False do
begin
Readln(F, S);
Writeln(s);
end;
finally
CloseFile(F);
end;
Readln;
end.
Пример программы, добавляющеё десять строк в конец текстового файла (скачать здесь):
program Project1;
{$APPTYPE CONSOLE}
uses SysUtils;
var F: TextFile;
i: integer;
begin
AssignFile(F, 'C:\NewFile.txt');
Append(F);
try
Rewrite(F);
for i:=0 to 9 do WriteLn(F, 'Строка
-', i);
Flush(F);
finally
CloseFile(F);
end;
Readln;
end.
Пример программы реализующей хранения целых чисел в текстовом файле (скачать здесь):
program Project1;
{$APPTYPE CONSOLE}
uses SysUtils;
var F: TextFile;
i: integer;
begin
AssignFile(F, 'C:\NewFile.txt');
try
Rewrite(F);
Writeln(F, ' 1 2 3 4
5 ');
Writeln(F, '
');
Writeln(F, '
');
Writeln(F, ' 55 123 545 4 43
557');
Reset(F);
while SeekEof(F)=false do
// Цикл построчного
чтения, игнорирующий пустые строки
begin
while SeekEoln(f)=false do
// Цикл чтения чисел из строки,
игнорирующий пробелы
begin
Read(F, i);
Writeln(i);
end;
end;
finally
CloseFile(f);
end;
Readln;
end.
[+] Типизированные файлы:
Несколько иначе обстоят дела в случае редактирования типизированного файла или чтения из него информации. Здесь может понадобиться системная переменная FileMode, определяющая режим открытия файла (по умолчанию её значение равно 2, что предоставляет право для чтения и записи одновременно).
Типизированные файлы используют ряд процедур и функций характерных только для них. Процедура Seek отвечает за позиционирование внутреннего указателя файла:
procedure Seek (var F; N: Longint);
Первый аргумент данного метода требует передачи файловой переменной. Второй параметр определяет, в каком месте файла мы собираемся отказаться. Чтобы поместить указатель в начале файла надо набрать Seek(F, 0); в конец файла - Seek(F, FileSize(F)-1); в середину файла - Seek(F, FileSize(F) div 2.
Чтобы узнать в каком месте находится указатель в настоящий момент существует метод:
function FilePos (var F): Longint;
Если указатель ссылается на начало файла, то функция вернёт нулевое значение, а если на конец, то возвратит значение FileSize(). Следующий метод сообщает о количестве записей в типизированном файле:
function FileSize (var F): integer;
Эта функция возвращает размер файла не в байтах, а в элементах, из которых состоит файл. Поэтому, чтобы выяснить физический размер типизированного файла в байтах, необходимо необходимо умножить значение FileSize(F) (возвращающее количество записей в файле) на размер этой записи в байтах - SizeOf( ).
Процедура переименования файла:
procedure Rename (var F; NewName: string);
Процедура удаления файла:
procedure Erase (var F);
Прежде чем удалить файл, определённый в файловой переменной F, обязательно закройте его, вызвав метод CloseFile( ).
Пример программы реализующей применение типизированного файла (скачать здесь):
program Project1;
{$APPTYPE CONSOLE}
uses SysUtils;
type
TPeople =
packed record
SurName:
string[25];
FName, LName:
Char;
Money:
currency;
end;
var People: TPeople;
F: File of
TPeople;
begin
AssignFile(F, 'C:\NewFile.dat');
Rewrite(F);
try
with People do //
Заполнение полей записи
begin
SurName:='Иванов';
People.FName:='B';
People.LName:='A';
People.Money:=1000;
end;
Write(F, People);
// Внесение записи
finally
CloseFile(F);
end;
end.
Важное преимущество типизированного файла - возможность редактировать любой из элементов файла. В обычном текстовом файле можно лишь добавлять строки. Следующий пример демонстрирует как это реализовать (скачать здесь):
program Project1;
{$APPTYPE CONSOLE}
uses SysUtils;
type
TPeople =
packed record
SurName:
string[25];
FName, LName:
Char;
Money:
currency;
end;
var People: TPeople;
F: File of
TPeople;
begin
AssignFile(F, 'C:\NewFile.dat');
FileMode:=1;
Reset(F);
try
with People do //
Заполнение полей записи
begin
SurName:='Петров';
People.FName:='Г';
People.LName:='Л';
People.Money:=800;
end;
Seek(F, 0);
Write(F, People);
// Внесение изменение в запись записи
finally
CloseFile(F);
end;
end.
Программа демонстрирует случай если понадобится удвоить оклады всем сотрудникам (скачать здесь):
program Project1;
{$APPTYPE CONSOLE}
uses SysUtils;
type
TPeople =
packed record
SurName:
string[25];
FName, LName:
Char;
Money:
currency;
end;
var People: TPeople;
F: File of
TPeople;
begin
AssignFile(F, 'C:\NewFile.dat');
FileMode:=2;
Reset(F);
try
while FilePos(F)<>FileSize(F)
do
begin
Read(F, People);
People.Money:=People.Money*2;
seek(F, FilePos(F)-1);
Write(F, People);
end;
finally
CloseFile(F);
end;
end.
Несколько сложнее обстоят дела с удалением из типизированного файла ненужного элемента (скачать здесь):
program Project1;
{$APPTYPE CONSOLE}
uses SysUtils;
type
TPeople =
packed record
SurName:
string[25];
FName, LName:
Char;
Money:
currency;
end;
var People: TPeople;
Source, Receptor: File
of TPeople;
begin
AssignFile(Source, 'C:\People.dat');
try
Rename(Source, 'c:\~People.dat');
// Переименовываем старый файл
Reset(Source);
AssignFile(Receptor, 'c:\People.dat');
Rewrite(Receptor);
while FilePos(Source)<>FileSize(Source)
do
begin
Read(Source, People);
if People.SurName<>'Петров'
then Write(Receptor, People);
end;
finally
CloseFile(Source);
Erase(Source);
CloseFile(Receptor);
end;
end.
Но если типизированный файл включает сотни и более записей, предложенный выше алгоритм становится весьма неэффективным. В таком случае в состав полей записи типизированного файла можно добавить поле логического типа, в котором хранится признак удаления. При поступлении команды на удаление записи вместо реального стирания строки полю присваивается значение True и запись убирается с экрана.
[+] Двоичные файлы:
Отличие состава базовых операций ввода-вывода, применяемых для нетипизированных файлов, заключается в методах, осуществляющих чтение и запись. Суть отличия заключается в том, что чтение и запись производится блоками:
procedure BlockRead (var F: file; var Buf; Count: integer [; var AmtTransferred: integer]);
procedure BlockWrite (var F: file; var Buf; Count: integer [; var AmtTransferred: integer]);
Первый аргумент - это файловая переменная. Второй аргумент - служит буфером, в который записываются прочитанные данные или, наоборот, из которой считывается информация для сохранения в файле. В качестве буфера используется байтовый массив:
var Buf: array [0..127] of byte;
Параметр Count определяет, сколько записей планируется считать из файла за одну операцию чтения. Как правило, ему присваивают значение, равное 1. Аргумент AmtTransferred - необязательный. Во время операций чтения из него можно узнать количество записей, реально считанных из файла, а во время записи в переменной окажется количество записей, внесённых в файл. Применяя термин записи имеют ввиду буфер, а размер массива буфера - размер записи. Размер записи всегда должен быть кратен размеру файла, с которым собираетесь работать. В противном случае вы столкнётесь с проблемой чтения за пределами файла. Самым универсальным размером записи служит 1 байт, однако такое значение снизит производительность процедур чтения-записи.
Листинг демонстрирует способ создания двоичного файла (скачать здесь):
program Project1;
{$APPTYPE CONSOLE}
uses SysUtils;
var F: File;
i: integer;
Buffer: array[0..1023] of byte;
begin
for i:=0 to SizeOf(Buffer) do Buffer[i]:=Random(255);
AssignFile(F, 'C:\NewFile.dat');
try
Rewrite(F, 1024);
BlockWrite(F, Buffer, 1);
finally
CloseFile(F);
end;
Readln;
end.
В качестве буфера используется массив размером 1 КБайт. Заполнив массив случайными числами, переносим содержимое его ячеек на жёсткий диск.
Эта программа демонстрирует процесс чтения из нетипизированного файла. Пример расчитан на то, что размер загружаемого файла кратен 256 байт (скачать здесь):
program Project1;
{$APPTYPE CONSOLE}
uses SysUtils;
var F: File;
i: integer;
Buffer: array[0..255] of byte;
begin
AssignFile(F, 'C:\NewFile.dat');
try
Reset(F, 256);
while Eof(F)=False do
begin
BlockRead(F, Buffer, 1);
end;
finally
CloseFile(F);
end;
Readln;
end.