Записи. Работа с бинарными файлами.

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

type Pupil is Record
    Name: String(1..25);       --Имя
    Surname: String(1..25);    --Фамилия
    Patronymic: String(1..30); --Отчество
    Age : Integer;             --Возраст
    Sex : Character;           --Пол (мужской/женский)
end Record;

Далее в программе для работы с такой записью нужно создать переменную типа этой записи:

Person : Pupil;

Доступ к полям этой записи осуществляется с помощью точки:

Person.Name := "Иван";
Person.Surname := "Иванов";
Person.Patronymic := "Иванович";
Person.Age := "20";
Person.Sex := "M";

Таким образом одна переменная Person хранит всю информацию о конкретном человеке.

Для хранения подобной информации о большом количестве людей мы можем создать массив таких записей. Например:

type Vector is array(1..10_000) of Pupil;
mas : Vector;

В результате у нас появляется массив для хранения информации о 10 000 человек.

Работа с бинарными файлами.

Для сохранения записей в файл можно, конечно, использовать приёмы, рассмотренные в разделе "Работа с текстовыми файлами в языке Ада", и записывать/считывать файл поэлементно (каждую переменную отдельно), но в Аде предусмотрена возможность записывать и считывать созданные нами записи целиком, с помощью т.н. бинарных файлов. Для этого в Аде существуют два пакета: Ada.Sequential_IO и Ada.Direct_IO. Второй мне кажется более удобным, но рассмотрим мы оба пакета.

Итак, Ada.Sequential_IO. Пакет позволяет записывать/считывать информацию любой сложности. Мы можем создать запись, состоящую из большого числа переменных, и её сохранение в файл (или считывание из файла) будет осуществляться одной командой. Единственное условие - все переменные записи должны быть конечными. Это значит, например, что строки Unbounded_String этим пакетом не поддерживаются, т.к. их размер неизвестен.

Пакет Ada.Sequential_IO по своему составу аналогичен пакету Ada.Text_IO, но процедуры Get() и Put() в нём заменены на Read() и Write(), а также отсутствуют подпрограммы End_Of_Line, Skip_Line и New_Line.

Рассмотрим пример (он создан просто для ознакомления и не несёт никакой полезной нагрузки): создадим тип записи из трёх основных полей (переменных) и одной вспомогательной - имя, пол и возраст студента, длина имени (вспомогательная переменная), затем на основе этого типа создадим три переменных (трёх студентов), заполним поля каждой записи, запишем всю информацию в файл, а затем прочитаем данные из файла и выведем на экран:

with Ada.Sequential_IO;
with Ada.Text_IO; use Ada.Text_IO;
 
procedure main is
	--Создаем тип записи Student
	type Student is Record
		Name : String(1..25); --Имя студента
		Name_Len : Integer; --Длина имени (см. работу со строками)
		Sex : Character; --Пол
		Age : Integer range 1..100; --Возраст
	end Record;
 
	--"Создание" пакета для файлового ввода-вывода переменных типа Student
	package Seq_IO is new Ada.Sequential_IO (Student);
	--Создание трех переменных типа Student для записи их в файл
	St_1, St_2, St_3 : Student;
	--Создание переменной для чтения данных из файла.
	tmp : Student;
	--Файл, для работы с пакетом Sequential_IO;
	My_File : Seq_IO.File_Type;
	--Имя файла
	My_File_Name : String := "records.dat";
