Однако в приведенном примере способа получения доступа к записям файла присутствуют две ошибки. Первая из них, хотя и небольшая, тем не менее, очень важная. Единственным методом определения размера каждой записи является считывание ее из исходного кода программы, которая осуществляет доступ к файлу. Если есть файл записей, то для определения длины записи необходимо поработать с окном шестнадцатеричного представления. Если длина записи и объем файла известны, можно легко определить количество записей в файле.
И вторая проблема - файлы данных не содержат информации о структуре записей, количестве полей и их типах. Если бы в файле хранился больший объем информации, работать с записями и самими файлами было бы намного проще.
Какую информацию, помимо записей, потребовалось бы хранить в файле? Как уже говорилось, одним из дополнительных полей могла бы быть длина записи, а вторым - количество находящихся в файле записей. При помощи этих двух полей можно определить допустимость файла (т.е. равен ли объем файла количеству записей, умноженному на длину записи, плюс размер служебной информации).
Предположим, что в файле находится специальный служебный блок данных. Пусть этот блок содержит некоторые важные данные о файле, за которыми следует определенное количество записей одинакового размера. Другими словами, служебный блок данных содержит постоянную информацию о массиве (размер элемента, количество элементов и, может быть, ряд других данных).
В таком случае мы можем написать свой класс, который будет открывать файл и вносить в него записи (и, конечно, соответствующим образом изменять содержимое служебного блока), считывать записи по заданному порядковому номеру, записывать и обновлять записи по порядковому номеру и закрывать файл. А как же удаление записей? Не хотелось бы перемещать записи в файле на одну позицию с целью закрытия "дыры", образованной после удаления одной записи, как мы это делали в массивах в памяти. Подобная процедура заняла бы слишком много времени.
Существует два возможных решения для организации удаления записей. Первое - самое простое, которое используется в файлах данных dBASE. Для каждой записи в файле устанавливается префикс, состоящий из одного байта и содержащий флаг удаления. Флаг может быть булевым значением (true/fasle) или символом (например, 'Y'/'N' или '*'/пусто). При удалении записи устанавливается флаг удаления, который и будет говорить о том, что данная запись удалена. Все кажется достаточно простым, но что делать с удаленными записями? Вариант А - просто игнорировать. К сожалению, в этом случае в файле будет накапливаться все большее и большее число удаленных записей и в некоторый момент времени файл придется уплотнять, дабы избавиться от ненужных записей и уменьшить размер файла данных. Вариант В - повторно использовать место, занимаемое удаленными записями. При добавлении в файл новой записи по файлу выполняется поиск удаленной записи, на место которой и будет добавлена новая запись. Очевидно, что вариант В неэффективен. Представьте себе, что в файле, содержащем 10000 записей, удалена только одна запись. Для того чтобы найти всего одну удаленную запись, нам придется выполнить цикл, по крайней мере, по 5000 записям. Эта операция принадлежит к классу О(n), поэтому вариант В лучше не реализовывать.
Тем не менее, вариант В имеет и свои положительные стороны, в частности, повторное использование места, занимаемого удаленными записями. Если бы только нам удалось привести его к классу O(1)! Такие рассуждения привели к разработке еще одного метода удаления записей - цепочке уделенных записей (для этого метода наличие служебного блока данных обязательно, поэтому будем считать, что служебные данные присутствуют).
Перед каждой записью находится 4-байтный префикс - значение типа longint. Он предназначен для хранения флага удаления. Его нормальное значение -1 - значение, которое указывает, что запись не удалена. Любое другое значение будет означать, что запись удалена. Но это еще не все. Обратите внимание, что размер каждой записи увеличивается на 4 байта. В свою очередь, пользователь считает, что размер записи не изменился. В служебном заголовке хранится еще одно значение типа longint, которое представляет собой порядковый номер первой удаленной записи. Нормальное значение для этого поля -2, которое означает, что в файле нет удаленных записей.
Рисунок 2.3. Удаление записи