Чтение и запись структур в файл


Хотя функции getc()/putc() позволяют вносить в файл отдельные символы, но фактически мы имеем дело с бинарными файлами. Если мы записываем в файл строку, то в принципе мы даже можем открыть записанный файл любом текстовом редакторе и понять, что там было записано. Но не всегда данные могут представлять строки. И чтобы более наглядно разобраться с работой с бинарными файлами, рассмотрим еще одни пример - с записью-чтением структуры из файла:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#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", 21 };

save(filename, &tom);
load(filename);

return 0;
}

// запись структуры в файл
int save(char * filename, struct person *p)
{
FILE * fp;
char *c;
int size = sizeof(struct person); // количество записываемых байтов

if ((fp = fopen(filename, "wb")) == NULL)
{
perror("Error occured while opening file");
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 = (struct person *) malloc(size);

if ((fp = fopen(filename, "rb")) == NULL)
{
perror("Error occured while opening file");
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;
}
При записи мы получаем указатель на структуру, который содержит начальный адрес блока памяти, по которому располагается структура. Для структуры выделяется 16 + 4=20 байт.

Функция putc записывает отдельный символ в файл, однако нам надо записать структуру. Для этого мы создаем указатель на символ (который по сути представляет один байт) и устанавливаем этот указатель на начало блока памяти, выделенного для структуры.

1
c = (char *)p;
То есть в данном случае мы получаем адрес в памяти первого байта из блока памяти, которая выделена для структуры. И затем мы можем пройтись по всему этому блоку и получить отдельные байты и занести их в файл:

1
2
3
4
for (int i = 0; i < size; i++)
{
putc(*c++, fp);
}
И в данном случае нам не важно, какие поля имеет структура, какой она имеет размер. Мы работаем с ней как с набором байт и заносим эти байты в файл. После занесения каждого отдельного байта в файл указатель c в блоке памяти перемещается на один байт вперед.

При чтении файла используется похожий принцип только в обратную сторону.

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

1
struct person * ptr = (struct person *) malloc(size);
После этого указатель ptr будет указывать на первый адрес блока из 20 байт.

Затем так как при прочтении мы получаем символы, устанавливаем указатель на символ на начало выделенного блока и в цикле считываем данные из файла в этот блок:

1
2
3
4
5
6
7
c = (char *)ptr;
// считываем посимвольно из файла
while ((i = getc(fp))!=EOF)
{
*c = i;
c++;
}
Здесь стоит обратить внимание на то, что в данном случае на самом деле считываем даже не символ, а числовой код символа в переменную типа int и только потом передаем значение указателю c. Это сделано для корректной обработки окончания файла EOF. Это значение может представлять любое отрицательное число. И если бы мы сохранили отрицательное число (например, возраст пользователя был бы отрицательным), то оно было бы некорректно интерпретировано при чтении как конец файла, и итоговый результа был бы неопределенным. Поэтому более правильно считывать именно числовой код символа в переменную int, а затем числовой код передавать в char.

Запись и чтение массива структур

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#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)
{
FILE * fp;
char *c;

// число записываемых байтов
int size = n * sizeof(struct person);

if ((fp = fopen(filename, "wb")) == NULL)
{
perror("Error occured while opening file");
return 1;
}
// записываем количество структур
c = (char *)&n;
for (int i = 0; i<sizeof(int); 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)
{
FILE * fp;
char *c;
int m = sizeof(int);
int n, i;

// выделяем память для количества данных
int *pti = (int *)malloc(m);

if ((fp = fopen(filename, "r")) == NULL)
{
perror("Error occured while opening file");
return 1;
}
// считываем количество структур
c = (char *)pti;
while (m>0)
{
i = getc(fp);
if (i == EOF) break;
*c = i;
c++;
m--;
}
//получаем число элементов
n = *pti;

// выделяем память для считанного массива структур
struct person * ptr = (struct person *) malloc(n * sizeof(struct person));
c = (char *)ptr;
// после записи считываем посимвольно из файла
while ((i= getc(fp))!=EOF)
{
*c = i;
c++;
}
// перебор загруженных элементов и вывод на консоль
printf("\n%d people in the file stored\n\n", n);

for (int k = 0; k<n; k++)
{
printf("%-5d %-20s %5d \n", k + 1, (ptr + k)->name, (ptr + k)->age);
}

free(pti);
free(ptr);
fclose(fp);
return 0;
}
Данная задача усложнена тем, что нам надо хранить массив структур, количество которых точно может быть неизвестно. Один из вариантов рещения этой проблемы состоит в сохранении некоторой метаинформации о файле в начале файла. В частности, в данном случае в начале файла сохраняется число записанных структур.

Запись во многом аналогична записи одной структуры. Сначала устанавливаем указатель на число n, которое представляет количество структур, и все байты этого числа записываем в файл:

1
2
3
4
5
c = (char *)&n;
for (int i = 0; i<sizeof(int); i++)
{
putc(*c++, fp);
}
Затем подобным образом записываем все байты из массива структур.

При чтении нам придется файктически считывать из файла два значения: количество структур и их массив. Поэтому при чтении два раза выделяется память. Вначале для количества элементов:

1
int *pti = (int *)malloc(m);
Затем мы считываем первые 4 байта из файла для получения числа:

1
2
3
4
5
6
7
8
9
10
11
c = (char *)pti;
while (m>0)
{
i = getc(fp);
if (i == EOF) break;
*c = i;
c++;
m--;
}
//получаем число элементов
n = *pti;
Затем аналогичные действия проделываем для массива структур.

И результатом программы должен быть вывод считанных данных:

4 people stored in the file

1 Tom 23
2 Alice 27
3 Bob 31
4 Kate 29