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

Последнее обновление: 09.01.2023

Хотя функции 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
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850