Работа с файлами в С++. Произвольный доступ. Ознакомление

Произвольный доступ к файлам тема достаточно простая, но тем не менее способная немного запутывать новичков. Путает людей то, что файл обрабатывается или в двоичном, или в текстовом режиме. Из-за небольшого различия обработки может случаться видимость нерабочести. Иногда непривычно или трудно понимать термин курсор, когда не видишь курсора как такового. И наличие seekp, seekg, tellp, tellg. Все эти мелочи в совокупности создают некоторого рода атмосферу неясности.
Произвольный доступ можно одинаково задействовать и для файлов, открытых в текстовом режиме, и для файлов, открытых в бинарном режиме.
В С++ используется две разновидности файловых курсоров: курсор чтения и курсор записи. В зависимости от действия: чтение из файла или запись в файл — выбирается seekp или seekg соответственно. В некоторых случаях они работают одинаково, но не стоит использовать их не по назначению. Кроме того, что можно принудительно установить курсор файла в любую произвольную позицию внутри файла, можно узнавать текущую позицию курсора. Здесь так же нужно разделять курсор чтения и курсор записи tellp и tellg.
  • Методы позиционирования различаются для входных и выходных потоков: для входных потоков имена методов заканчиваются символом 'g' (от слова get), а методы выходных потоков заканчиваются символом 'p' (от слова put).
seekg
установить позицию чтения

seekp
установить позицию записи

tellg
узнать позицию курсора чтения

tellp
узнать позицию курсора записи

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

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

Так, например, в ASCII легко получить различные коды из разного числа символов:

Символ
Код
Пояснение

#
35
код символа состоит из двух минимальных единиц информации: 3, 5

a
97
код символа состоит из двух минимальных единиц информации: 9, 7

x
120
код символа состоит из трёх минимальных единиц информации: 1, 2, 0

Важно помнить, что в С++ отсчёт начинается с нуля, поэтому в файлах, как и в индексах массивов, какая-нибудь 5-я позиция будет обозначать 6-й элемент:

Значение
A
B
C
D
E
F

Позиция
0
1
2
3
4
5

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

При работе с двоичными файлами не приходится думать о кодах символов, поэтому позиционирование в таких файлах выполняется относительно легко. Только в отличие от текстовых файлов нужно или использовать write/read, или перегружать операции >>/<<. Иначе — здравствуйте коды символов, сбивающие настройку позиционирования.

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

Числа в файле
5
6
7
8
9
10
11
12
13
14

Позиция числа
0
1
2
3
4
5

  • Важная составляющая удобного позиционирования — ровный шаг
  • Также важно внимательно смотреть на режим открытия файла. Легко забыть поменять текстовый на бинарный, что может создать иллюзию или нерабочести или неправильной работы.
Под неровным шагом понимается, что при прогулке по символьным кодам (символьные коды — это числа) для шага требуется когда 2, когда 3, а когда и другое число символов.
Будьте внимательны непосредственно к типам. Так, например, в листинге #1 в центре внимания тип char, он используется и в условии цикла, и для запоминания в файл, и для запоминания считанного из файла значения. А в листинге #2 в центре внимания оказывается тип int. Просто не запутайтесь, с чем работаете, здесь это совсем несложно.
Поскольку я именно читал произвольное значение из файла, то использовал seek с окончанием g: seekg. В случае необходимости изменения произвольного значения, следует использовать seekp.
Напишем код, который подменит каждое второе значение, сохранённое в файле, на 0 (ноль). Будем использовать бинарный режим. Это поможет нам избежать кривого шага и упростит решение. Чтобы не создавать страшную кашу, разобью поведение программы на отдельные функции: в зависимости от желания пользователя файл или будет создаваться новый, или читаться, или изменяться.

Чтобы посмотреть результат работы, для начала нужно создать файл (выбрать 1), потом можно посмотреть файл (выбрать 2), потом подменить хранимые данные (выбрать 3), потом снова посмотреть файл (выбрать 4). Пока что у меня сделано так, что программу для каждого действия надо каждый раз запускать заново. Если можете исправить — вперёд. Я же хочу акцентировать внимание на наиболее нужных частях кода, поэтому пока так.
Обращаю ваше внимание на проявление вашего внимания к каждой мелочи кода. Важно помнить, что файл можно затереть, если не открыть в правильном режиме. Такой результат плачевен. ofstream по умолчанию открывает файл в режиме trunc (усекает файл, эффект полного стирания). Чтобы этого избежать, можно добавить к режимам открытия ios::in, сиё действие отключит дерзкое поведение. Важно помнить, что для открытия файла в нужном режиме, при комбинациях режимов, используют не логические операции, а битовые. Ещё обязательно понимать, что смещение происходит не по числу, а по количеству байтов, количество байтов зависит от типа. Т. е. если я хочу сместиться, например, на три позиции, то правильный шаг будет

