Раздел 14 — Двоичные файлы

Записи фиксированного размера, прямой доступ к любой из них и обработка файла по байтам

Прогресс курса Раздел 14 из 28

Что вы освоите в этом разделе

1 академический час теории и 4 часа практики: практические работы №12 «Двоичный файл записей» и №13 «Побайтовая обработка файла» — на портале. Раздел открывает блок 2.
01

Текстовый и двоичный файл

В разделе 13 записи хранились текстом: Ivanov;21V;4.75. Такой файл читает человек и любая программа, но у него два недостатка. Строки разной длины — чтобы найти сотую запись, надо прочитать 99 предыдущих. И число при записи переводится в текст и обратно: %.2f округляет, и прочитанное значение может отличаться от записанного.

Двоичный файл хранит байты памяти как есть: структура Student занимает в файле ровно sizeof(Student) байтов, как в памяти. Все записи одного размера — запись номер i начинается с байта i * sizeof(Student), и до неё можно «перепрыгнуть». Число не переводится в текст — читается тем же, каким было записано.

Текстовый ("r", "w")Двоичный ("rb", "wb")
Что лежитсимволы: 4.75 — 4 байта текстабайты памяти: double — 8 байтов
Чтение и записьfprintf, fgets, sscanffwrite, fread, fgetc
Найти запись № iпрочитать i строкfseek на i * sizeof
'\n' в Windowsпишется как \r\n, читается как \nбайт пишется как есть
Кто прочтётчеловек, Excel, любая программапрограмма, знающая формат
Буква b обязательна для любого нетекстового файла. В Linux режимы "r" и "rb" одинаковы, а в Windows текстовый режим заменяет байт 0x0A на пару 0x0D 0x0A при записи и считает байт 0x1A концом файла при чтении. Картинка, скопированная в текстовом режиме, в Linux получается целой, а в Windows испорчена.
02

fwrite и fread: массив структур за один вызов

fwrite(адрес, размер элемента, количество, файл) записывает подряд количество элементов по размер байтов, начиная с адреса. fread с теми же параметрами читает. Обе возвращают число полностью записанных или прочитанных элементов — не байтов:

/* Записать n записей в файл path; 0 — успех, 1 — ошибка */
int save(const char *path, const Student a[], int n)
{
    FILE *f = fopen(path, "wb");
    if (f == NULL) {
        perror(path);
        return 1;
    }
    size_t written = fwrite(a, sizeof a[0], (size_t)n, f);
    if (fclose(f) != 0 || written != (size_t)n) {
        fprintf(stderr, "%s: write error\n", path);
        return 1;
    }
    return 0;
}

/* Прочитать не больше max записей; число прочитанных или -1 */
int load(const char *path, Student a[], int max)
{
    FILE *f = fopen(path, "rb");
    if (f == NULL) {
        perror(path);
        return -1;
    }
    size_t n = fread(a, sizeof a[0], (size_t)max, f);
    if (ferror(f)) {
        fprintf(stderr, "%s: read error\n", path);
        n = 0;
    }
    fclose(f);
    return (int)n;
}

Программа examples/14/records.c записывает трёх студентов, узнаёт размер файла (как — в пункте 4) и читает их обратно:

sizeof(Student) = 64, file 192 bytes, 3 records
0: Ivanov   21V  4.75
1: Petrova  21V  3.80
2: Sidorov  22V  4.50
03

Что лежит в файле: дамп

Посмотрим на первые 64 байта students.dat — одну запись — программой hexdump из пункта 5. Слева смещение, в середине байты в шестнадцатеричном виде, справа они же символами (непечатные — точкой):

00000000  49 76 61 6e 6f 76 00 00 00 00 00 00 00 00 00 00  |Ivanov..........|
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00 00 32 31 56 00 00 00 00  |.........21V....|
00000030  00 00 00 00 00 00 00 00 00 00 00 00 00 00 13 40  |...............@|
Двоичный файл структур непереносим. Выравнивание зависит от компилятора и платформы, порядок байтов — от процессора, размер int и long — от системы (long в Windows 4 байта, в Linux 8). Такой файл надёжно читает только программа, собранная из того же описания структуры тем же компилятором. Форматы для обмена (PNG, ZIP) описывают каждое поле побайтно: размер, порядок байтов, смещение — и читают их по байтам, как в пункте 5.
04

Прямой доступ: fseek и ftell

У открытого файла есть текущая позиция — номер байта, с которого начнётся следующее чтение или запись. После каждого fread и fwrite она сдвигается на обработанные байты. Позицию можно поставить вручную:

ВызовЧто делает
fseek(f, offset, SEEK_SET)позиция = offset от начала файла
fseek(f, offset, SEEK_CUR)сдвиг от текущей позиции, offset может быть отрицательным
fseek(f, offset, SEEK_END)от конца: fseek(f, 0, SEEK_END) — в конец
ftell(f)текущая позиция, long; после fseek(f, 0, SEEK_END) — размер файла
rewind(f)в начало, то же, что fseek(f, 0, SEEK_SET)

Запись номер index начинается с байта index * sizeof(Student), число записей — размер файла, делённый на sizeof(Student). Чтобы изменить одну запись, файл открывают режимом "r+b" — чтение и запись без стирания — и пишут запись на её место:

