Бинарный режим доступа. Функции fwrite() и fread()

Практический курс по C/C++: https://stepik.org/course/193691

На этом занятии мы рассмотрим бинарный режим доступа к файлам.

До сих пор на предыдущих занятиях файлы открывались в текстовом режиме, который подразумевает запись и чтение текстовой информации. Соответственно, все рассмотренные функции, как правило, используются именно для текстовых данных, хотя их можно применять и в бинарном режиме они будут работать абсолютно также.

Так что же такое бинарный режим и чем он отличается от текстового? Давайте представим, что нам в программе нужно сохранять в файл значения различных переменных, например, таких:

int var_i = -10;
double pi = 3.141592653589793;
char ch = 'S';

Если использовать текстовый формат представления, то придется эти числа прямо в таком виде записать в выходной файл, например, с помощью функции fprintf():

#include <stdio.h>
 
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;
}

В файле увидим следующую информацию:

-10; 3.141593; S

Обратите внимание, вещественное число 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 <stdio.h>
 
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 <stdio.h>
 
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;
}

Здесь все должно быть вам понятно. После запуска программы увидим результат:

res = 5

То есть, функция fwrite() записала 5 порций данных, то есть 5 структур типа POINT.

Давайте теперь прочитаем эти данные из файла. Это можно сделать следующим образом:

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

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

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме