Перейти к основному содержимому

Файловый ввод-вывод

Разработчику Архитектору

Файловый ввод-вывод

Программа на С взаимодействует с диском через потоки (streams) из <stdio.h> — абстракцию "последовательность байт с буфером". Консоль — те же потоки — stdin (ввод), stdout (вывод), stderr (ошибки).

Файл на диске открывают функцией fopen; она возвращает указатель FILE * — дескриптор потока в памяти программы. Закрытие fclose сбрасывает буферы и освобождает ресурс ОС.

Подробные сигнатуры — в справочнике. Ошибки — по идиомам.


Термины

ТерминОбъяснение
FILE *объект потока; не путать с "файлом" на диске
Текстовый режимвозможны преобразования \n\r\n (Windows)
Бинарный режим ("rb")байты как есть
Буферизациябиблиотека копит данные пачками для скорости
EOFконец файла при чтении

Открытие и закрытие

#include <stdio.h>

FILE *f = fopen("data.txt", "r");
if (f == NULL) {
perror("data.txt");
return 1;
}

/* ... */

fclose(f);

Разбор:

  • fopen("data.txt", "r") открывает поток чтения и возвращает FILE *, связанный с файловым дескриптором ОС.
  • Проверка f == NULL обрабатывает ошибки доступа, отсутствия файла или нехватки дескрипторов до основной логики.
  • perror("data.txt") печатает контекст и системное описание ошибки на основе текущего errno.
  • fclose(f) завершает поток, сбрасывает буфер и освобождает связанные ресурсы.

Режимы второго аргумента:

РежимНазначение
"r"чтение, файл должен существовать
"w"запись, создать или обнулить
"a"дозапись в конец
"rb", "wb"бинарный (важно на Windows)
"r+", "w+"чтение и запись

fopen возвращает NULL при ошибке; детали — errno и perror/strerror.

Дозапись в конец файла:

Код ITЗагрузка примера кода…

Разбор:

  • Режим "a" (append) открывает файл для дозаписи в конец без обнуления существующего содержимого.
  • fprintf возвращает число записанных символов; отрицательное значение сигнализирует ошибку записи.
  • fclose сбрасывает буфер и освобождает дескриптор потока.

Разбор строки:

FILE *f = fopen("data.txt", "r");

Разбор:

  • Вызов создаёт только логический поток чтения; фактические операции с диском начнутся при fgets/fread.

  • Путь "data.txt" разрешается относительно текущей рабочей директории процесса, а не относительно исходного файла .c.

  • Режим "r" запрещает запись и требует существования файла в момент открытия.

  • "data.txt" — путь относительно текущей рабочей директории процесса (откуда запустили программу), если не указан полный путь;

  • "r" — только чтение; файл должен существовать;

  • f — либо валидный поток, либо NULL.


Текстовый ввод-вывод

Построчное чтение:

char line[256];
while (fgets(line, sizeof line, f) != NULL) {
/* line может содержать '\n' */
}

Разбор:

  • char line[256] ограничивает буфер и задаёт верхнюю границу длины считываемой строки.
  • fgets(line, sizeof line, f) читает не более 255 символов плюс \0, что защищает от переполнения памяти.
  • Цикл while (... != NULL) естественно останавливается на EOF или ошибке, позволяя обрабатывать файл построчно.
  • Содержимое line может включать символ новой строки, поэтому при парсинге часто делают дополнительную нормализацию.

fgets ограничивает длину — защита от переполнения буфера. Не путать с небезопасным gets (удалён из стандарта).

Возвращаемое значение fgets:

  • указатель line — строка прочитана (может включать \n в конце);
  • NULL — конец файла или ошибка; отличить: feof(f) и ferror(f).
if (fgets(line, sizeof line, f) == NULL) {
if (ferror(f))
perror("read");
/* feof — нормальный конец файла */
}

Разбор:

  • Этот шаблон разделяет два сценария NULL: нормальный конец файла и реальную ошибку чтения.
  • ferror(f) указывает на I/O-сбой (например, ошибка носителя), а feof(f) фиксирует штатное достижение конца.
  • Явная развилка нужна для корректной диагностики и для отличия "прочитали всё" от "сломалось чтение".

Форматированный вывод:

fprintf(f, "id=%d name=%s\n", id, name);

Разбор:

  • fprintf записывает форматированную строку напрямую в файловый поток f.
  • %d и %s подставляют значения переменных с контролем формата, поэтому результат удобен для машинной и ручной обработки.
  • Символ \n завершает строку и часто провоцирует быстрый сброс буфера в построчном режиме.

Форматированный разбор — fscanf

fscanf читает из потока по той же строке формата, что scanf, но источник — файл, а не stdin:

int id = 0;
char name[64] = "";

if (fscanf(f, "%d %63s", &id, name) != 2) {
fprintf(stderr, "строка не разобрана (ожидались id и имя)\n");
}

Разбор:

  • Возвращаемое значение — число успешно сопоставленных полей; для шаблона %d %63s ожидается 2.
  • %63s ограничивает длину строки размером буфера name[64] (63 символа + \0).
  • Пробел в формате пропускает любое количество пробельных символов между полями.

Для произвольных текстовых файлов (логи, CSV с кавычками, разная кодировка) надёжнее fgets + ручной разбор или strtol/strtod с проверкой хвоста строки. fscanf удобен для учебных файлов с фиксированным шаблоном полей.

Запись символа и строки без форматирования: fputc, fputs.


Посимвольное чтение — fgetc и fputc

Когда строка не помещается в буфер целиком или нужен разбор "по символу", используют fgetc / fputc:

int ch;
while ((ch = fgetc(f)) != EOF) {
if (ch == '\n')
break;
fputc(ch, stdout);
}

Разбор:

  • fgetc возвращает код символа как unsigned char, приведённый к int, или EOF в конце файла/при ошибке.
  • Сравнение с EOF выполняют до приведения к char, иначе байт 0xFF на некоторых платформах спутают с концом файла.
  • fputc(ch, stdout) дублирует символ в стандартный вывод; для записи в файл передайте свой FILE *.

Для целых строк по-прежнему удобнее fgets (см. выше). Ошибки потока — ferror, perror (идиомы).


Копирование файла целиком

Учебный приём "прочитать весь файл и записать в другой" на практике делают блоками, а не посимвольно — так меньше системных вызовов:

Код ITЗагрузка примера кода…

Разбор:

  • Режим "rb" / "wb" сохраняет байты без преобразования перевода строк (важно на Windows).
  • Цикл читает порциями до sizeof buf; n == 0 при штатном EOF завершает цикл.
  • fwrite должен записать ровно n байт; иначе — ошибка диска или потока.
  • При ошибке закрывают оба потока; в продакшене удобнее паттерн goto cleanup из идиом.

Посимвольное копирование через fgetc/fputc проще для понимания, но медленнее на больших файлах:

int ch;
while ((ch = fgetc(in)) != EOF)
if (fputc(ch, out) == EOF) { perror("write"); break; }

Размер файла перед выделением памяти можно оценить через fseek/ftell (см. ниже), но загружать весь файл в RAM имеет смысл только если размер известен и укладывается в доступную память.


Бинарный ввод-вывод

В бинарном режиме байты передаются без преобразования перевода строк. Блоковые операции:

size_t n = fread(buffer, 1, sizeof buffer, f);
if (n == 0 && ferror(f)) { /* ошибка чтения */ }

size_t w = fwrite(data, elem_size, count, f);

Разбор:

  • fread(buffer, 1, sizeof buffer, f) читает блок байтов и возвращает фактическое число прочитанных элементов.
  • Пара elem_size и count в fwrite определяет логическую структуру записи; проверять нужно количество элементов, а не только байты.
  • При неполном результате необходимо проверять ferror/feof и решать, можно ли продолжать обработку.

fread/fwrite возвращают число успешно обработанных элементов (не байт, если elem_size > 1). Всегда сравнивать с ожидаемым count.

Пример: прочитать ровно 100 байт в буфер:

size_t got = fread(buf, 1, 100, f);
if (got != 100) {
if (ferror(f)) { /* ошибка диска */ }
else if (feof(f)) { /* файл короче 100 байт */ }
}

Разбор:

  • Код жёстко проверяет контракт "прочитать ровно 100 байт" через сравнение got != 100.
  • Ветка ferror(f) сигнализирует ошибку устройства/файловой системы, а feof(f) — что файл просто закончился раньше.
  • Такая развилка полезна для бинарных протоколов, где неполный блок обычно считается повреждением или неполными данными.

На Windows для бинарных форматов (картинки, .db) режим "rb" / "wb" обязателен — иначе байт 0x1A может интерпретироваться особым образом в текстовом режиме.


Буферизация

Библиотека буферизует поток для производительности. Режимы:

  • полная буферизация;
  • построчная (терминал);
  • без буфера.
fflush(f); /* сбросить буфер записи на диск/ОС */
setvbuf(f, ...); /* настроить при открытии */

Разбор:

  • fflush(f) принудительно передаёт буферизованные данные ниже по стеку I/O, уменьшая риск потери при аварийном завершении.
  • setvbuf позволяет управлять стратегией буферизации и балансом между скоростью и латентностью записи.
  • В критичных сценариях логирования этот контроль помогает точнее управлять моментом фактического появления данных в файле.

При аварийном завершении без fclose/fflush часть данных может остаться в буфере. Для критичных записей — fflush после важных блоков или открытие с немедленной записью.


Позиционирование

fseek(f, 0, SEEK_END);
long size = ftell(f);
rewind(f);

fseek(f, offset, SEEK_SET);

Разбор:

  • fseek(..., SEEK_END) переносит позицию в конец файла, чтобы затем получить размер через ftell.
  • rewind(f) возвращает курсор в начало и одновременно сбрасывает индикаторы EOF/ошибки потока.
  • fseek(f, offset, SEEK_SET) даёт случайный доступ по абсолютному смещению и полезен для обновления фиксированных записей.

Для файлов больше 2 ГБ на 32-битных платформах используют fseeko/ftello (POSIX). Случайный доступ удобен для индексированных форматов и обновления записи фиксированной длины.


Сериализация структур

Запись struct целиком через fwrite(&obj, sizeof obj, 1, f) не переносима между компиляторами, архитектурами и версиями из-за выравнивания и порядка байтов (endianness).

Переносимые варианты:

  • поля по одному в известном порядке и фиксированных типах (uint32_t из <stdint.h>);
  • текстовые форматы (CSV, JSON через внешнюю библиотеку);
  • явный бинарный протокол с магическим числом и версией заголовка.

Для учебных проектов на одной машине допустим "сырой" дамп, если файл не уходит на другую платформу.

Переносимая запись полей по одному:

Код ITЗагрузка примера кода…

Разбор:

  • Типы uint32_t/uint16_t фиксируют размер полей и делают формат предсказуемым между платформами.
  • Каждое поле пишется отдельным fwrite, поэтому порядок байтов и выравнивание контролируются явно.
  • Проверка != 1 после каждой записи позволяет остановиться при первой ошибке диска.

Строки как потоки — sprintf / sscanf

Форматирование в память:

char buf[64];
snprintf(buf, sizeof buf, "value=%d", x);

int x;
sscanf(buf, "value=%d", &x);

Разбор:

  • snprintf ограничивает запись размером буфера и защищает от переполнения строки при форматировании.
  • Формат "value=%d" задаёт стабильный шаблон сериализации числа в текст.
  • sscanf выполняет обратный разбор по тому же шаблону, а &x передаёт адрес для записи результата.
  • Надёжный код дополнительно проверяет возвращаемое значение sscanf на число успешно прочитанных полей.

snprintf ограничивает размер — предпочтительнее sprintf. Аналогично для разбора — проверять код возврата sscanf (число сопоставленных полей).


Время и псевдослучайные числа

Заголовок <time.h> даёт календарное время; его же часто передают в srand из <stdlib.h>, чтобы последовательность rand() менялась от запуска к запуску.

Код ITЗагрузка примера кода…

Разбор:

  • time(NULL) возвращает секунды с эпохи Unix; значение подходит для меток в логах (fprintf в файл — см. пример с "a" выше).
  • localtime переводит time_t в разобранные поля; указатель может указывать на статический буфер — не сохраняйте его между вызовами без копирования.
  • strftime форматирует дату по шаблону (аналог printf для struct tm).
  • srand((unsigned)time(NULL)) задаёт зерно генератора; rand() % 6 + 1 даёт число 1…6 для простых игр.

Подробнее про игровой цикл и проверку ввода — в примерах на С. Для криптографии rand() не подходит — нужны специализированные API ОС или библиотеки.


Связь с другими темами

См. также: Память процесса, Преобразование в исполняемый файл.


Что попробовать

  1. Скопируйте текстовый файл функцией copy_file и сравните результат с fc / diff.
  2. Запишите в log.txt три строки с меткой времени через fprintf и режим "a".
  3. Разберите учебный файл формата 42 Alice через fscanf и выведите поля на экран.
  4. После намеренно неверного пути к файлу посмотрите вывод perror и значение errno (идиомы).