begin
	--Заполняем три записи: информация о трех студентах
	St_1.Name(1..11) := "Иван Иванов";
	St_1.Name_Len := 11;
	St_1.Sex := 'M';
	St_1.Age := 19;
 
	St_2.Name(1..11) := "Петр Петров";
	St_2.Name_Len := 11;
	St_2.Sex := 'M';
	St_2.Age := 20;
 
	St_3.Name(1..16) := "Василий Васечкин";
	St_3.Name_Len := 16;
	St_3.Sex := 'M';
	St_3.Age := 21;
 
	--Создаём файл для записи
	Seq_IO.Create(File => My_File, Mode => Seq_IO.Out_File, Name => My_File_Name);
	--Запись в файл трех переменных
	Seq_IO.Write(My_File, St_1);
	Seq_IO.Write(My_File, St_2);
	Seq_IO.Write(My_File, St_3);
	--Переоткрываем файл в режиме чтения
	Seq_IO.Reset(My_File, Mode => Seq_IO.In_File);
	--Последовательное чтение записей из файла и их вывод на экран
	Seq_IO.Read(My_File, tmp);
	Ada.Text_IO.Put_Line(tmp.Name(1..tmp.Name_Len) & " " & tmp.Sex & " " & Integer'Image(tmp.Age));
	Seq_IO.Read(My_File, tmp);
	Ada.Text_IO.Put_Line(tmp.Name(1..tmp.Name_Len) & " " & tmp.Sex & " " & Integer'Image(tmp.Age));
	Seq_IO.Read(My_File, tmp);
	Ada.Text_IO.Put_Line(tmp.Name(1..tmp.Name_Len) & " " & tmp.Sex & " " & Integer'Image(tmp.Age));
	--Закрытие файла
	Seq_IO.Close(My_File);
 
end main;

Если открыть файл records.dat в текстовом редакторе, то мы увидим набор символов, получить из которых какую-то информацию просто прочтением не получится. Это и есть бинарный файл.

Пакет Ada.Direct_IO. Этот пакет - надстройка над Ada.Sequential_IO. Он добавляет возможность прямого обращения к конкретной записи (а следовательно индексацию файла и определение размера файла), а также новый режим - InOut_File - режим чтения и записи. То есть, можно одновременно считывать информацию из файла и записывать информацию в файл. Ограничения те же - конечность переменных записи.

Рассмотрим пример. В прошлом примере мы создали файл, в котором сохранили три записи:

Иван Иванов M 19
Петр Петров M 20
Василий Васечкин M 21

Используем этот файл. Задача прочитать вторую запись (то, что касается Петра Петрова) из файла, изменить эту запись и сохранить в этом же месте файла.

with Ada.Direct_IO;
with Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
 
procedure main is
	type Student is Record
		Name : String(1..25); --Имя студента
		Name_Len : Integer; --Длина имени (так как имя - конечная переменная)
		Sex : Character; --Пол
		Age : Integer range 1..100; --Возраст
	end Record;
 
	--"Создание" пакета для файлового ввода-вывода переменных типа Student
	package Dir_IO is new Ada.Direct_IO (Student);
	--Создание переменной для чтения данных из файла.
	tmp : Student;
	--Файл, для работы с пакетом Direct_IO;
	My_File : Dir_IO.File_Type;
	--Имя файла
	My_File_Name : String := "records.dat";
begin
 
	--Открываем файл для чтения и записи
	Dir_IO.Open(File => My_File, Mode => Dir_IO.InOut_File, Name => My_File_Name);
	--Пусть нам нужно прочитать вторую запись из файла (не затрагивая первую)
	--Высчитываем размер одной записи и устанавливаем курсор на нужную нам запись
	Dir_IO.Set_Index(File => My_File, To => Dir_IO.Positive_Count(2));	
	--Считываем вторую запись (в данном случае это информация о Петре Петрове)
	Dir_IO.Read(My_File, tmp);
	--Выводим считанную запись на экран.
	Ada.Text_IO.Put_Line(tmp.Name(1..tmp.Name_Len) & " " & tmp.Sex & " " & Integer'Image(tmp.Age));
	--Меняем информацию о студенте
	tmp.Name(1..13) := "Семен Семенов";
	tmp.Name_Len := 13;
	tmp.Age := 18;
	--Опять переносим курсор на вторую запись
	Dir_IO.Set_Index(File => My_File, To => Dir_IO.Positive_Count(2));
	--и записываем её в файл
	Dir_IO.Write(My_File, tmp);
 
	--Для проверки выведем содержимое файла на экран
	--Переносим курсор в начало файла
	Dir_IO.Set_Index(File => My_File, To => Dir_IO.Positive_Count(1));	
	--и в цикле читаем содержимое файла и выводим на экран
	while not Dir_IO.End_Of_File(My_File) loop
		Dir_IO.Read(My_File, tmp);
		Ada.Text_IO.Put_Line(tmp.Name(1..tmp.Name_Len) & " " & tmp.Sex & " " & Integer'Image(tmp.Age));
	end loop;
 
	--Закрытие файла	
	Dir_IO.Close(My_File);
 
end main;

В результате работы программы информация о Петре Петрове будет перезаписана информацией о Семене Семенове:

Иван Иванов M 19
Семен Семенов M 18
Василий Васечкин M 21

Рассмотрим поподробнее строку Dir_IO.Set_Index(File => My_File, To => Dir_IO.Positive_Count(2)):

  • Функция Dir_IO.Positive_Count(2) вернёт позицию (смещение) файла, с которой начинается запись номер два. Т.е. эта функция вычислит размер одной записи и рассчитает, с какой позиции файла начинается вторая запись (номер записи, который указан в скобках).
  • Процедура Dir_IO.Set_Index() устанавливает курсор файла на позицию, указанную в аргументе To, а эту позицию нам как раз вернёт функция Dir_IO.Positive_Count().

Итак, если нам нужно работать с текстовым файлом, мы используем пакет Ada.Text_IO. Если нам нужно записать в файл массив сложных структур данных, то мы используем пакет Ada.Sequential_IO (или Ada.Direct_IO). Если же нам нужно обращаться к конкретным участкам файла (не считывая весь файл), то мы используем пакет Ada.Direct_IO.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *