Файловый ввод-вывод
Файловый ввод-вывод
Программа на С взаимодействует с диском через потоки (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 ОС или библиотеки.
Связь с другими темами
- Логи и конфиги на диске — те же потоки; большие данные — встраиваемая БД.
- Дескрипторы низкого уровня (
open,read,write) — в системном программировании, поверх них часто строятstdio.
См. также: Память процесса, Преобразование в исполняемый файл.
Что попробовать
- Скопируйте текстовый файл функцией
copy_fileи сравните результат сfc/diff. - Запишите в
log.txtтри строки с меткой времени черезfprintfи режим"a". - Разберите учебный файл формата
42 Aliceчерезfscanfи выведите поля на экран. - После намеренно неверного пути к файлу посмотрите вывод
perrorи значениеerrno(идиомы).