Хотя функции getc()/putc() позволяют вносить в файл отдельные символы, но фактически мы имеем дело с бинарными файлами. Если мы записываем в файл строку, то в принципе мы даже можем открыть записанный файл любом текстовом редакторе и понять, что там было записано. Но не всегда данные могут представлять строки. И чтобы более наглядно разобраться с работой с бинарными файлами, рассмотрим еще одни пример - с записью-чтением структуры из файла.
#include <stdio.h>
#include <stdlib.h>
struct person
{
char name[16];
int age;
};
int save(char * filename, struct person *p);
int load(char * filename);
int main(void)
{
char * filename = "person.dat";
struct person tom = { "Tom Smith", 21 };
save(filename, &tom);
load(filename);
return 0;
}
// запись структуры в файл
int save(char * filename, struct person *p)
{
FILE * fp;
char *c;
int size = sizeof(struct person); // количество записываемых байтов
fp = fopen(filename, "wb"); //открываем файл для бинарной записи
if (!fp) // если не удалось открыть файл
{
printf("Error occured while opening file \n");
return 1;
}
// устанавливаем указатель на начало структуры
c = (char *)p;
// посимвольно записываем в файл структуру
for (int i = 0; i < size; i++)
{
putc(*c++, fp);
}
fclose(fp);
return 0;
}
// загрузка из файла структуры
int load(char * filename)
{
FILE * fp;
char *c;
int i; // для считывания одного символа
// количество считываемых байтов
int size = sizeof(struct person);
// выделяем память для считываемой структуры
struct person * ptr = malloc(size);
fp = fopen(filename, "rb"); // открываем файл для бинарного чтения
if (!fp)
{
printf("Error occured while opening file \n");
return 1;
}
// устанавливаем указатель на начало блока выделенной памяти
c = (char *)ptr;
// считываем посимвольно из файла
while ((i = getc(fp))!=EOF)
{
*c = i;
c++;
}
fclose(fp);
// вывод на консоль загруженной структуры
printf("%-20s %5d \n", ptr->name, ptr->age);
free(ptr);
return 0;
}
В данном случае запись и чтение структуры выделены в отдельные функции: save() и load() соответственно.
Для записи в функции save() через параметр struct person *p получаем указатель на сохраняемую структур.
Фактически его значением является начальный адрес блока памяти, где располагается структура.
Функция putc записывает отдельный символ в файл, однако нам надо записать структуру. Для этого мы создаем указатель на символ (который по сути представляет один байт) и устанавливаем этот указатель на начало блока памяти, выделенного для структуры.
c = (char *)p;
То есть в данном случае мы получаем адрес в памяти первого байта из блока памяти, которая выделена для структуры. И затем мы можем пройтись по всему этому блоку и получить отдельные байты и занести их в файл:
for (int i = 0; i < size; i++)
{
putc(*c++, fp);
}
И в данном случае нам не важно, какие поля имеет структура, какой она имеет размер. Мы работаем с ней как с набором байт и заносим эти байты в файл. После занесения каждого отдельного байта в файл указатель c в блоке памяти перемещается на один байт вперед.
При чтении файла в функции load() используется похожий принцип только в обратную сторону.
Во-первых, для считывания структуры из файла мы выделяем блок динамической памяти для хранения прочитанных данных:
struct person * ptr = (struct person *) malloc(size);
После этого указатель ptr будет указывать на первый адрес блока из 20 байт (наша структура занимает 20 байт = 16 символов и 4 байта для числа int).
Затем так как при прочтении мы получаем символы, устанавливаем указатель на первый байт выделенного блока и в цикле считываем данные из файла в этот блок:
c = (char *)ptr;
// считываем посимвольно из файла
while ((i = getc(fp))!=EOF)
{
*c = i;
c++;
}
Здесь стоит обратить внимание на то, что в данном случае на самом деле считываем даже не символ, а числовой код символа в переменную типа int и только потом передаем значение указателю c. Это сделано для корректной обработки окончания файла EOF. Это значение может представлять любое отрицательное число. И если бы мы сохранили отрицательное число (например, возраст пользователя был бы отрицательным), то оно было бы некорректно интерпретировано при чтении как конец файла, и итоговый результа был бы неопределенным. Поэтому более правильно считывать именно числовой код символа в переменную int, а затем числовой код передавать в char.
Выше приведен пример по работе с одной структурой. Но, как правило, при работе с файлами мы оперируем не одной структурой, а каким-то набором структур. Поэтому усложним задачу и сохраним и считаем из файла массив структур:
#include <stdio.h>
#include <stdlib.h>
struct person
{
char name[20];
int age;
};
int save(char * filename, struct person *st, int n);
int load(char * filename);
int main(void)
{
char * filename = "people.dat";
struct person people[] = { {"Tom", 23}, {"Alice", 27}, {"Bob", 31}, {"Kate", 29 }};
int n = sizeof(people) / sizeof(people[0]);
save(filename, people, n);
load(filename);
return 0;
}
// запись в файл массива структур
int save(char * filename, struct person * st, int n)
{
char *c; // для записи отдельных символов
// число записываемых байтов
int size = n * sizeof(struct person);
FILE * fp = fopen(filename, "wb");
if (!fp)
{
printf("Error occured while opening file\n");
return -1;
}
// записываем количество структур
c = (char *)&n;
for (int i = 0; i < sizeof(n); i++)
{
putc(*c++, fp);
}
// посимвольно записываем в файл все структуры
c = (char *)st;
for (int i = 0; i < size; i++)
{
putc(*c, fp);
c++;
}
fclose(fp);
return 0;
}
// загрузка из файла массива структур
int load(char * filename)
{
char *c; // для считывания отдельных символов
int m = sizeof(int); // сколько надо считать для получения размера массива
int n; // количество структур в массиве
FILE * fp = fopen(filename, "r");
if (!fp)
{
printf("Error occured while opening file\n");
return -1;
}
// выделяем память для хранения количества данных
int *ptr_count = malloc(m);
// считываем количество структур
c = (char *)ptr_count;
// пока не считаем m байт, сохраняем байт в выделенный блок для размера массива
while (m > 0 && (*c = getc(fp)) != EOF)
{
c++;
m--;
}
//получаем число элементов
n = *ptr_count;
free(ptr_count); // освобождаем память
// выделяем память для считанного массива структур
struct person * ptr = malloc(n * sizeof(struct person));
// устанавливаем указатель на блок памяти, выделенный для массива структур
c = (char *)ptr;
// считываем посимвольно из файла
while ((*c= getc(fp))!=EOF)
{
c++;
}
// перебор загруженных элементов и вывод на консоль
printf("\nFound %d people\n\n", n);
for (int i = 0; i < n; i++)
{
printf("%-5d %-10s %5d \n", i + 1, (ptr + i)->name, (ptr + i)->age);
// или так
// printf("%-5d %-10s %5d \n", i + 1, ptr[i].name, ptr[i].age);
}
free(ptr);
fclose(fp);
return 0;
}
Данная задача усложнена тем, что нам надо хранить массив структур, количество которых точно может быть неизвестно. Один из вариантов рещения этой проблемы состоит в сохранении некоторой метаинформации о файле в начале файла. В частности, в данном случае в начале файла сохраняется число записанных структур.
Запись во многом аналогична записи одной структуры. Сначала устанавливаем указатель на число n, которое представляет количество структур, и все байты этого числа записываем в файл:
c = (char *)&n;
for (int i = 0; i<sizeof(n); i++)
{
putc(*c++, fp);
}
Затем подобным образом записываем все байты из массива структур - устанавливаем указатель на первый байт массива структур и записываем size байт в файл:
// посимвольно записываем в файл все структуры
c = (char *)st;
for (int i = 0; i < size; i++)
{
putc(*c, fp);
c++;
}
При чтении нам придется файктически считывать из файла два значения: количество структур и их массив. Поэтому при чтении два раза выделяется память. Вначале для количества элементов:
int *ptr_count = malloc(m);
Затем мы считываем первые 4 байта из файла для получения числа:
c = (char *)ptr_count;
while (m > 0 && (*c = getc(fp)) != EOF)
{
c++;
m--;
}
//получаем число элементов
n = *ptr_count;
Затем аналогичные действия проделываем для массива структур.
struct person * ptr = malloc(n * sizeof(struct person));
// устанавливаем указатель на блок памяти, выделенный для массива структур
c = (char *)ptr;
// считываем посимвольно из файла
while ((*c= getc(fp))!=EOF)
{
c++;
}
И результатом программы должен быть вывод считанных данных:
Found 4 people 1 Tom 23 2 Alice 27 3 Bob 31 4 Kate 29