Работа с бинарными файлами в стиле STL
Работа с бинарными файлами — традиционная тема при обучении программированию — по меньшей мере, в России. Не последнюю роль здесь играет широкое распространение в российских школах языка программирования Pascal, который имеет встроенную поддержку работы с так называемыми типизированными файлами (типы file of integer , file of real и т. п.). В некоторых случаях (при углублённом изучении программирования у школьников или на младших курсах университета), когда начинается изучение языка C++, возникает желание решать задачи на обработку «файлов типа T» уже в новом окружении данного языка. И тут возникает вопрос, какие средства при этом использовать.
К сожалению, для работы с бинарными файлами в языке C++ предусмотрены только низкоуровневые средства — методы read / write стандартных типов потоков istream / ostream . Кроме других очевидных недостатков этот факт не даёт использовать в полную силу программирование «в стиле STL» (то есть, в первую очередь, часть стандартной библиотеки C++, связанную с алгоритмами и итераторами).
Итак, задача состоит в том, чтобы обеспечить работу с бинарными файлами, хранящими последовательность значений типа T , как с последовательностью STL ( vector<T> и т. п.). Для T подразумеваются значения базовых типов, а также, так называемые POD-типы (везде дальше можно думать только о базовых типах, если вы не знакомы с понятием POD).
Возможные решения
ios_base::binary: fail #0
Если вы никогда не встречались с подобной задачей (что было бы странно!), возможно, вы вспомните что-то про флаг ios_base::binary , но те, кто встречался, хорошо знает, что данное средство практически никак не поможет в решении. Стандарт языка предельно краток на тему того, какого эффекта можно ожидать от указания данного флага при открытии потока, но в сети можно встретить пояснение, что он просто отключает платформенно-зависимые трансляции символов перехода на новые строки и, возможно, ещё некоторых символов, что не имеет прямого отношения к нашей задаче.
Статический полиморфизм: fail #1
Вероятно, люди, знающие стандартную библиотеку чуть глубже, чем на базовом уровне, помнят, что стандартные типы для файловых потоков ofstream / ifstream являются синонимами для явных инстанций шаблонов basic_ofstream / basic_ifstream . Эти шаблоны имеют два аналогичных типовых параметра: тип символов потока (назовём этот параметр Ch ) и тип характеристик типа символов — по умолчанию это std::char_traits<Ch> . Для ofstream / ifstream в качестве Ch взят тип char .
Здесь немедленно возникает мысль попробовать инстанцировать эти шаблоны с тем типом T , который мы хотим читать из бинарного файла, указав его в качестве значения шаблонного параметра Ch . Простейший код, пытающийся читать из потока такого типа значения типа int падает с исключением времени выполнения bad_cast . Возможно, что-то можно было бы изменить, написав свою специализацию для char_traits<int> и передав её вторым параметром шаблону класса файлового потока, однако этот путь показался беспеперспективным (писать специализацию весьма обширного шаблона char_traits для каждого типа T …) и я не стал разбираться с ним далее.
ООП и динамический полиморфизм: fail #2
После первых неудач можно прийти к мысли, что получить нужное поведение «совсем бесплатно» из стандартных средств не получится и придётся написать кое-какой код. Попробуем решить эту задачу в парадигме ООП, то есть написав пару-другую своих классов. Классов потоков, естественно. Отнаследоваться у стандартных ofstream / ifstream , сохранив максимум определений из предков, и посмотреть, что получится. (В скобках замечу, что задача эта сама по себе не лишена смысла хотя бы ввиду того, что отмечена довольно высоким рейтингом сложности в списке упражнений из книги Б. Страуструпа — упр. 15, п. 21.10 в третьем и специальном изданиях книги «Язык программирования C++».)
С самого начала очевидной была необходимость перегрузки операций << и >> для своих классов потоков. Казалось, этого будет достаточно. Проблема возникла в следующем. Чтобы работать с потоком с помощью алгоритмов стандартной библиотеки, следует использовать итераторы ввода/вывода. По заявлениям авторов STL её средства все из себя обобщённые и я ожидал, что как только мой класс потока будет удовлетворять некоторым неявным требованиям библиотеки, она с радостью заработает с ним — статический полиморфизм… В частности, я ожидал, что стандартные итераторы параметризованы типом потока, с которым они работают. Не тут-то было! В определении шаблонов итераторов ввода/вывода жёстко зашиты стандартные типы basic_ofstream / basic_ifstream .
Надежда на спасение на этом пути остаётся, если обратить внимание на одну особенность реализации операций << и >> : для базовых типов они реализованы как функции-члены шаблонов классов потоков. Если бы они, кроме того, были объявлены виртуальными, то можно было бы положиться на динамический полиморфизм (стандартные итераторы хранили бы объекты моих потоков по ссылке на базовый класс) — получилось бы частичное решение исходной задачи, работавшее только для базовых типов (int, double и т. п.). Однако указанные функции-члены не являются виртуальными. Тут можно было бы порассуждать о логике устройства стандартной библиотеки или отсутствии таковой (например, известно, что для STL изначально не предполагалось использования всей мощи ООП, наследования и полиморфизма, но ведь потоковая библиотека построена на ООП…), однако перейдём к финальному решению.
Ad-hoc полиморфизм (перегрузка): win
В конце концов, всё, что требуется — вызывать специальные версии операций << и >> , которые бы прятали низкоуровневую работу с файлами посредством read / write . Достаточно предоставить свою перегрузку этих операций и позаботиться о том, чтобы вызывалась именно она. Достичь этого можно использованием специальных типов в аргументах. Манипулировать типами потоков у нас уже не получилось — остаётся придумать специальные типы для вводимого/выводимого. Здесь напрашивается использование того, что называется «обёртками».
К счастью, нам нет нужны писать новые классы обёрток для разных типов T : можно ограничиться одним шаблоном класса, хранящим поле параметра-типа T и умеющего преобразовываться к ссылке на это поле — константной и неконстантной. Конструктор с одним параметром типа T , который не объявлен как explicit , позволит неявно преобразовывать значения типа T к типу-обёртке. Конструктор без параметров — требование STL. Результирующий код приведён ниже.
Использование static_cast требует от компилятора вызвать определённую в теле шаблона класса операцию приведения типа для получения ссылки на информационное поле, а reinterpret_cast приводит адрес этого поля к указателю на char , готовя нас к низкоуровневой работе с read / write .
Вот пример, демонстрирующий использование обёртки. Он несёт на себе отпечаток тех идей, которые закладывались изначально, а именно, программирование в стиле STL.
Заключение
Понятно, что в результате получился «абсолютный велосипед», который, наверное, писался многими программистами на C++, однако в сети или в каких-то известных библиотеках (например, Boost, в частности, Boost.Iostreams) подобного я не заметил.
Ещё я хотел бы отметить, что намерено оставил за скобками обсуждение актуальности поставленной задачи. Наверное, есть люди, не представляющие работу с файлами на более высоком, чем read / write , уровне. Возможно, найдутся те, кто скажет, что бинарные файлы это прошлое, что они жутко непереносимы или что-то похожее. Может быть, отчасти это так, однако само упражнение в решении такой задачи мне показалось интересным и познавательным.
За постановку задачи и обсуждение решения я искренне благодарю Виталия Николаевича Брагилевского.
UPD1: в комментариях спросили, почему вместо преобразований типов не написать обычные функции-члены get. Преобразования по существу используются в примерах наподобие следующего.
Бинарные файлы
Т екстовые файлы хранят данные в виде текста (sic!). Это значит, что если, например, мы записываем целое число 12345678 в файл, то записывается 8 символов, а это 8 байт данных, несмотря на то, что число помещается в целый тип. Кроме того, вывод и ввод данных является форматированным, то есть каждый раз, когда мы считываем число из файла или записываем в файл происходит трансформация числа в строку или обратно. Это затратные операции, которых можно избежать.
Текстовые файлы позволяют хранить информацию в виде, понятном для человека. Можно, однако, хранить данные непосредственно в бинарном виде. Для этих целей используются бинарные файлы.
Выполните программу и посмотрите содержимое файла output.bin. Число, которое ввёл пользователь записывается в файл непосредственно в бинарном виде. Можете открыть файл в любом редакторе, поддерживающем представление в шестнадцатеричном виде (Total Commander, Far) и убедиться в этом.
Запись в файл осуществляется с помощью функции
Функция возвращает число удачно записанных элементов. В качестве аргументов принимает указатель на массив, размер одного элемента, число элементов и указатель на файловый поток. Вместо массив, конечно, может быть передан любой объект.
Запись в бинарный файл объекта похожа на его отображение: берутся данные из оперативной памяти и пишутся как есть. Для считывания используется функция fread
Функция возвращает число удачно прочитанных элементов, которые помещаются по адресу ptr. Всего считывается count элементов по size байт. Давайте теперь считаем наше число обратно в переменную.
fseek
Одной из важных функций для работы с бинарными файлами является функция fseek
Эта функция устанавливает указатель позиции, ассоциированный с потоком, на новое положение. Индикатор позиции указывает, на каком месте в файле мы остановились. Когда мы открываем файл, позиция равна 0. Каждый раз, записывая байт данных, указатель позиции сдвигается на единицу вперёд.
fseek принимает в качестве аргументов указатель на поток и сдвиг в offset байт относительно origin. origin может принимать три значения
- SEEK_SET — начало файла
- SEEK_CUR — текущее положение файла
- SEEK_END — конец файла. К сожалению, стандартом не определено, что такое конец файла, поэтому полагаться на эту функцию нельзя.
В случае удачной работы функция возвращает 0.
Дополним наш старый пример: запишем число, затем сдвинемся указатель на начало файла и прочитаем его.
Вместо этого можно также использовать функцию rewind, которая перемещает индикатор позиции в начало.
В си определён специальный тип fpos_t, который используется для хранения позиции индикатора позиции в файле.
Функция
используется для того, чтобы назначить переменной pos текущее положение. Функция
используется для перевода указателя в позицию, которая хранится в переменной pos. Обе функции в случае удачного завершения возвращают ноль.
возвращает текущее положение индикатора относительно начала файла. Для бинарных файлов — это число байт, для текстовых не определено (если текстовый файл состоит из однобайтовых символов, то также число байт).
Рассмотрим пример: пользователь вводит числа. Первые 4 байта файла: целое, которое обозначает, сколько чисел было введено. После того, как пользователь прекращает вводить числа, мы перемещаемся в начало файла и записываем туда число введённых элементов.
Вторая программа сначала считывает количество записанных чисел, а потом считывает и выводит числа по порядку.
Примеры
1. Имеется бинарный файл размером 10*sizeof(int) байт. Пользователь вводит номер ячейки, после чего в неё записывает число. После каждой операции выводятся все числа. Сначала пытаемся открыть файл в режиме чтения и записи. Если это не удаётся, то пробуем создать файл, если удаётся создать файл, то повторяем попытку открыть файл для чтения и записи.
2. Пишем слова в бинарный файл. Формат такой — сначало число букв, потом само слово без нулевого символа. Ели длина слова равна нулю, то больше слов нет. Сначала запрашиваем слова у пользователя, потом считываем обратно.
3. Задача — считать данные из текстового файла и записать их в бинарный. Для решения зачи создадим функцию обёртку. Она будет принимать имя файла, режим доступа, функцию, которую необходимо выполнить, если файл был удачно открыт и аргументы этой функции. Так как аргументов может быть много и они могут быть разного типа, то их можно передавать в качестве указателя на структуру. После выполнения функции файл закрывается. Таким образом, нет необходимости думать об освобождении ресурсов.
4. Функция saveInt32Array позволяет сохранить массив типа int32_t в файл. Обратная ей loadInt32Array считывает массив обратно. Функция loadInt32Array сначала инициализирует переданный ей массив, поэтому мы должны передавать указатель на указатель; кроме того, она записывает считанный размер массива в переданный параметр size, из-за чего он передаётся как указатель.
5. Создание таблицы поиска. Для ускорения работы программы вместо вычисления функции можно произвести сначала вычисление значений функции на интервале с определённой точностью, после чего брать значения уже из таблицы. Программа сначала производит табулирование функции с заданными параметрами и сохраняет его в файл, затем подгружает предвычисленный массив, который уже используется для определения значений. В этой программе все функции возвращают переменную типа Result, которая хранит номер ошибки. Если функция отработала без проблем, то она возвращает Ok (0).
6. У нас имеются две структуры. Первая PersonKey хранит логин, пароль, id пользователя и поле offset. Вторая структура PersonInfo хранит имя и фамилию пользователя и его возраст. Первые структуры записываются в бинарный файл keys.bin, вторые структуры в бинарный файл values.bin. Поле offset определяет положение соответствующей информации о пользователе во втором файле. Таким образом, получив PersonKey из первого файла, по полю offset можно извлечь из второго файла связанную с данным ключом информацию.
Зачем так делать? Это выгодно в том случае, если структура PersonInfo имеет большой размер. Извлекать массив маленьких структур из файла не накладно, а когда нам понадобится большая структура, её можно извлечь по уже известному адресу в файле.

