Бинарный режим доступа. Функции fwrite() и fread()
На этом занятии мы рассмотрим бинарный режим доступа к файлам.
До сих пор на предыдущих занятиях файлы открывались в текстовом режиме, который подразумевает запись и чтение текстовой информации. Соответственно, все рассмотренные функции, как правило, используются именно для текстовых данных, хотя их можно применять и в бинарном режиме они будут работать абсолютно также.
Так что же такое бинарный режим и чем он отличается от текстового? Давайте представим, что нам в программе нужно сохранять в файл значения различных переменных, например, таких:
int var_i = -10; double pi = 3.141592653589793; char ch = 'S';
Если использовать текстовый формат представления, то придется эти числа прямо в таком виде записать в выходной файл, например, с помощью функции fprintf():
#include int main(void) { int var_i = -10; double pi = 3.141592653589793; char ch = 'S'; FILE* fp = fopen("my_file.txt", "w"); if(fp == NULL) { perror("my_file.txt"); return 1; } fprintf(fp, "%d; %f; %c\n", var_i, pi, ch); fclose(fp); return 0; }
В файле увидим следующую информацию:
Обратите внимание, вещественное число pi представлено в усеченном виде. Конечно, это можно было бы поправить, но, очевидно, создает некоторые неудобства. Кроме того, при обратном считывании этих данных с помощью функции fscanf(), придется текстовое представление переводить в соответствующее числовое. А это дополнительное процессорное время.
Выйти из этой ситуации можно, если данные в файле воспринимать подобно ячейкам памяти. И хранить переменные так же, как они хранятся в памяти устройства: 4 байта для типа int; 8 байтов для типа double; 1 байт для типа char. В сумме получился бы файл размером 4+8+1 = 13 байт, что, во-первых, меньше текстовой записи и, во-вторых, данные не нужно преобразовывать в текстовый формат. Это и есть пример бинарного режима записи и чтения данных. В общем случае, отличие бинарного режима от текстового в том, что функции чтения и записи данных читают (или записывают) каждый байт без искажений и пропусков, какое бы значение он ни принимал. В текстовом режиме некоторые символы могут не читаться. Например, символ возврата каретки ‘\r’ игнорируется при чтении и записи. Понятно, что в бинарном режиме такое недопустимо, поэтому все данные заносятся и считываются без искажений один в один.
Давайте посмотрим, как можно включить и использовать бинарный режим. Вначале, при открытии файлового потока, конечно же, нужно прописать букву ‘b’ для параметра mode функции fopen():
FILE* fp = fopen("my_file.txt", "wb");
Надо заметить, что в ОС Unix файлы сразу открываются в бинарном режиме, то есть, различия между текстовым и бинарным доступом там нет. Но в других ОС это может быть не так. Например, в ОС Windows – это два разных режима и буква ‘b’ строго обязательна для включения бинарного режима.
Функции fwrite() и fread()
После того, как файл открыт на запись в бинарном режиме, нам нужно в него записать значения фрагментов ячеек, которые занимают переменные var_i, pi и ch в оперативной памяти. Уже известные нам функции не очень подходят, т.к. они «заточены» на работу со строками, а не произвольными данными. Но в языке Си есть еще две полезные функции, которые, как раз, удобны для работы в бинарном режиме:
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);
size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);
Эти функции принимают указатель ptr произвольного типа на область данных, которую следует записать или, в которую нужно занести данные из файла. Вторым параметром size указывается размер порции данных (например, это может быть размер элемента массива). Следующий параметр nmemb – число порций данных размером size. То есть, всего функция прочитает или запишет объем, равный size * nmemb байт. Последний параметр – это файловый поток. Обе функции возвращают число успешно прочитанных или записанных порций данных.
Давайте воспользуемся функцией fwrite() для записи наших переменных в выходной файл. Получим:
fwrite(&var_i, sizeof(var_i), 1, fp); fwrite(&pi, sizeof(pi), 1, fp); fwrite(&ch, sizeof(ch), 1, fp);
Здесь все достаточно очевидно. Передается адрес переменной, затем, ее размер и количество таких переменных на тот случай, если бы у нас был, например, массив. После выполнения программы файл my_file.txt составляет всего 13 байт.
Давайте теперь прочитаем записанные данные и убедимся, что они будут в точности равны исходным. Сделать это можно следующим образом:
#include int main(void) { int r_var_i; double r_pi; char r_ch; FILE* in = fopen("my_file.txt", "rb"); if(in == NULL) { perror("my_file.txt"); return 1; } fread(&r_var_i, sizeof(r_var_i), 1, in); fread(&r_pi, sizeof(r_pi), 1, in); fread(&r_ch, sizeof(r_ch), 1, in); fclose(in); printf("r_var_i = %d, r_pi = %f, r_ch = %c\n", r_var_i, r_pi, r_ch); return 0; }
После выполнения программы увидим результат:
r_var_i = -10, r_pi = 3.141593, r_ch = S
Переменные равны тем значениям, которые ранее были записаны в файл. При этом данные не пришлось конвертировать в текстовый вид. В этом преимущество бинарного режима доступа.
Но это очень простой пример. Давайте рассмотрим что-нибудь более сложное и практичное.
Пусть у нас в программе объявлена структура и в файл нужно сохранить массив из этих структур:
#include enum {name_size=10}; typedef struct { char name[name_size]; double x, y; } POINT; int main(void) { POINT fig[] = { {"Point 1", 0.0, 0.0}, {"Point 2", 4.23, -21.0}, {"Point 3", 6.65, -31.34}, {"Point 4", 3.2, -44.62}, {"Point 5", -1.65, 1.0}, }; FILE* fp = fopen("my_file.txt", "wb"); if(fp == NULL) { perror("my_file.txt"); return 1; } int res = fwrite(fig, sizeof(POINT), sizeof(fig) / sizeof(*fig), fp); fclose(fp); printf("res = %d\n", res); return 0; }
Здесь все должно быть вам понятно. После запуска программы увидим результат:
То есть, функция fwrite() записала 5 порций данных, то есть 5 структур типа POINT.
Давайте теперь прочитаем эти данные из файла. Это можно сделать следующим образом:
#include enum {name_size=10, max_points=50}; typedef struct { char name[name_size]; double x, y; } POINT; int main(void) { POINT fig[max_points]; int length = 0; FILE* fp = fopen("my_file.txt", "rb"); if(fp == NULL) { perror("my_file.txt"); return 1; } while(fread(&fig[length], sizeof(POINT), 1, fp) == 1) length++; fclose(fp); for(int i = 0;i length; ++i) printf("%s: (%.2f, %.2f)\n", fig[i].name, fig[i].x, fig[i].y); return 0; }
Мы здесь читаем данные по одной структуре, пока функция fread() возвращает 1, то есть, пока данные читаются корректно. Как только очередная порция данных не может быть прочитана, цикл завершается и на экран выводится прочитанная информация из массива структур:
Point 1: (0.00, 0.00)
Point 2: (4.23, -21.00)
Point 3: (6.65, -31.34)
Point 4: (3.20, -44.62)
Point 5: (-1.65, 1.00)
Видите, как просто и удобно можно считывать и записывать сложные, разнородные данные, используя бинарный режим доступа.
Видео по теме
Язык Си. Рождение легенды
#1. Этапы трансляции программы в машинный код. Стандарты
#2. Установка компилятора gcc и Visual Studio Code на ОС Windows
#3. Структура и понимание работы программы «Hello, World!»
#4. Двоичная, шестнадцатеричная и восьмеричная системы счисления
#5. Переменные и их базовые типы. Модификаторы unsigned и signed
#6. Операция присваивания. Числовые и символьные литералы. Операция sizeof
#7. Стандартные потоки ввода/вывода. Функции putchar() и getchar()
#8. Функция printf() для форматированного вывода
#9. Функция scanf() для форматированного ввода
#10. Арифметические операции: сложение, вычитание, умножение и деление
#11. Арифметические операции деления по модулю, инкремента и декремента
#12. Арифметические операции +=, -=, *=, /=, %=
#13. Булевый тип. Операции сравнения. Логические И, ИЛИ, НЕ
#14. Условный оператор if. Конструкция if-else
#15. Условное тернарное выражение
#16. Оператор switch множественного выбора. Ключевое слово break
#17. Битовые операции И, ИЛИ, НЕ, XOR. Сдвиговые операции
#18. Генерация псевдослучайных чисел. Функции математической библиотеки
#19. Директивы макропроцессора #define и #undef
#20. Директива #define для определения макросов-функций. Операции # и ##
#21. Директивы #include и условной компиляции
#22. Оператор цикла while
#23. Оператор цикла for
#24. Цикл do-while с постусловием. Вложенные циклы
#25. Операторы break, continue и goto
#26. Указатели. Проще простого
#27. Указатели. Приведение типов. Константа NULL
#28. Долгожданная адресная арифметика
#29. Введение в массивы
#30. Вычисление размера массива. Инициализация массивов
#31. Указатели на массивы
#32. Ключевое слово const с указателями и переменными
#33. Операции с массивами копирование, вставка, удаление и сортировка
#34. Двумерные и многомерные массивы. Указатели на двумерные массивы
#35. Строки. Способы объявления, escape-последовательности
#36. Ввод/вывод строк в стандартные потоки
#37. Строковые функции strlen(), strcpy(), strncpy(), strcat(), strncat()
#38. Строковые функции сравнения, поиска символов и фрагментов
#39. Строковые функции sprintf(), atoi(), atol(), atoll() и atof()
#40. Объявление и вызов функций
#41. Оператор return. Вызов функций в аргументах
#42. Прототипы функций
#43. Указатели как параметры. Передача массивов в функции
#44. Указатели на функцию. Функция как параметр (callback)
#45. Стековый фрейм. Автоматические переменные
#46. Рекурсивные функции
#47. Функции с произвольным числом параметров
#48. Локальные и глобальные переменные
#49. Локальные во вложенных блоках
#50. Ключевые слова static и extern
#51. Функции malloc(), free(), calloc(), realloc(), memcpy() и memmove()
#52. Перечисления (enum). Директива typedef
#53. Структуры. Вложенные структуры
#54. Указатели на структуры. Передача структур в функции
#55. Реализация стека (пример использования структур)
#56. Объединения (union). Битовые поля
#57. Файловые функции: fopen(), fclose(), fgetc(), fputc()
#58. Функции perror(), fseek() и ftell()
#59. Функции fputs(), fgets() и fprintf(), fscanf()
#60. Функции feof(), fflush(), setvbuf()
#61. Бинарный режим доступа. Функции fwrite() и fread()
#1. Первая программа на С++
#2. Ввод-вывод с помощью объектов cin и cout
#3. Пространства имен (namespace)
#4. Оператор using
#5. Новые типы данных. Приведение типов указателей
#6. Инициализация переменных. Ключевые слова auto и decltype
#7. Ссылки. Константные ссылки
#8. Объект-строка string. Операции с объектами класса string
#9. Файловые потоки. Открытие и закрытие файлов. Режимы доступа
#10. Чтение и запись данных в файл в текстовом режиме
#11. Чтение и запись данных в файл в бинарном режиме
#12. Перегрузка функций. Директива extern C
#13. Значения параметров функции по умолчанию
#15. Лямбда-выражения. Объявление и вызов
#16. Захват внешних значений в лямбда выражениях
#17. Структуры в С++, как обновленный тип данных
#18. Структуры. Режимы доступа. Сеттеры и геттеры
#19. Структуры. Конструкторы и деструкторы
#20. Операторы new / delete и new [] / delete []
© 2024 Частичное или полное копирование информации с данного сайта для распространения на других ресурсах, в том числе и бумажных, строго запрещено. Все тексты и изображения являются собственностью сайта
Что такое бинарный режим
Бинарные файлы в отличие от текстовых хранят информацию в виде набора байт. При открытии бинарного файла на чтение или запись также надо учитывать, что нам нужно применять режим «b» в дополнение к режиму записи («w») или чтения («r»).
При чтении мы получаем файл в виде набора байт, и наоборот, при записи в метод write() передается набор байт. Например, скопируем файл:
FILENAME = "forest.png" # файл для чтения NEWFILENAME = "forest_new.png" # файл для записи image_data = [] # список для хранения считанных данных # считываем файл в список image_data with open(FILENAME, "rb") as file: image_data = file.read() # запись выше считанных байт в новый файл with open(NEWFILENAME, "wb") as file: file.write(image_data) print(f"Файл скопирован в ")
Считывает файл, путь к которому хранится в переменной FILENAME. В данном случае это файл изображения «forest.png». Считанные байты помещаются в список image_data. Затем этот список записываем в файл с именем NEWFILENAME. Таким образом, мы скопируем содержимое одного файла в другой.
Модуль pickle
Также для работы с бинарными файлами Python предоставляет специальный встроенный модуль pickle , который упрощает работу с бинарными файлами. Этот модуль предоставляет два метода:
- dump(obj, file) : записывает объект obj в бинарный файл file
- load(file) : считывает данные из бинарного файла в объект
Допустим, надо надо сохранить значения двух переменных:
import pickle FILENAME = "user.dat" name = "Tom" age = 19 with open(FILENAME, "wb") as file: pickle.dump(name, file) pickle.dump(age, file) with open(FILENAME, "rb") as file: name = pickle.load(file) age = pickle.load(file) print("Имя:", name, "\tВозраст:", age)
С помощью функции dump последовательно записываются два объекта. Поэтому при чтении файла также последовательно посредством функции load мы можем считать эти объекты. Консольный вывод программы:
Имя: Tom Возраст: 28
Подобным образом мы можем сохранять и извлекать из файла наборы объектов:
import pickle FILENAME = "users.dat" users = [ ["Tom", 28, True], ["Alice", 23, False], ["Bob", 34, False] ] with open(FILENAME, "wb") as file: pickle.dump(users, file) with open(FILENAME, "rb") as file: users_from_file = pickle.load(file) for user in users_from_file: print("Имя:", user[0], "\tВозраст:", user[1], "\tЖенат/замужем:", user[2])
В зависимости от того, какой объект мы записывали функцией dump, тот же объект будет возвращен функцией load при считывании файла.
Имя: Tom Возраст: 28 Женат/замужем: True Имя: Alice Возраст: 23 Женат/замужем: False Имя: Bob Возраст: 34 Женат/замужем: False
Бинарный файл
Бинарный файл (binary в переводе с английского — «двоичный») — это формат представления данных с использованием двоичной системы счисления. В отличие от текстовых файлов, которые содержат информацию в виде символов (букв, цифр, спецсимволов и т. д.), такие форматы хранят информацию как последовательность битов (минимальных единиц информации, состоящих из 0 или 1). Двоичные файлы могут содержать данные разных типов, такие как числа (целые или с плавающей точкой), символы, звуковые или видеофайлы, изображения и многое другое.
«IT-специалист с нуля» наш лучший курс для старта в IT
IT-специалист с нуля
Наш лучший курс для старта в IT. За 2 месяца вы пробуете себя в девяти разных профессиях: мобильной и веб-разработке, тестировании, аналитике и даже Data Science — выберите подходящую и сразу освойте ее.
Профессия / 8 месяцев
IT-специалист с нуля
Попробуйте 9 профессий за 2 месяца и выберите подходящую вам
Что такое бинарный файл (двоичный файл)
Бинарными называются файлы с данными в двоичном формате, что означает, что каждому значению соответствует определенная последовательность битов. Например, для представления числа 7 в такой системе счисления требуется 4 бита: 0111. Бинарные файлы также могут содержать заголовки и дополнительные метаданные, которые описывают структуру и содержимое файла. Они используются программами для корректной интерпретации и обработки данных, записанных в двоичном коде. Некоторые примеры бинарных форматов файлов включают изображения (например, BMP, JPEG, PNG), звуковые файлы (например, WAV, MP3), базы данных и т. д.
Строго говоря, любой файл, хранящийся на жестком диске компьютера, является бинарным для самого компьютера, так как в конечном счете на аппаратном уровне машина может воспринимать только двоичный код. Однако для пользователя файлы бывают либо бинарными, либо текстовыми. Рассмотрим основные отличия между ними:
- Формат хранения данных. Бинарные файлы содержат информацию в двоичном формате, то есть в виде последовательности нулей и единиц. Текстовые файлы, напротив, хранят данные в виде символов, используя определенную кодировку, такую как ASCII или UTF-8.
- Читаемость данных. Одной из важных особенностей бинарных файлов является то, что человек не может их воспринимать без специальных программ или средств для их интерпретации. Текстовые файлы, с другой стороны, содержат информацию в виде понятного нам текста. Ее можно прочитать с помощью любого текстового редактора.
- Объем хранимых данных. Бинарные файлы обеспечивают более компактное хранение данных, так как они сохраняют исходный двоичный код без лишней информации. Текстовые файлы, в свою очередь, обычно требуют большего объема памяти, так как единицы измерения информации имеют более высокую степень диспетчеризации. Кроме того, использование двоичных данных обеспечивает их более точное представление, так как текстовые форматы могут потерять точность при конвертации чисел или при хранении особенных символов.
- Обработка данных. Бинарные файлы могут быть менее доступными для работы с данными в сравнении с текстовыми файлами. Их содержимое не может быть прочитано или отредактировано обычным текстовым редактором, и для работы требуется специализированное программное обеспечение. Кроме того, разработчики программ должны быть особенно внимательны при работе с бинарными файлами, чтобы избежать проблем совместимости или потери данных. Все это делает их более сложными для работы по сравнению с текстовыми файлами.
- Типы данных. Бинарные файлы могут хранить любые данные, включая числа, строки, изображения, звуковые файлы и другие. Текстовые файлы, с другой стороны, предназначены для хранения текстов, состоящих из букв, цифр, общих и специальных символов.
Чтобы компьютер смог работать с текстовым файлом (например, программой, написанной на высокоуровневом языке), он сначала переводит его в бинарный вид с помощью компилятора.
Курс для новичков «IT-специалист
с нуля» – разберемся, какая профессия вам подходит, и поможем вам ее освоить
Структура бинарного файла
Структура бинарных данных может быть представлена следующим образом:
- Заголовок файла. Он содержит основные сведения о файле, такие как размер, дату его создания и другие метаданные. Заголовок является необязательным элементом, но часто используется для хранения дополнительной информации о файле.
- Блоки данных. В бинарном файле их может быть несколько, между собой они разделяются маркерами. Блоки данных обычно имеют фиксированный размер и могут содержать любые данные, включая числа, строки, структуры и т. д.
- Структура данных. Бинарный файл может содержать структурированные данные, такие как массивы, списки, деревья и другие типы структур. Они представлены в виде последовательности блоков информации и используют специальные маркеры для обозначения начала и конца.
- Флаги и метки. В структуре двоичного файла они используются для определения определенных свойств или состояний содержащейся в нем информации. Например, флаги могут указывать на наличие ошибок или дополнительных сведений о данных.
- Конец файла. Бинарный файл обычно содержит маркер, который определяет конец содержащихся в нем данных. Он используется для определения размера и проверки целостности файла.
Структура бинарного файла может быть различной в зависимости от конкретного формата или приложения, которое его использует. Например, форматы изображений, звуковых и видеофайлов имеют свою специфическую структуру, определенную соответствующими стандартами или спецификациями.
Чтение и обработка бинарных файлов
Работа с бинарными файлами осуществляется в несколько этапов:
- Открытие файла. Для начала работы с бинарными данными необходимо открыть их с помощью специальных инструментов или методов в языке программирования. При открытии файла задается режим доступа, как для чтения, так и для записи.
- Чтение данных из файла. После открытия бинарного файла можно начать считывать данные из него. Это может быть выполнено блоками или пошагово, в зависимости от структуры файла и содержащейся в нем информации. Для считывания данных из файла используются специальные функции или методы, которые позволяют получить необработанные бинарные данные.
- Преобразование данных. Считанные бинарные данные могут требовать преобразования для дальнейшей обработки. Например, если считаны данные в виде последовательности байтов, то их можно превратить их в числа или строки в соответствии с требованиями программы.
- Обработка данных. После преобразования бинарных данных можно приступить непосредственно к работе с ними. В зависимости от цели обработки можно выполнять различные операции, такие как поиск и замена значений, фильтрация, анализ структуры файла и другие.
- Запись в бинарный файл. Если необходимо изменить или добавить данные в двоичный файл, можно воспользоваться соответствующими функциями или методами записи. Новая информация должна быть предварительно преобразована в бинарный формат, если это требуется.
- Закрытие файла. После завершения чтения и обработки бинарного файла необходимо его закрыть, чтобы освободить ресурсы и убедиться, что все изменения сохранены.
Для работы с бинарными файлами необходимо специальное программное обеспечение, так как их нельзя открыть, просмотреть или обработать с помощью обычных текстовых редакторов. К числу такого ПО относятся:
- IDA Pro. Это одна из самых популярных программ для анализа и обратной разработки бинарных файлов. IDA Pro предоставляет широкие возможности для исследования исполняемых файлов, дизассемблирования кода, поиска уязвимостей и обнаружения потенциальных вредоносных программ.
- OllyDbg. Это отладчик для анализа исполняемых файлов. OllyDbg позволяет разбирать, изменять и отлаживать код, содержащийся в бинарных файлах. Он полезен для исследования и обнаружения уязвимостей, а также для создания патчей и исправления ошибок в программном обеспечении.
- HxD. Это бесплатный шестнадцатеричный и дисковый редактор, который позволяет просматривать и изменять содержимое. HxD обеспечивает удобный интерфейс для просмотра и редактирования в различных форматах, а также поддерживает различные операции, такие как вставка, удаление и замена данных.
- FileAlyzer. Это мощный инструмент для анализа бинарных данных. FileAlyzer позволяет просматривать различные секции файлов, анализировать структуру и содержимое, искать строковые значения и выполнять другие операции для получения подробной информации о файле.
- Binwalk. Это инструмент для анализа и извлечения данных из бинарных файлов, таких как встроенные системы, образы файловых систем и другие встраиваемые ресурсы. Binwalk позволяет обнаруживать и извлекать различные типы файлов, а также искать подписи и присущую им информацию.
Применение бинарных файлов в программировании
Хранение и передача сложных структур данных. Бинарные файлы обеспечивают компактное хранение информации в двоичной форме. Они позволяют сохранять и восстанавливать сложные объекты, такие как массивы, структуры, классы и другие типы данных, без потери их исходной структуры и формата. Благодаря этому бинарные файлы используются для работы с большими объемами информации, такими как изображения, звуковые файлы или базы данных.
Сериализация и десериализация объектов. Бинарные файлы позволяют преобразовывать объекты в последовательности байтов и сохранять их в файле или передавать по сети. Сериализация используется для сохранения состояния объекта или передачи его на другую систему. Десериализация выполняет обратное преобразование, позволяя восстановить объект из бинарных данных. Обе операции часто применяются в работе с базами данных, в клиент-серверных системах, при создании игр и приложений обмена данными.
Чтение и запись потока данных. Бинарные файлы обеспечивают более эффективный способ чтения и записи данных, особенно при работе с большими объемами информации. Они позволяют пропускать символы форматирования или другие метаданные, что снижает объем передаваемой информации и ускоряет процесс ее обработки.
Управление состоянием программы. Бинарные файлы могут использоваться для сохранения состояния программы и его последующего восстановления. Это особенно важно при работе с приложениями или играми, где нужно сохранять текущее состояние игры или данные пользователя. Также эта способность применяется в операционных системах для их восстановления после критической ошибки.
Работа с медиа. С помощью двоичных данных можно сохранять изображения в форматах, специально разработанных для этой цели, таких как BMP, JPEG, PNG и других. Бинарные форматы обязательно сохраняют каждый пиксель изображения и все его детали, что делает их наиболее подходящими для работы с графикой. Двоичные файлы также широко применяются в обработке аудио- и видеофайлов. Форматы, такие как MP3, WAV, AVI, MP4 и многие другие, используют двоичное представление данных для хранения звуковой и видеоинформации. Бинарные файлы позволяют точное сохранение и воспроизведение этих данных, обеспечивая качественную обработку и воспроизведение медиаресурсов.
Разработка программного обеспечения. Бинарные файлы используются в процессе разработки программного обеспечения для хранения кода и исполняемых файлов. Бинарные форматы обеспечивают эффективную компиляцию, линковку и выполнение кода, что делает их необходимым инструментом для создания и запуска программ.
Работа с базами данных. Бинарные файлы используются для хранения и обработки информации в БД. Они позволяют компактно сохранять большие объемы информации, такой как текст, числа, даты и другие значения. Бинарные форматы эффективно управляют структурой и доступом к данным, обеспечивая быструю и надежную работу с ними.
Недостатки бинарных файлов
Непереносимость. Бинарные файлы имеют формат, зависящий от архитектуры компьютера и операционной системы. Если они созданы на одной платформе, то могут не работать на другой. Для обеспечения переносимости данных необходимо выполнить дополнительную работу по преобразованию формата при работе с разными платформами.
Неудобочитаемость. Бинарные файлы хранят данные в двоичной форме, которая нечитаема для человека. Это затрудняет процесс их отладки и тестирования, которые требуют использования специальных интерпретаторов, преобразователей и других инструментов.
Уязвимость к ошибкам и повреждениям. В случае ошибки при чтении или записи бинарного файла может произойти частичная или полная потеря данных. Бинарные файлы более чувствительны к ошибкам в сравнении с текстовыми, где некорректная информация может быть обработана или проигнорирована без значительных последствий.
Ограниченный доступ к данным. Работа с бинарными файлами требует знания и использования низкоуровневых операций, таких как чтение и запись определенного количества байтов. Поиск, сортировка или модификация содержащейся в них информации могут потребовать дополнительного кодирования и управления данными.
Таким образом, бинарные файлы являются одним из двух основных способов представления данных, используемых в программировании, хранении, обработке и передаче информации. В отличие от текстовых файлов, они более компактны и требуют меньше аппаратных ресурсов для работы с ними. Однако человеку для чтения, анализа, обработки и редактирования двоичных данных нужно использовать различные специализированные инструменты.
Бинарные файлы
Т екстовые файлы хранят данные в виде текста (sic!). Это значит, что если, например, мы записываем целое число 12345678 в файл, то записывается 8 символов, а это 8 байт данных, несмотря на то, что число помещается в целый тип. Кроме того, вывод и ввод данных является форматированным, то есть каждый раз, когда мы считываем число из файла или записываем в файл происходит трансформация числа в строку или обратно. Это затратные операции, которых можно избежать.
Текстовые файлы позволяют хранить информацию в виде, понятном для человека. Можно, однако, хранить данные непосредственно в бинарном виде. Для этих целей используются бинарные файлы.
#include #include #include #define ERROR_FILE_OPEN -3 void main() < FILE *output = NULL; int number; output = fopen("D:/c/output.bin", "wb"); if (output == NULL) < printf("Error opening file"); getch(); exit(ERROR_FILE_OPEN); >scanf("%d", &number); fwrite(&number, sizeof(int), 1, output); fclose(output); _getch(); >
Выполните программу и посмотрите содержимое файла output.bin. Число, которое ввёл пользователь записывается в файл непосредственно в бинарном виде. Можете открыть файл в любом редакторе, поддерживающем представление в шестнадцатеричном виде (Total Commander, Far) и убедиться в этом.
Запись в файл осуществляется с помощью функции
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
Функция возвращает число удачно записанных элементов. В качестве аргументов принимает указатель на массив, размер одного элемента, число элементов и указатель на файловый поток. Вместо массив, конечно, может быть передан любой объект.
Запись в бинарный файл объекта похожа на его отображение: берутся данные из оперативной памяти и пишутся как есть. Для считывания используется функция fread
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
Функция возвращает число удачно прочитанных элементов, которые помещаются по адресу ptr. Всего считывается count элементов по size байт. Давайте теперь считаем наше число обратно в переменную.
#include #include #include #define ERROR_FILE_OPEN -3 void main() < FILE *input = NULL; int number; input = fopen("D:/c/output.bin", "rb"); if (input == NULL) < printf("Error opening file"); getch(); exit(ERROR_FILE_OPEN); >fread(&number, sizeof(int), 1, input); printf("%d", number); fclose(input); _getch(); >
fseek
Одной из важных функций для работы с бинарными файлами является функция fseek
int fseek ( FILE * stream, long int offset, int origin );
Эта функция устанавливает указатель позиции, ассоциированный с потоком, на новое положение. Индикатор позиции указывает, на каком месте в файле мы остановились. Когда мы открываем файл, позиция равна 0. Каждый раз, записывая байт данных, указатель позиции сдвигается на единицу вперёд.
fseek принимает в качестве аргументов указатель на поток и сдвиг в offset байт относительно origin. origin может принимать три значения
- SEEK_SET — начало файла
- SEEK_CUR — текущее положение файла
- SEEK_END — конец файла. К сожалению, стандартом не определено, что такое конец файла, поэтому полагаться на эту функцию нельзя.
В случае удачной работы функция возвращает 0.
Дополним наш старый пример: запишем число, затем сдвинемся указатель на начало файла и прочитаем его.
#include #include #include #define ERROR_FILE_OPEN -3 void main() < FILE *iofile = NULL; int number; iofile = fopen("D:/c/output.bin", "w+b"); if (iofile == NULL) < printf("Error opening file"); getch(); exit(ERROR_FILE_OPEN); >scanf("%d", &number); fwrite(&number, sizeof(int), 1, iofile); fseek(iofile, 0, SEEK_SET); number = 0; fread(&number, sizeof(int), 1, iofile); printf("%d", number); fclose(iofile); _getch(); >
Вместо этого можно также использовать функцию rewind, которая перемещает индикатор позиции в начало.
В си определён специальный тип fpos_t, который используется для хранения позиции индикатора позиции в файле.
Функция
int fgetpos ( FILE * stream, fpos_t * pos );
используется для того, чтобы назначить переменной pos текущее положение. Функция
int fsetpos ( FILE * stream, const fpos_t * pos );
используется для перевода указателя в позицию, которая хранится в переменной pos. Обе функции в случае удачного завершения возвращают ноль.
long int ftell ( FILE * stream );
возвращает текущее положение индикатора относительно начала файла. Для бинарных файлов — это число байт, для текстовых не определено (если текстовый файл состоит из однобайтовых символов, то также число байт).
Рассмотрим пример: пользователь вводит числа. Первые 4 байта файла: целое, которое обозначает, сколько чисел было введено. После того, как пользователь прекращает вводить числа, мы перемещаемся в начало файла и записываем туда число введённых элементов.
#include #include #include #define ERROR_OPEN_FILE -3 void main() < FILE *iofile = NULL; unsigned counter = 0; int num; int yn; iofile = fopen("D:/c/numbers.bin", "w+b"); if (iofile == NULL) < printf("Error opening file"); getch(); exit(ERROR_OPEN_FILE); >fwrite(&counter, sizeof(int), 1, iofile); do < printf("enter new number? [1 - yes, 2 - no]"); scanf("%d", &yn); if (yn == 1) < scanf("%d", &num); fwrite(&num, sizeof(int), 1, iofile); counter++; >else < rewind(iofile); fwrite(&counter, sizeof(int), 1, iofile); break; >> while(1); fclose(iofile); getch(); >
Вторая программа сначала считывает количество записанных чисел, а потом считывает и выводит числа по порядку.
#include #include #include #define ERROR_OPEN_FILE -3 void main() < FILE *iofile = NULL; unsigned counter; int i, num; iofile = fopen("D:/c/numbers.bin", "rb"); if (iofile == NULL) < printf("Error opening file"); getch(); exit(ERROR_OPEN_FILE); >fread(&counter, sizeof(int), 1, iofile); for (i = 0; i < counter; i++) < fread(&num, sizeof(int), 1, iofile); printf("%d\n", num); >fclose(iofile); getch(); >
Примеры
1. Имеется бинарный файл размером 10*sizeof(int) байт. Пользователь вводит номер ячейки, после чего в неё записывает число. После каждой операции выводятся все числа. Сначала пытаемся открыть файл в режиме чтения и записи. Если это не удаётся, то пробуем создать файл, если удаётся создать файл, то повторяем попытку открыть файл для чтения и записи.
#include #include #include #define SIZE 10 void main() < const char filename[] = "D:/c/state"; FILE *bfile = NULL; int pos; int value = 0; int i; char wasCreated; do < wasCreated = 0; bfile = fopen(filename, "r+b"); if (NULL == bfile) < printf("Try to create file. \n"); getch(); bfile = fopen(filename, "wb"); if (bfile == NULL) < printf("Error when create file"); getch(); exit(1); >for (i = 0; i < SIZE; i++) < fwrite(&value, sizeof(int), 1, bfile); >printf("File created successfully. \n"); fclose(bfile); wasCreated = 1; > > while(wasCreated); do < printf("Enter position [0..9] "); scanf("%d", &pos); if (pos < 0 || pos >= SIZE) < break; >printf("Enter value "); scanf("%d", &value); fseek(bfile, pos*sizeof(int), SEEK_SET); fwrite(&value, sizeof(int), 1, bfile); rewind(bfile); for (i = 0; i < SIZE; i++) < fread(&value, sizeof(int), 1, bfile); printf("%d ", value); >printf("\n"); > while(1); fclose(bfile); >
2. Пишем слова в бинарный файл. Формат такой — сначало число букв, потом само слово без нулевого символа. Ели длина слова равна нулю, то больше слов нет. Сначала запрашиваем слова у пользователя, потом считываем обратно.
#include #include #include #include #define ERROR_FILE_OPEN -3 void main() < const char filename[] = "C:/c/words.bin"; const char termWord[] = "exit"; char buffer[128]; unsigned int len; FILE *wordsFile = NULL; printf("Opening file. \n"); wordsFile = fopen(filename, "w+b"); if (wordsFile == NULL) < printf("Error opening file"); getch(); exit(ERROR_FILE_OPEN); >printf("Enter words\n"); do < scanf("%127s", buffer); if (strcmp(buffer, termWord) == 0) < len = 0; fwrite(&len, sizeof(unsigned), 1, wordsFile); break; >len = strlen(buffer); fwrite(&len, sizeof(unsigned), 1, wordsFile); fwrite(buffer, 1, len, wordsFile); > while(1); printf("rewind and read words\n"); rewind(wordsFile); getch(); do < fread(&len, sizeof(int), 1, wordsFile); if (len == 0) < break; >fread(buffer, 1, len, wordsFile); buffer[len] = '\0'; printf("%s\n", buffer); > while(1); fclose(wordsFile); getch(); >
3. Задача — считать данные из текстового файла и записать их в бинарный. Для решения зачи создадим функцию обёртку. Она будет принимать имя файла, режим доступа, функцию, которую необходимо выполнить, если файл был удачно открыт и аргументы этой функции. Так как аргументов может быть много и они могут быть разного типа, то их можно передавать в качестве указателя на структуру. После выполнения функции файл закрывается. Таким образом, нет необходимости думать об освобождении ресурсов.
#include #include #include #define DEBUG #ifdef DEBUG #define debug(data) printf(«%s», data); #else #define debug(data) #endif const char inputFile[] = «D:/c/xinput.txt»; const char outputFile[] = «D:/c/output.bin»; struct someArgs < int* items; size_t number; >; int writeToFile(FILE *file, void* args) < size_t i; struct someArgs *data = (struct someArgs*) args; debug("write to file\n") fwrite(data->items, sizeof(int), data->number, file); debug(«write finished\n») return 0; > int readAndCallback(FILE *file, void* args) < struct someArgs data; size_t size, i = 0; int result; debug("read from file\n") fscanf(file, "%d", &size); data.items = (int*) malloc(size*sizeof(int)); data.number = size; while (!feof(file)) < fscanf(file, "%d", &data.items[i]); i++; >debug(«call withOpenFile\n») result = withOpenFile(outputFile, «w», writeToFile, &data); debug(«read finish\n») free(data.items); return result; > int doStuff() < return withOpenFile(inputFile, "r", readAndCallback, NULL); >//Обёртка — функция открывает файл. Если файл был благополучно открыт, //то вызывается функция fun. Так как аргументы могут быть самые разные, //то они передаются через указатель void*. В качестве типа аргумента //разумно использовать структуру int withOpenFile(const char *filename, const char *mode, int (*fun)(FILE* source, void* args), void* args) < FILE *file = fopen(filename, mode); int err; debug("try to open file ") debug(filename) debug("\n") if (file != NULL) < err = fun(file, args); >else < return 1; >debug(«close file «) debug(filename) debug(«\n») fclose(file); return err; > void main()
4. Функция saveInt32Array позволяет сохранить массив типа int32_t в файл. Обратная ей loadInt32Array считывает массив обратно. Функция loadInt32Array сначала инициализирует переданный ей массив, поэтому мы должны передавать указатель на указатель; кроме того, она записывает считанный размер массива в переданный параметр size, из-за чего он передаётся как указатель.
#include #include #include #include #define SIZE 100 int saveInt32Array(const char *filename, const int32_t *a, size_t size) < FILE *out = fopen(filename, "wb"); if (!out) < return 0; >//Записываем длину массива fwrite(&size, sizeof(size_t), 1, out); //Записываем весь массив fwrite(a, sizeof(int32_t), size, out); fclose(out); return 1; > int loadInt32Array(const char *filename, int32_t **a, size_t *size) < FILE *in = fopen(filename, "rb"); if (!in) < return 0; >//Считываем длину массива fread(size, sizeof(size_t), 1, in); //Инициализируем массив (*a) = (int32_t*) malloc(sizeof(int32_t) * (*size)); if (!(*a)) < return 0; >//Считываем весь массив fread((*a), sizeof(int32_t), *size, in); fclose(in); return 1; > void main() < const char *tmpFilename = "tmp.bin"; int32_t exOut[SIZE]; int32_t *exIn = NULL; size_t realSize; int i; for (i = 0; i < SIZE; i++) < exOut[i] = i*i; >saveInt32Array(tmpFilename, exOut, SIZE); loadInt32Array(tmpFilename, &exIn, &realSize); for (i = 0; i < realSize; i++) < printf("%d ", exIn[i]); >_getch(); >
5. Создание таблицы поиска. Для ускорения работы программы вместо вычисления функции можно произвести сначала вычисление значений функции на интервале с определённой точностью, после чего брать значения уже из таблицы. Программа сначала производит табулирование функции с заданными параметрами и сохраняет его в файл, затем подгружает предвычисленный массив, который уже используется для определения значений. В этой программе все функции возвращают переменную типа Result, которая хранит номер ошибки. Если функция отработала без проблем, то она возвращает Ok (0).
#define _CRT_SECURE_NO_WARNINGS //Да, это теперь обязательно добавлять, иначе не заработает #include #include #include #include #include //Каждая функция возвращает результат. Если он равен Ok, то функция //отработала без проблем typedef int Result; //Возможные результаты работы #define Ok 0 #define ERROR_OPENING_FILE 1 #define ERROR_OUT_OF_MEMORY 2 //Функция, которую мы будем табулировать double mySinus(double x) < return sin(x); >Result tabFunction(const char *filename, double from, double to, double step, double (*f)(double)) < Result r; FILE *out = fopen(filename, "wb"); double value; if (!out) < r = ERROR_OPENING_FILE; goto EXIT; >fwrite(&from, sizeof(from), 1, out); fwrite(&to, sizeof(to), 1, out); fwrite(&step, sizeof(step), 1, out); for (from; from < to; from += step) < value = f(from); fwrite(&value, sizeof(double), 1, out); >r = Ok; EXIT: fclose(out); return r; > Result loadFunction(const char *filename, double **a, double *from, double *to, double *step) < Result r; uintptr_t size; FILE *in = fopen(filename, "rb"); if (!in) < r = ERROR_OPENING_FILE; goto EXIT; >//Считываем вспомогательную информацию fread(from, sizeof(*from), 1, in); fread(to, sizeof(*to), 1, in); fread(step, sizeof(*step), 1, in); //Инициализируем массив size = (uintptr_t) ((*to - *from) / *step); (*a) = (double*) malloc(sizeof(double)* size); if (!(*a)) < r = ERROR_OUT_OF_MEMORY; goto EXIT; >//Считываем весь массив fread((*a), sizeof(double), size, in); r = Ok; EXIT: fclose(in); return r; > void main() < const char *tmpFilename = "tmp.bin"; Result r; double *exIn = NULL; int accuracy, option; double from, to, step, arg; uintptr_t index; //Запрашиваем параметры для создания таблицы поиска printf("Enter parameters\nfrom = "); scanf("%lf", &from); printf("to = "); scanf("%lf", &to); printf("step = "); scanf("%lf", &step); r = tabFunction(tmpFilename, from, to, step, mySinus); if (r != Ok) < goto CATCH_SAVE_FUNCTION; >//Обратите внимание на формат вывода. Точность определяется //во время работы программы. Формат * подставит значение точности, //взяв его из списка аргументов accuracy = (int) (-log10(step)); printf("function tabulated from %.*lf to %.*lf with accuracy %.*lf\n", accuracy, from, accuracy, to, accuracy, step); r = loadFunction(tmpFilename, &exIn, &from, &to, &step); if (r != Ok) < goto CATCH_LOAD_FUNCTION; >accuracy = (int)(-log10(step)); do < printf("1 to enter values, 0 to exit : "); scanf("%d", &option); if (option == 0) < break; >else if (option != 1) < continue; >printf("Enter value from %.*lf to %.*lf : ", accuracy, from, accuracy, to); scanf("%lf", &arg); if (arg < from || arg >to) < printf("bad value\n"); continue; >index = (uintptr_t) ((arg - from) / step); printf("saved %.*lf\ncomputed %.*lf\n", accuracy, exIn[index], accuracy, mySinus(arg)); > while (1); r = Ok; goto EXIT; CATCH_SAVE_FUNCTION: < printf("Error while saving values"); goto EXIT; >CATCH_LOAD_FUNCTION: < printf("Error while loading values"); goto EXIT; >EXIT: free(exIn); _getch(); exit(r); >
6. У нас имеются две структуры. Первая PersonKey хранит логин, пароль, id пользователя и поле offset. Вторая структура PersonInfo хранит имя и фамилию пользователя и его возраст. Первые структуры записываются в бинарный файл keys.bin, вторые структуры в бинарный файл values.bin. Поле offset определяет положение соответствующей информации о пользователе во втором файле. Таким образом, получив PersonKey из первого файла, по полю offset можно извлечь из второго файла связанную с данным ключом информацию.
Зачем так делать? Это выгодно в том случае, если структура PersonInfo имеет большой размер. Извлекать массив маленьких структур из файла не накладно, а когда нам понадобится большая структура, её можно извлечь по уже известному адресу в файле.
#define _CRT_SECURE_NO_WARNINGS #include #include #include #include typedef struct PersonKey < long long id; char login[64]; char password[64]; long offset;//Положение соответствующих значений PersonInfo >PersonKey; typedef struct PersonInfo < unsigned age; char firstName[64]; char lastName[128]; >PersonInfo; /* Функция запрашивает у пользователя данные и пишет их подряд в два файла */ void createOnePerson(FILE *keys, FILE *values) < static long long pkey; PersonInfo pinfo; pkey.id = id++; //Так как все значения пишутся друг за другом, то текущее положение //указателя во втором файле будет позицией для новой записи pkey.offset = ftell(values); printf("Login: "); scanf("%63s", pkey.login); printf("Password: "); scanf("%63s", pkey.password); printf("Age: "); scanf("%d", &(pinfo.age)); printf("First Name: "); scanf("%63s", pinfo.firstName); printf("Last Name: "); scanf("%127s", pinfo.lastName); fwrite(&pkey, sizeof(pkey), 1, keys); fwrite(&pinfo, sizeof(pinfo), 1, values); >void createPersons(FILE *keys, FILE *values) < char buffer[2]; int repeat = 1; int counter = 0;//Количество элементов в файле //Резервируем место под запись числа элементов fwrite(&counter, sizeof(counter), 1, keys); printf("CREATE PERSONS\n"); do < createOnePerson(keys, values); printf("\nYet another one? [y/n]"); scanf("%1s", buffer); counter++; if (buffer[0] != 'y' && buffer[0] != 'Y') < repeat = 0; >> while(repeat); //Возвращаемся в начало и пишем количество созданных элементов rewind(keys); fwrite(&counter, sizeof(counter), 1, keys); > /* Создаём массив ключей */ PersonKey* readKeys(FILE *keys, int *size) < int i; PersonKey *out = NULL; rewind(keys); fread(size, sizeof(*size), 1, keys); out = (PersonKey*) malloc(*size * sizeof(PersonKey)); fread(out, sizeof(PersonKey), *size, keys); return out; >/* Функция открывает сразу два файла. Чтобы упростить задачу, возвращаем массив файлов. */ FILE** openFiles(const char *keysFilename, const char *valuesFilename) < FILE **files = (FILE**)malloc(sizeof(FILE*)*2); files[0] = fopen(keysFilename, "w+b"); if (!files[0]) < return NULL; >files[1] = fopen(valuesFilename, "w+b"); if (!files[1]) < fclose(files[0]); return NULL; >return files; > /* Две вспомогательные функции для вывода ключа и информации */ void printKey(PersonKey pk) < printf("%d. %s [%s]\n", (int)pk.id, pk.login, pk.password); >void printInfo(PersonInfo info) < printf("%d %s %s\n", info.age, info.firstName, info.lastName); >/* Функция по ключу (вернее, по его полю offset) достаёт нужное значение из второго файла */ PersonInfo readInfoByPersonKey(PersonKey pk, FILE *values) < PersonInfo out; rewind(values); fseek(values, pk.offset, SEEK_SET); fread(&out, sizeof(PersonInfo), 1, values); return out; >void getPersonsInfo(PersonKey *keys, FILE *values, int size) < int index; PersonInfo p; do < printf("Enter position of element. To exit print bad index: "); scanf("%d", &index); if (index < 0 || index >= size) < printf("Bad index"); return; >p = readInfoByPersonKey(keys[index], values); printInfo(p); > while (1); > void main() < int size; int i; PersonKey *keys = NULL; FILE **files = openFiles("C:/c/keys.bin", "C:/c/values.bin"); if (files == 0) < printf("Error opening files"); goto FREE; >createPersons(files[0], files[1]); keys = readKeys(files[0], &size); for (i = 0; i < size; i++) < printKey(keys[i]); >getPersonsInfo(keys, files[1], size); fclose(files[0]); fclose(files[1]); FREE: free(files); free(keys); _getch(); >
ru-Cyrl 18- tutorial Sypachev S.S. 1989-04-14 sypachev_s_s@mail.ru Stepan Sypachev students
Всё ещё не понятно? – пиши вопросы на ящик