/* Прочитать запись номер index; false — такой записи нет */
bool read_record(FILE *f, long index, Student *s)
{
    if (fseek(f, index * (long)sizeof *s, SEEK_SET) != 0) {
        return false;
    }
    return fread(s, sizeof *s, 1, f) == 1;
}

/* Записать запись номер index на её место */
bool write_record(FILE *f, long index, const Student *s)
{
    if (fseek(f, index * (long)sizeof *s, SEEK_SET) != 0) {
        return false;
    }
    return fwrite(s, sizeof *s, 1, f) == 1;
}
$ ./direct students.dat 1 4.10
before: Petrova 21V 3.80
after:  Petrova 21V 4.10
$ ./direct students.dat 5 4.10
No record 5
Изменение одной записи стоит одного fseek и одного fwrite при любом размере файла. Текстовый файл пришлось бы переписать целиком: запись другой длины сдвигает все строки после неё. На этом принципе — записи фиксированной длины и доступ по номеру — построены файлы баз данных.
05

Побайтовая обработка

Любой файл — картинка, архив, exe — это последовательность байтов. Программа, которая не знает его формат, может читать его по байтам: fgetc(f) возвращает очередной байт как число 0…255 или EOF (−1) в конце. Результат хранят в int, не в char: иначе байт 0xFF станет равен EOF и чтение оборвётся на нём.

int c;
while (offset < limit && (c = fgetc(f)) != EOF) {   /* c — int: EOF не равен ни одному байту */
    line[used++] = (unsigned char)c;
    offset++;
    if (used == PER_LINE) {
        print_line(offset - used, line, used);
        used = 0;
    }
}

Это цикл программы hexdump (examples/14/hexdump.c), печатающей дамп из пункта 3. На текстовом файле из одной строки Hi! видно, что перевод строки — тоже байт, 0a:

00000000  48 69 21 0a                                      |Hi!.|

Для больших файлов fgetc медленнее чтения блоками: копирование читает fread по 4096 байтов с размером элемента 1 — тогда результат fread равен числу байтов, и последний неполный блок не теряется:

unsigned char buffer[4096];
size_t n;
while ((n = fread(buffer, 1, sizeof buffer, in)) > 0) {
    if (fwrite(buffer, 1, n, out) != n) {
        /* ошибка записи: сообщить, закрыть оба файла, вернуть 1 */
    }
    total += (long long)n;
}

Формат файла определяют не по расширению, а по сигнатуре — первым байтам, которые формат требует:

ФорматПервые байтыСимволами
PNG89 50 4E 47 0D 0A 1A 0A.PNG....
PDF25 50 44 46%PDF
ZIP (а также docx, xlsx)50 4B 03 04PK..
BMP42 4DBM
exe Windows4D 5AMZ

Программа examples/14/filecopy.c копирует файл и сравнивает его первые байты с таблицей через memcmp:

$ ./filecopy students.dat copy.dat
192 bytes copied, type: unknown
$ ./filecopy fake.png fake2.png
8 bytes copied, type: PNG image
$ ./filecopy missing.bin x.bin
missing.bin: No such file or directory

Байты PNG 0D 0A и 1A выбраны нарочно: файл, скопированный в текстовом режиме Windows, сразу видно по испорченной сигнатуре (пункт 1).

06

Конец файла или ошибка

Цикл чтения заканчивается, когда fread вернул меньше запрошенного или fgetc вернул EOF. Причин две: данные кончились или чтение сломалось (диск, сеть, флешку вынули). Различают их после цикла:

ФункцияИстина, если
feof(f)попытка чтения упёрлась в конец файла
ferror(f)произошла ошибка чтения или записи
errno (errno.h)код последней ошибки системы; текст — strerror(errno), его же печатает perror
int failed = ferror(in);
fclose(in);
if (fclose(out) != 0 || failed) {
    fprintf(stderr, "Copy failed\n");
    return 1;
}

Как и в разделе 13, feof — не условие цикла: он становится истинным только после неудачного чтения. Условие цикла — результат самой функции чтения, а feof и ferror отвечают на вопрос «почему цикл кончился».

Ключевые выводы раздела

Запомните главное

  • Двоичный файл хранит байты памяти; режим — с буквой b, иначе Windows исказит байты 0x0A и 0x1A
  • fwrite и fread возвращают число целых элементов, а не байтов; результат проверяют всегда
  • Указатели в файл не пишут; строки в записи — массивы char фиксированного размера
  • Структура в файле — с выравниванием и порядком байтов своей платформы: такой файл читает только «своя» программа
  • Запись № i — с байта i * sizeof: fseek + fread; изменение на месте — режим "r+b"
  • Размер файла — fseek(f, 0, SEEK_END) и ftell(f); число записей — размер / sizeof
  • Результат fgetc — в int; большие файлы копируют блоками fread(buf, 1, size, f)
  • Формат определяют по сигнатуре, а не по расширению
  • Почему кончилось чтение — feof или ferror после цикла; при записи проверяют и fclose
Связи раздела. Опирается на раздел 3 — размеры типов и sizeof; раздел 12 — шестнадцатеричная запись байтов; раздел 13 — структуры, fopen, perror, аргументы командной строки. Нужен для: раздела 15 — qsort записей, загруженных из файла; раздела 17 — загрузка файла неизвестной длины в динамический массив; раздела 19 — сохранение словаря на диск.
Практические работы №12, №13 и тест по разделу 14 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Экзамен по блоку 1 Все разделы Раздел 15: Функции подробно