Как создать бинарный файл в c
Для работы с бинарными файлами предназначена пара классов BinaryWriter и BinaryReader . Эти классы позволяют читать и записывать данные в двоичном формате.
BinaryWriter
Для создания объекта BinaryWriter можно применять ряд конструкторов. Возьмем наиболее простую:
в его конструктор передается объект Stream (обычно это объект FileStream).
Основные методы класса BinaryWriter
Close() : закрывает поток и освобождает ресурсы
Flush() : очищает буфер, дописывая из него оставшиеся данные в файл
Seek() : устанавливает позицию в потоке
Write() : записывает данные в поток. В качестве параметра этот метод может принимать значения примитивных данных:
Либо можно передать массивы типов byte и char
При записи массива дополнительно можно указать, с кого элемента массива надо выполнять запись, а также число записываемых элементов массива:
Write(byte[], int, int)
Write(char[], int, int)
Рассмотрим простейшую запись бинарного файла:
Здесь в файл person.dat записываются два значения: строка «Tom» и число 37. Для создание объекта применяется вызов new BinaryWriter(File.Open(path, FileMode.OpenOrCreate))
Подобным образом можно сохранять более сложные данные. Например, сохраним в файл массив объектов:
В данном случае последовательно сохраняем в файл people.dat данные объектов Person из массива people.
BinaryReader
Для создания объекта BinaryReader можно применять ряд конструкторов. Возьмем наиболее простую версию:
в его конструктор также передается объект Stream (также обычно это объект FileStream).
Основные методы класса BinaryReader
Close() : закрывает поток и освобождает ресурсы
ReadBoolean() : считывает значение bool и перемещает указатель на один байт
ReadByte() : считывает один байт и перемещает указатель на один байт
ReadChar() : считывает значение char, то есть один символ, и перемещает указатель на столько байтов, сколько занимает символ в текущей кодировке
ReadDecimal() : считывает значение decimal и перемещает указатель на 16 байт
ReadDouble() : считывает значение double и перемещает указатель на 8 байт
ReadInt16() : считывает значение short и перемещает указатель на 2 байта
ReadInt32() : считывает значение int и перемещает указатель на 4 байта
ReadInt64() : считывает значение long и перемещает указатель на 8 байт
ReadSingle() : считывает значение float и перемещает указатель на 4 байта
ReadString() : считывает значение string. Каждая строка предваряется значением длины строки, которое представляет 7-битное целое число
С чтением бинарных данных все просто: соответствующий метод считывает данные определенного типа и перемещает указатель на размер этого типа в байтах, например, значение типа int занимает 4 байта, поэтому BinaryReader считает 4 байта и переместит указатель на эти 4 байта.
Например, выше в примере с BinaryWriter в файл person.dat записывалась строка и число. Считаем их с помощью BinaryReader:
Конструктор класса BinaryReader также в качестве параметра принимает объект потока, только в данном случае устанавливаем в качестве режима FileMode.Open: new BinaryReader(File.Open(«person.dat», FileMode.Open)) .
В каком порядке данные были записаны в файл, в таком порядке мы их можем оттуда считать. То есть если сначала записывалась строка, а потом число, то в данном порядке мы их можем считать из файла.
Или подобным образом считаем данные из файла people.dat, который был записан в примере выше и который содержит данные объектов Person:
Здесь в цикле while считываем данные. Чтобы узнать окончание потока, вызываем метод PeekChar() . Этот метод считывает следующий символ и возвращает его числовое представление. Если символ отсутствует, то метод возвращает -1, что будет означать, что мы достигли конца файла.
В цикле последовательно считываем значения для свойств объектов Person в том же порядке, в каком они записывались.
Класс 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() используется для очистки всех буферов текущего модуля записи. После этого можно записывать любые буферизированные данные на устройство, в которое на данный момент разрешено производить запись.
40.6.5. Работа с бинарными файлами
Под бинарными файлами понимаются файлы, в которых хранятся бинарные данные, т. е. данные, представленные двоичными кодами. Такие файлы обычно хранят набор записей, каждая из которых является копией содержимого структуры, определенной в прикладной программе. При работе с бинарными файлами в языке программирования C++ нужно придерживаться следующих двух правил:
поток, связанный с бинарным файлом, должен быть открыт в бинарном режиме;
510 Часть IV. Стандартная библиотека языка программирования С++
для доступа к данным, хранящимся в бинарном файле, нужно использовать не форматирующие операции ввода/вывода.
Чтение и запись бинарных файлов в C#
Для работы с бинарными файлами в C# можно использовать как обычный FileStream , так и более удобные для этих целей классы, а именно — Binary Writer и Binary Reader . Эти классы, содержат достаточно большое количество перегруженных версий методов, позволяющих записывать и считывать различные данные различных типов в бинарные файлы.
Запись бинарных файлов в C# (класс BinaryWriter)
Для создания экземпляра класса BinaryWriter мы можем воспользоваться одним из следующих конструкторов:
BinaryWriter() Инициализирует новый экземпляр класса BinaryWriter , который осуществляет запись в поток. BinaryWriter(Stream) Инициализирует новый экземпляр класса BinaryWriter на основании указанного потока с использованием кодировки UTF-8. BinaryWriter(Stream, Encoding) Инициализирует новый экземпляр класса BinaryWriter на основе указанного потока и кодировки символов. BinaryWriter(Stream, Encoding, Boolean) Инициализирует новый экземпляр класса BinaryWriter на основании указанного потока и кодировки символов, а также при необходимости оставляет поток открытым. Для работы с бинарными данными у класса BinaryWriter определен ряд методов, которые имеют перегруженные версии:
Метод Описание Flush() Очищает все буферы текущего модуля записи и вызывает немедленную запись всех буферизованных данных на базовое устройство. Seek(Int32, Seek Origin) Задает позицию в текущем потоке. См. работу аналогичного метода у FileStream . Write() Записывает значение различных типов в поток. Определены методы для записи чисел, символов, строк, значений boolean и т.д. Write7BitEncodedInt(Int32) Записывает 32-разрядное целое число в сжатом формате. Так как мы рассматриваем работу с бинарными файлами, то запишем в файл, например, объект такого класса:
Бинарные файлы, обычно, имеют строго определенную структуру, благодаря чему, мы можем получать доступ к определенной части данных и считывать или записывать их. В нашем классе «тонким» местом является имя. У разных людей длина имени различна — от одного символа до десятков символов и более. Соответственно, чтобы мы могли точно устанавливать позицию в файле для чтения данных, записывать этот файл мы должны строго по определенному формату. Для этого договоримся, что длина имени человека не должна превышать 100 символов, а если, друг, имя превысит 100 символов то оно будет обрезаться.
Таким образом мы получаем следующий формат бинарного файла:
- первые 100 байт файла отведены для имени ( Name )
- далее 4 байта для возраста ( Age )
- далее 8 байт для веса ( Weight )
Итого, после записи одного объекта в файл его размер должен стать ровно 100+4+8 = 112 байт. Вот как может выглядеть запись объекта в файл в C#:
Вспомогательная функция CopyBytes создает массив ровно на 100 байт. После записи мы получим файл размером 112 байт. Так как нам известен формат файла, то мы можем таким образом записать в файл, например, не один, а 15 объектов и, при необходимости, получить доступ к любому из объектов.
Так как строка само по себе хранит свою длину, то, в принципе, представленный выше пример можно упростить и не создавать массив на 100 байт, а писать в файл сразу строку. Правда, при этом, на каждый объект будет выделяться различное количество памяти. Например, объект представленный выше, если его записать в файл следующим образом:
Будет занимать не 112 байт, как ранее, а всего 51, но, при этом мы уже не сможем точно позиционировать курсор на необходимом нам объекте, если будем записывать в файл несколько объектов одного класса.
Чтение бинарных файлов в C# (класс BinaryReader)
Для чтения бинарного файла мы можем использовать класс BinaryReader , которые, как и предыдущий класс BinaryWriter содержит ряд специальных методов, но только для чтения данных из файла. Конструкторы класса BinaryReader — точная копия конструкторов BinaryWriter :
Binary Reader(Stream) Инициализирует новый экземпляр класса BinaryReader на основании указанного потока с использованием кодировки UTF-8. Binary Reader(Stream, Encoding) Инициализирует новый экземпляр класса BinaryReader на основе указанного потока и кодировки символов. Binary Reader(Stream, Encoding, Boolean) Инициализирует новый экземпляр класса BinaryReader на основании указанного потока и кодировки символов, а также при необходимости оставляет поток открытым. Основные методы для чтения бинарных данных представлены в таблице ниже:
Метод Описание Fill Buffer(Int32) Заполняет внутренний буфер указанным количеством байтов, которые были cчитаны из потока. Peek Char() Возвращает следующий доступный для чтения символ, не перемещая позицию байта или символа вперед. Read() Выполняет чтение знаков из базового потока и перемещает текущую позицию в потоке вперед в соответствии с используемым значением Encoding и конкретным знаком в потоке, чтение которого выполняется в настоящий момент. Read(Byte[], Int32, Int32) Считывает указанное количество байтов из потока, начиная с заданной точки в массиве байтов. Read(Char[], Int32, Int32) Считывает указанное количество символов из потока, начиная с заданной точки в массиве символов. Read7Bit Encoded Int() Считывает 32-разрядное целое число в сжатом формате. Read Boolean() Считывает значение Boolean из текущего потока и перемещает текущую позицию в потоке на один байт вперед. Read Byte() Считывает из текущего потока следующий байт и перемещает текущую позицию в потоке на один байт вперед. Read Bytes(Int32) Считывает указанное количество байтов из текущего потока в массив байтов и перемещает текущую позицию на это количество байтов. Read Char() Считывает следующий знак из текущего потока и изменяет текущую позицию в потоке в соответствии с используемым значением Encoding и конкретным знаком в потоке, чтение которого выполняется в настоящий момент. Read Chars(Int32) Считывает указанное количество символов из текущего потока, возвращает данные в массив символов и перемещает текущую позицию в соответствии с используемой Encoding и определенным символом, считываемым из потока. Read Decimal() Считывает десятичное значение из текущего потока и перемещает текущую позицию в потоке на шестнадцать байтов вперед. Read Double() Считывает число с плавающей запятой длиной 8 байт из текущего потока и перемещает текущую позицию в потоке на восемь байт вперед. Read Int16() Считывает целое число со знаком длиной 2 байта из текущего потока и перемещает текущую позицию в потоке на два байта вперед. Read Int32() Считывает целое число со знаком длиной 4 байта из текущего потока и перемещает текущую позицию в потоке на четыре байта вперед. Read Int64() Считывает целое число со знаком длиной 8 байта из текущего потока и перемещает текущую позицию в потоке на восемь байтов вперед. Read SByte() Считывает из текущего потока байт со знаком и перемещает текущую позицию в потоке на один байт вперед. Read Single() Считывает число с плавающей запятой длиной 4 байта из текущего потока и перемещает текущую позицию в потоке на четыре байта вперед. Read String() Считывает строку из текущего потока. Строка предваряется значением длины строки, которое закодировано как целое число блоками по семь битов. Read UInt16() Считывает целое число без знака длиной 2 байта в формате с прямым порядком байтов из текущего потока и перемещает текущую позицию в потоке на два байта вперед. Read UInt32() Считывает целое число без знака длиной 4 байта из текущего потока и перемещает текущую позицию в потоке на четыре байта вперед. Read UInt64() Считывает целое число без знака длиной 8 байт из текущего потока и перемещает текущую позицию в потоке на восемь байтов вперед. Чтобы прочитать наш файл, созданный без использования буфера на 100 байт, можно использовать следующий код:
Если же мы будем придерживаться заданного формата и записывать имя как массив байт, то чтение очередного объекта из файла можно произвести следующим образом:
Так как при записи массива с именем в файл все «пустые» элементы массива «забиваются» нулями, то здесь метод ReadString() определяет последний значащий (не равный нулю) символ в переданном ему массиве и создает из полученного массива строку в заданной кодировке.
Произвольный доступ к объектам в бинарном файле
В заключении рассмотрим пример того, как, используя заданный формат бинарного файла можно получить доступ к произвольному объекту в файле. Полный исходный код приложения:
Рассмотрим это пример подробно. Во-первых, здесь мы записываем в файл массив из трех объектов типа Person . Запись производится в методе WriteObjectToFile :
Код метода CopyBytes был представлен выше. После того, как файл записан, а все ресурсы, используемые объектом BinaryWriter и потоком FileStream освобождены, мы создаем объект типа BinaryReader для чтения файла и первым делом определяем количество объектов в файле:
Так как размер, занимаемый одним объектом нам точно известен (112 байт), то рассчитать количество объектов в файле не составляет никакого труда. Далее, мы просим пользователя ввести номер объекта который необходимо прочитать. Если номер оказывается допустимым (он менее или равен количеству объектов в файле), то вызывается метод чтения конкретного объекта из файла — ReadObjectFromFile :
здесь мы устанавливаем курсор в потоке на необходимую позицию и последовательно считываем все поля объекта. После того, как объект прочитан — выводим его пользователю в консоль. Результат может быть, например, таким:
Представленный выше пример, конечно, можно улучшить, определить более экономичный, с точки зрения потребления места на диске, формат и так далее. Здесь же я просто привел один из вариантов того, как можно осуществить произвольный доступ к объектам, записанным в бинарном файле, используя BinaryReader .
Итого
Для доступа к данным в бинарных файлах удобно использовать классы BinaryWriter и BinaryReader . Эти классы содержат специальные методы, позволяющие без дополнительных преобразований записывать и считывать строки, числа и другие данные из файлов. Используя заданный формат бинарного файла можно организовать произвольный доступ к различным частям бинарного файла для считывания/записи данных.
- 4.1. Метод Write() — много перегруженных реализаций