Как записать структуру в бинарный файл c
Хотя функции getc()/putc() позволяют вносить в файл отдельные символы, но фактически мы имеем дело с бинарными файлами. Если мы записываем в файл строку, то в принципе мы даже можем открыть записанный файл любом текстовом редакторе и понять, что там было записано. Но не всегда данные могут представлять строки. И чтобы более наглядно разобраться с работой с бинарными файлами, рассмотрим еще одни пример — с записью-чтением структуры из файла.
В данном случае запись и чтение структуры выделены в отдельные функции: save() и load() соответственно.
Для записи в функции save() через параметр struct person *p получаем указатель на сохраняемую структур. Фактически его значением является начальный адрес блока памяти, где располагается структура.
Функция putc записывает отдельный символ в файл, однако нам надо записать структуру. Для этого мы создаем указатель на символ (который по сути представляет один байт) и устанавливаем этот указатель на начало блока памяти, выделенного для структуры.
То есть в данном случае мы получаем адрес в памяти первого байта из блока памяти, которая выделена для структуры. И затем мы можем пройтись по всему этому блоку и получить отдельные байты и занести их в файл:
И в данном случае нам не важно, какие поля имеет структура, какой она имеет размер. Мы работаем с ней как с набором байт и заносим эти байты в файл. После занесения каждого отдельного байта в файл указатель c в блоке памяти перемещается на один байт вперед.
При чтении файла в функции load() используется похожий принцип только в обратную сторону.
Во-первых, для считывания структуры из файла мы выделяем блок динамической памяти для хранения прочитанных данных:
После этого указатель ptr будет указывать на первый адрес блока из 20 байт (наша структура занимает 20 байт = 16 символов и 4 байта для числа int ).
Затем так как при прочтении мы получаем символы, устанавливаем указатель на первый байт выделенного блока и в цикле считываем данные из файла в этот блок:
Здесь стоит обратить внимание на то, что в данном случае на самом деле считываем даже не символ, а числовой код символа в переменную типа int и только потом передаем значение указателю c. Это сделано для корректной обработки окончания файла EOF. Это значение может представлять любое отрицательное число. И если бы мы сохранили отрицательное число (например, возраст пользователя был бы отрицательным), то оно было бы некорректно интерпретировано при чтении как конец файла, и итоговый результа был бы неопределенным. Поэтому более правильно считывать именно числовой код символа в переменную int, а затем числовой код передавать в char.
Запись и чтение массива структур
Выше приведен пример по работе с одной структурой. Но, как правило, при работе с файлами мы оперируем не одной структурой, а каким-то набором структур. Поэтому усложним задачу и сохраним и считаем из файла массив структур:
Данная задача усложнена тем, что нам надо хранить массив структур, количество которых точно может быть неизвестно. Один из вариантов рещения этой проблемы состоит в сохранении некоторой метаинформации о файле в начале файла. В частности, в данном случае в начале файла сохраняется число записанных структур.
Запись во многом аналогична записи одной структуры. Сначала устанавливаем указатель на число n , которое представляет количество структур, и все байты этого числа записываем в файл:
Затем подобным образом записываем все байты из массива структур — устанавливаем указатель на первый байт массива структур и записываем size байт в файл:
При чтении нам придется файктически считывать из файла два значения: количество структур и их массив. Поэтому при чтении два раза выделяется память. Вначале для количества элементов:
How to read / write a struct in Binary Files?
I am facing a small problem. I have a struct, which has a vector. Note that the vector is dynamic per every iteration. Now, in a particular iteration, how do I store the struct which contains a vector of size n to a binary file?
Also, when retrieving, assume that I know how the size of the vector, how to I retrieve from the binary file, the struct variable containing the vector of all the stored elements?
I am able to store something to the binary file (as I can see the size increasing when writing), but when I am trying to retrieve back the elements, I am getting size of vector to be zero.
Unfortunately, I have to achieve this using the standard STL and not use any third-party libraries.
![]()
2 Answers 2
You should have a look at Boost Serialization.
If you can’t use 3rd party libraries, you must know that C++ doesn’t support serialization directly. This means you’ll have to do it yourself.
This article shows a nice way of serializing a custom object to the disk and retrieving it back. And this tutorial shows you how to get started right now with fstream.
This is my attempt:
EDIT: since the OP asked how to store/retrieve more than record I decided to updated the original code.
So, what changed? Now there’s an array student_t apprentice[3]; to store information of 3 students. The entire array is serialized to the disk and then it’s all loaded back to the RAM where reading/searching for specific records is possible. Note that this is a very very small file (84 bytes). I do not suggest this approach when searching records on huge files.
Outputs:
Dump of the binary file:
![]()
You typically serialize a vector by writing the length of the vector, followed by that number of elements. When you read it back in, having the length come first lets you know how many more items to read as part of that vector. As a simple first approximation, consider something like this:
This is open to lots of tweaks, improvements, and simple modifications — just for example, for the moment, I’ve passed the vector (or whatever — could be a std::deque, etc.) by reference rather than passing iterators. That probably simplifies most use, but doesn’t fit as well with the rest of the library.
This also serializes in text format, one number per line. Discussions comparing text to binary have happened before, so I won’t try to repeat all the arguments here — I’ll just note that the same basic idea can be done in binary format just as well as text.
Как записать структуру в бинарный файл c
БлогNot. Структура со строками string и файловые чтение/запись массива таких структур
Структура со строками string и файловые чтение/запись массива таких структур
В отличие от этого примера, используем в структурном типе данных более удобные в обращении и современные строки string и файловые потоки вместо классических си-строк char * и файлов из <cstdio> . Лекции по всем этим темам можно найти в оглавлении.
В итоге должна получиться программка, которая покажет непосредственное задание значений полям структурной переменной и ввод значений полей с консоли, а также запись файла структур и последующее его контрольное чтение. Проверяться она будет в консоли Visual Studio 2015, проект создан как вот здесь.
В начале файла укажем нужные библиотеки и директивы, в комментариях написано, для чего какая служит:
Опишем структурный тип данных, включающий в себя идентификатор (номер) записи, две строки для хранения имени и даты рождения, а также вещественное поле money для хранения, например, зарплаты:
Мы не указываем какие-либо ограничения на длину строки, потому что сами будем управлять этим.
Так как программа может завершиться тремя способами — нормально, при ошибке записи файла и при ошибке его чтения, напишем функцию error с аргументом n (номер ошибки), которая будет за это отвечать. Ошибка номер ноль, как принято в C++, будет означать нормальное завершение. Вообще такую «общую точку выхода» зачастую полезно делать в процедурно-ориентированном коде:
Функция для ввода данных с консоли input получает аргументами адрес структуры a , куда нужно заносить данные (это может быть, в том числе, и адрес элемента массива структур) и идентификатор записи i , остальные поля записи она запрашивает у пользователя.
В реальности стоило бы снабдить код большим количеством проверок корректности данных и вводом их в некую буферную запись, откуда потом скорректированные данные могут быть скопированы в массив или в файл.
Также обратите внимание, что мы не должны «резать» поля структуры кодом вроде
но с отдельной буферной строкой имели бы на это право, конечно же, прежде, чем копировать её в запись, предназначенную для постоянного хранения.
Со всеми этими оговорками, простейшая функция ввода записи получилась такой:
Функция output , соответственно, выводит в консоль поля записи a , переданной аргументом:
При корректных данных должны получиться столбцы правильной ширины.
Главной программе осталось русифицировать консоль и создать массив записей, для простоты предусмотрим там всего 2 элемента:
Первый элемент с id , равным нулю, зададим программно, а второй введём с консоли:
Откроем файл notes.dat для записи и поместим туда данные, заодно печатая их в консоль:
Обратите внимание, что применение sizeof к составному объекту string некорректно, приходится писать в бинарный файл отдельными полями.
После закрытия файла, откроем его вновь для чтения и покажем прочитанные в цикле записи на экране, после чего можно сделать нормальный выход из программы:
Вы понимаете, что наш файл — двоичный, и открывать его текстовым редактором бессмысленно.
Вот лог работы нашей программы:
скриншот 16-ричного вида файла (см. комменты)
- писать и читать только string.c_str() в бинарные файлы, сведя задачу к предыдущей ссылке (но тогда не нужны и string , а можно делать классически на char * );
- всё же отказаться от бинарных, а ограничиться текстовыми файлами и парсить полученные через getline строки;
- предусмотреть в формате файла одновременное сохранение длины строковых данных объекта string .
В последнем случае имеем примерно такой подход (проверен не на структурах, а на паре строк).
Программа для записи набора string в бинарный файл:
Потом читаем это (возможно, с точностью до кодировки):
В моей консоли Visual Studio 2015 обе программки сработали. Итак, если строка string является полем структуры, разницы в подходах нет.
P.P.S. Ну и, раз пошла такая пьянка, теперь разрешим строкам string в массиве структур иметь произвольную длину и содержать пробелы (по умолчанию чтение string из файлового потока прервётся на первом пробельном символе), а сохранять всё будем в текстовом файле, и читать из него же.
Чтобы всё работало, нам пришлось переписать оператор >> для своей структуры.
Программа создаст и затем прочитает такой файл:
При чтении код сам избавится от лишних пробелов в строках между лексемами, следующими после числа.
Класс BinaryWriter . Работа с бинарными файлами
Перед изучением данной темы рекомендуется ознакомиться со следующими темами:
Содержание
- 1. Класс BinaryWriter . Назначение
- 2. Взаимодействие класса BinaryWriter с потоками опорных хранилищ
- 3. Конструкторы класса BinaryWriter . Создание экземпляра
- 4. Основные методы класса BinaryWriter
- 4.1. Метод Write() — много перегруженных реализаций
- 4.1.1. Запись одиночных значений. Перегруженный метод Write() . Пример
- 4.1.2. Запись байтовых массивов byte[] в поток. Пример
- 4.1.3. Запись символьных массивов char[] в поток. Пример
Поиск на других ресурсах:
1. Класс BinaryWriter . Назначение
Класс BinaryWriter предназначен для записи данных в двоичном (бинарном) формате. Запись данных может осуществляться в файлы, сеть, изолированное хранилище, память и т.п. Во время записи строк существует возможность указания нужной кодировки. По умолчанию установлена кодировка UTF-8.
Класс реализован в пространстве имен System.IO . Для того, чтобы использовать возможности этого класса нужно добавить строкуКласс BinaryWriter записывает данные, которые представлены:
- примитивными типами: int , float , double и другими;
- строками типа string в указанной кодировке.
2. Взаимодействие класса BinaryWriter с потоками опорных хранилищ
Класс BinaryWriter (а также BinaryReader ) относится к адаптерам потоков. Это означает следующее. Чтобы получить доступ к файлу, сети или памяти, нужно использовать промежуточный класс опорного хранилища ( FileStream , MemoryStream , NetworkStream и т.д.).
На рисунке отображено взаимодействие класса BinaryWriter с файловым хранилищем, которому соответствует класс FileStream .Рисунок. Взаимодействие класса BinaryWriter с файлом через класс FileStream
3. Конструкторы класса BinaryWriter . Создание экземпляра
Класс BinaryWriter имеет несколько конструкторов, наиболее распространенными из которых следующие:
- output – ссылка на абстрактный класс Stream , являющийся вершиной иерархии классов ввода-вывода;
- encoding – система кодировки (Unicode, UTF32, UTF8 или другая). По умолчанию установлена система кодировки UTF8.
Пример. Для того, чтобы создать экземпляр класса BinaryWriter и связать его с файлом «abc.bin» нужно выполнить приблизительно следующий код.
Как видно из примера, при создании потоков используется синтаксис с использованием ключевого слова using() . Это освобождает от необходимости закрывать потоки методом Close() , поскольку при таком подходе очистка ресурсов происходит автоматически.
4. Основные методы класса BinaryWriter
4.1. Метод Write() — много перегруженных реализаций
Главный метод класса BinaryWriter — метод Write() . Этот метод позволяет записывать в поток данные всех примитивных (стандартных) типов.
Метод имеет 20 перегруженных реализаций, основные из которых приведены ниже.
4.1.1. Запись одиночных значений. Перегруженный метод Write() . Пример
Чтобы записать одиночные значения используются методы Write() , которые имеют следующую общую форму
здесь value – значение одного из стандартных (примитивных) типовв, которое нужно записать в поток.
Пример.
В примере в файл «data.bin» записываются данные разных типов с помощью метода Write() . Затем эти данные считываются с целью контроля.
Результат выполнения программы
4.1.2. Запись байтовых массивов byte[] в поток. Пример
Запись байт массивов типа byte[] полезен, когда в поток нужно записывать массивы стандартных типов (например, массивы int[] , double[] , decimal[] и т.д.). Для конвертирования из стандартных типов в тип byte[] удобно использовать возможности класса BitConverter .
- buffer – участок памяти, из которого будут записываться данные в поток побайтно;
- index – позиция (индекс) начала в массиве buffer , с которой будет происходить запись в поток;
- count – количество байт, которые нужно записать в поток.
Пример. В примере, с помощью метода Write() в файл «array.dat» записывается массив чисел типа double[] . Затем этот массив считывается и выводится на экран для сверки.
Результат выполнения программы
4.1.3. Запись символьных массивов char[] в поток. Пример
Для записи строк символов в виде массива char[] используются две следующие реализации метода Write() :
- chars – массив символов, который нужно записать в поток;
- index — позиция первого символа в массиве chars , начиная с которой нужно записать данные;
- count — количество символов, которые нужно прочитать из массива chars и записать в поток.
Пример. В примере записывается строка типа string в файловый поток в виде символьного массива char[] .
Результат выполнения программы
4.2. Метод Seek() . Пример
Метод Seek() устанавливает позицию в текущем потоке. Общая форма метода следующая:
- offset — смещение в байтах относительно позиции origin ;
- origin — значение, указывающее начальную точку, с которой должна быть получена новая позиция.
Значение origin есть перечислением и имеет тип
Значения из перечисления могут быть следующими:
- SeekOrigin.Begin — начальная точка отсчета устанавливается на начало потока (файла);
- SeekOrigin.End — начальная точка отсчета устанавливается в конец потока (файла);
- SeekOrigin.Current — определяется текущая позиция в потоке (файле).
Пример. В примере продемонстрированы следующие операции:
- запись чисел в файл;
- перезапись ранее записанных чисел;
- дописывание числа в конец файла.
Результат выполнения программы
4.3. Метод Flush() . Пример
Метод Flush() используется для очистки всех буферов текущего модуля записи. После этого можно записывать любые буферизированные данные на устройство, в которое на данный момент разрешено производить запись.
- 4.1. Метод Write() — много перегруженных реализаций