|
Бинарный режим доступа. Функции fwrite() и fread()
На этом занятии мы рассмотрим бинарный режим доступа к файлам.
До сих пор на
предыдущих занятиях файлы открывались в текстовом режиме, который подразумевает
запись и чтение текстовой информации. Соответственно, все рассмотренные
функции, как правило, используются именно для текстовых данных, хотя их можно
применять и в бинарном режиме они будут работать абсолютно также.
Так что же такое
бинарный режим и чем он отличается от текстового? Давайте представим, что нам в
программе нужно сохранять в файл значения различных переменных, например,
таких:
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)
Видите, как
просто и удобно можно считывать и записывать сложные, разнородные данные,
используя бинарный режим доступа.
|