Бинарные файлы в языке C

Текст хранится в обычном своем формате. Если, к примеру, записать 12345678 как обычные цифры, то они займут 8 байт. И если такое значение станет возрастать до огромного количества, то придется выделять много памяти для хранения данных.

Однако, такого можно избежать. Для такого требуется использование специальных бинарных данных, связанных с языком программирования C. Как раз-таки в данной статье будет расписано, как работать с таковыми и конвертировать текст в более удобный формат.

Бинарные файлы – как с ними работать?

Чтобы рассмотреть принцип работы с бинарными файлами, рекомендуется просто-напросто обращаться к примерам. Вот один из таковых, что используется:

#include <conio.h> ?
#include <stdio.h>
#include <stdlib.h>

#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();
}

Выполнив такую программу, можно далее просмотреть содержимое бинарного файла. Главное, чтобы приложение, в котором открывается файл, поддерживалась шестнадцатеричная система считывания информации. Тогда можно будет увидеть простой текст. А чтобы записать информацию в .bin файл, требуется таким образом ввести команду:

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

Всего считывается в count элементов по размеру size. Число удачно прочитанных элементов помещается по адресу, называемому ptr. И чтобы считать в обратную сторону данные, необходимо ввести следующий набор строчек кода:

#include <conio.h> ?
#include <stdio.h>
#include <stdlib.h>

#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. Вот, как выглядит ее применение:

int fseek ( FILE * stream, long int offset, int origin );

Origin в данной структуре может принимать только 3 значения:

  • SEEK_SET – начало файла, его первые символы для обработки информации;
  • SEEK_CUR – середина файла. По определению, это место, находящееся между началом и концом текстового документа .bin;
  • SEEK_END – конец файла. К сожалению, нельзя точно сказать, что такое «конец» у документа, поэтому не стоит рассчитывать на применение данной функции на практике.

Если функции работают исправно, то вернется 0.

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#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, что изменит указатель на самое начало страницы с текстом.

Функция, используемая для назначения переменной posтекущей позиции:

int fgetpos ( FILE * stream, fpos_t * pos );

Данный отрывок кода необходим для перемещения указателя в то положение, которое хранится в переменной pos:

int fsetpos ( FILE * stream, const fpos_t * pos );

Следующая функция возвращает текущее положение индикатора относительно начала файла:

long int ftell ( FILE * stream );

Вот пример. Человек вводит 4 цифры. Первые 4 байта файла – обозначение, сколько цифр ввел пользователь. Когда ввод заканчивается, совершается перенос в начало файла, после чего начинается запись количество введенных частиц:

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>

#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 <stdio.h>
#include <conio.h>
#include <stdlib.h>

#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();
}

А теперь пойдут сложные примеры с определенными целями. Первое, что можно будет разобрать – ситуацию, когда необходимо конвертировать текстовые данные в бинарный формат, используя функцию «обертка». Данный элемент взаимодействия берет имя файла, режим доступа, функцию для выполнения после открытия файла и аргументы.

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

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#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() {
printf(«result = %d», doStuff());
getch();
}

И последний пример, который требуется разобрать – с двумя структурами в одном. Первая – PersonKey с логином, паролем, id и с полем offset. Вторая – PersonInfo, содержащая имя, фамилию и возраст человека. Первая структура записывается в key.bin, а вторая – в values.bin. offset определяет положение информации о человеке во второй структуре, ввиду чего возможно взять данные из 1 файла и получить соотношением информацию из 2 документа. Выглядит это так:

#define _CRT_SECURE_NO_WARNINGS

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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 id = 0;
PersonKey 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();
}

Очень полезно применять такую структуру при большом объеме PersonInfo. Все-таки, данных много, а с помощью такого кода возможно быстро находить пользователя по уже имеющейся информации.

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

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