3 * sizeof(ЁМКОСТЬ_ТИПА)

Это обусловлено тем, что для разных типов разный шаг смещения. Чем шире тип — тем шире шаг.

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


К сожалению, есть во всём этом деле и подводные камни. Основная проблема — код платформозависим. Зависимость происход из совокупности задействованных механизмов. Достаточно знать, что тип int, как и многие другие типы, имеет изменчивую вместимость байтов (вместимость напрямую зависит от компьютера). Этого хватает, чтобы делать выводы о платформозависимости кода. Каждый компьютер может записывать файл по-своему. На разных компьютерах вполне можно ожидать получение неодинаковых файлов. Кроме этого, есть незнакомое новичкам влияние порядка следования сохранения байтов. Есть два варианта: слева-направо и справа-налево. Слева-направо называется big-indian, справа-налево называется little-indian. В некоторых случаях можно использовать платформонезависимые типы. Но в случае с порядком следования байт остаётся надеяться только на соглашение использования одинакового порядка следования байт (или слева-направо, или справа-налево). Давайте немного исправим нашу программу, задействовав платформонезависимый тип.

Сейчас я подменил платформозависимый тип int платформонезависимым типом uint8_t. Поскольку я записываю в файл числа без знака, то выбран беззнаковый тип, представляющий собой целые числа, вмещающий в себя 8 бит. Тут нужно учитывать, какие числа записываются в файл, для этого нужны некоторые знания информатики и понимание битов, байтов. Поскольку я записываю числа 5..14, а 8 бит без знака способны уместить в себя любые целые числа из диапазона 0..28 — 1, то мои числа принадлежат диапазону. Но в моём случае возникает необходимость приведения типов при выводе значения на экран. Когда я вывожу операцией << значение переменной типа uint8_t на экран, мне выводится не число, а символ. Так реализован тип uint8_t на моём компьютере: он является синонимом типа unsigned char, на вашем компьютере может быть иначе. Из-за этой детали приходится принудительно приводить значение к числу при выводе на экран. Как реализовано у вас, можно подсмотреть в файле stdint.h. Но если вы нацелены использовать файл на разных компьютерах, то для предупреждения неправильного вывода лучше насильно приведите выводимый тип к числу.
Есть несколько платформонезависимых целочисленных типов:

8 бит
16 бит
32 бита
64 бита

знаковые (signed)
int8_t
int16_t
int32_t
int64_t

беззнаковые (unsigned)
uint8_t
uint16_t
uint32_t
uint64_t

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

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

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

7 комментариев на «“Работа с файлами в С++. Произвольный доступ. Ознакомление”»

  1. Igor:

    почему в первом аргументе write или read идет такое выражение: (char*)&переменная ?? почему не просто переменная?

    • Наверное, потому что в зависимости от системы просто указатель может транслироваться как в число, так и в символ.
      Лично я без понятия, почему обязательно приводить адрес к Си-строке.

      &переменная — это указатель.

      Синтаксис пришёл из языка С. Функции read и write написаны так, что первым параметром им требуется указатель. А коли обычная переменная не может быть указателем, когда она не аргумент-неуказатель, то есть есть как есть и нужно отдавать функции указатель на char.

  2. Igor:

    а когда читаешь с одного файла в режиме binary и записываешь в другой файл в режиме binary, то визуально в текстовом файле должен быть текст или иероглифы?

    • Не понятен вопрос. Если читаешь то, что записано в текстовом режиме, и потом записываешь это в другой фай, то будет видно как текст.

  3. Igor:

    если читать и записывать все в бинарном режиме (с флагом ios::binary) с текстового файла в пустой, то во втором файле будет виден текст?

    • Как записано, так и будет видно.

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

  4. Igor:

    спс, понял что будет виден текст

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

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

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/vitaliy-potopahin/turbo-pascal-reshenie-slozhnyh-zadach-2/?lfrom=15589587
Яндекс.Метрика