Всё ещё не понятно? – пиши вопросы на ящик
Бинарный файл: определение, правила и особенности работы
![]()
Бинарный формат — это формат, при котором информация записана при помощи последовательности байт. Бинарным он называетс я п отому , что все записи внутри файла делаются только при помощи «1» и «0». Такой формат еще называют двоичным, что не является ошибкой.
Бинарный формат — это противопоставление текстовому формату. Условно любую информацию для компьютера можно записать либо в бинарном формате, либо в текстовом. Кстати, код, написанный на любом языке программирования , — это текстовый формат. Текстовый формат понятен людям, а бинарный формат понятен компьютерам. Но есл и к опнуть «глубже» в сравнени е текстового и бинарного формата и взглянуть с технической реализации, тогда можно выяснить, что текстовый формат, по сути, является разновидностью бинарного формата. Любой текстовый файл конвертируется в бинарный, для того чтобы его мог «прочитать» компьютер. А любому текстовому символу соответствует бинарное сочетание символов , п оэтому с технической стороны каждый формат, используемый в компьютере, является бинарным.
Что такое бинарный формат или файл
-
beye;
-
hiew;
-
WinHex;
-
и др.
-
теоретическ и р едактировать бинарные файлы можно;
-
есть алгоритмы, которые могут конвертировать бинарны й фай л «обратно» в исходный файл.
Заключение
Бинарный формат документа — это специфический формат, который применяется «внутри» компьютерных устройств. «Бинарный» или «двоичный» означает, что документ состоит из последовательности единиц и нулей. Чтение и редактирование бинарного файла возможны при помощи специальных редакторов. Однако, чтобы понять все , написанное двоичным кодом, нужно обладать соответствующими знаниями.
Мы будем очень благодарны
если под понравившемся материалом Вы нажмёте одну из кнопок социальных сетей и поделитесь с друзьями.
40.6.5. Работа с бинарными файлами
Под бинарными файлами понимаются файлы, в которых хранятся бинарные данные, т. е. данные, представленные двоичными кодами. Такие файлы обычно хранят набор записей, каждая из которых является копией содержимого структуры, определенной в прикладной программе. При работе с бинарными файлами в языке программирования C++ нужно придерживаться следующих двух правил:
поток, связанный с бинарным файлом, должен быть открыт в бинарном режиме;
510 Часть IV. Стандартная библиотека языка программирования С++
для доступа к данным, хранящимся в бинарном файле, нужно использовать не форматирующие операции ввода/вывода.