Записи фиксированного размера, прямой доступ к любой из них и обработка файла по байтам
fwrite и загружать его fread, проверяя результатfseek, не загружая весь файлВ разделе 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, sscanf | fwrite, fread, fgetc |
| Найти запись № i | прочитать i строк | fseek на i * sizeof |
'\n' в Windows | пишется как \r\n, читается как \n | байт пишется как есть |
| Кто прочтёт | человек, Excel, любая программа | программа, знающая формат |
b обязательна для любого нетекстового файла. В Linux режимы "r" и "rb" одинаковы, а в Windows текстовый режим заменяет байт 0x0A на пару 0x0D 0x0A при записи и считает байт 0x1A концом файла при чтении. Картинка, скопированная в текстовом режиме, в Linux получается целой, а в Windows испорчена.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
fread просит до max записей, а возвращает, сколько было в файле, — так загружается файл неизвестной длины. Результат меньше запрошенного означает конец файла или ошибку; что именно — скажет ferror (пункт 6).fclose тоже проверяют: данные лежат в буфере и уходят на диск при закрытии — ошибка «диск заполнен» может проявиться только там.char фиксированного размера, а не char *.Посмотрим на первые 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 |...............@|
name[41]: Ivanov, '\0' и остаток массива. Массив пишется целиком, даже пустая его часть.group[11]: 21V с 0x29.double начинался с адреса, кратного 8. Поэтому sizeof(Student) = 64, а не 41 + 11 + 8 = 60.score = 4.75. Это 0x4013000000000000, а в файле 00 … 00 13 40: процессоры x86 и ARM хранят число младшим байтом вперёд (little-endian).int и long — от системы (long в Windows 4 байта, в Linux 8). Такой файл надёжно читает только программа, собранная из того же описания структуры тем же компилятором. Форматы для обмена (PNG, ZIP) описывают каждое поле побайтно: размер, порядок байтов, смещение — и читают их по байтам, как в пункте 5.У открытого файла есть текущая позиция — номер байта, с которого начнётся следующее чтение или запись. После каждого 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 за конец файла ошибкой не считается — ошибку покажет fread, который вернёт 0. Поэтому «нет записи 5» определяется по результату чтения.fseek на n * sizeof и fwrite) дописывает запись; дальше конца — оставляет «дыру» из нулей. Новые записи добавляют в конец."r+b" между чтением и записью (и наоборот) обязателен fseek или fflush — так требует стандарт C. В функциях выше fseek стоит перед каждой операцией, и правило выполняется само.fseek и одного fwrite при любом размере файла. Текстовый файл пришлось бы переписать целиком: запись другой длины сдвигает все строки после неё. На этом принципе — записи фиксированной длины и доступ по номеру — построены файлы баз данных.Любой файл — картинка, архив, 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;
}
Формат файла определяют не по расширению, а по сигнатуре — первым байтам, которые формат требует:
| Формат | Первые байты | Символами |
|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A | .PNG.... |
25 50 44 46 | %PDF | |
| ZIP (а также docx, xlsx) | 50 4B 03 04 | PK.. |
| BMP | 42 4D | BM |
| exe Windows | 4D 5A | MZ |
Программа 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).
Цикл чтения заканчивается, когда 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 и 0x1Afwrite и fread возвращают число целых элементов, а не байтов; результат проверяют всегдаchar фиксированного размераi * sizeof: fseek + fread; изменение на месте — режим "r+b"fseek(f, 0, SEEK_END) и ftell(f); число записей — размер / sizeoffgetc — в int; большие файлы копируют блоками fread(buf, 1, size, f)feof или ferror после цикла; при записи проверяют и fclosesizeof; раздел 12 — шестнадцатеричная запись байтов; раздел 13 — структуры, fopen, perror, аргументы командной строки. Нужен для: раздела 15 — qsort записей, загруженных из файла; раздела 17 — загрузка файла неизвестной длины в динамический массив; раздела 19 — сохранение словаря на диск.