Всё, что программа вычислила, исчезает при её завершении, если только она это не записала. Файловый интерфейс C живёт в <stdio.h> — том же заголовке, что и printf, — и сходство неслучайно: файл — это поток байтов, а у функций, работающих с экраном, есть близнецы, принимающие поток аргументом.
Тип дескриптора — FILE *. Внутрь FILE вы никогда не заглядываете; это непрозрачный тип (см. typedef), и вы всегда держите только указатель на него.
Три шага
Любая операция с файлом устроена одинаково: открыть, использовать, закрыть.
Проверка на NULL — не лишняя паранойя. fopen завершается неудачей, когда файла не существует (в режиме чтения), когда каталог недоступен для записи, когда путь неверен или когда у процесса кончились дескрипторы, — и в каждом из этих случаев возвращается NULL. Использование NULL-дескриптора — это ошибка сегментации.
Отправлять сообщение об ошибке в stderr, а не в stdout, — общепринятая практика: этот поток не буферизуется и его можно перенаправить отдельно от полезного вывода.
Режимы fopen
Второй аргумент — короткая строка режима. Ошибка в ней — самая разрушительная во всей этой области, потому что "w" молча опустошает существующий файл.
| Режим | Чтение | Запись | Если файл существует | Если не существует |
|---|---|---|---|---|
"r" | да | нет | открывает с начала | неудача, возвращает NULL |
"w" | нет | да | обрезается до нуля | создаётся |
"a" | нет | да | пишет только в конец | создаётся |
"r+" | да | да | открывает с начала | неудача, возвращает NULL |
"w+" | да | да | обрезается до нуля | создаётся |
"a+" | да | да | читает где угодно, пишет в конец | создаётся |
Добавьте b к любому из них ("rb", "wb", "ab+") для бинарного режима — о нём в конце.
Два правила, спасающих реальные данные от потери:
- Используйте
"r", когда собираетесь читать."r+"с опечаткой в имени файла безопасно завершится неудачей;"w+"создаст пустой файл, и вы потеряете лишь время. А вот"w"с правильным именем файла, когда вы имели в виду"r", уничтожит данные. - Используйте
"a"для логов. Каждыйfprintfпопадёт в конец независимо от того, где стояла позиция потока, — ровно то, что нужно журналу.
Запись и чтение в одной программе
Редактор ниже выполняет полный круг: создаёт файл, записывает в него записи, закрывает, снова открывает на чтение и выводит найденное.
fprintf и fscanf — это printf и scanf с потоком в качестве первого аргумента; всё, что касается их спецификаторов формата, совпадает, включая ширину %31s, которая не даёт name переполниться.
Условие цикла — == 2, число элементов, которые запрашивает формат. Сравнение с EOF вместо этого — классический баг: на некорректной строке fscanf вернёт 0, а не EOF, и цикл будет крутиться вечно на вводе, который он не может поглотить.
Построчное чтение с fgets
fscanf удобен для жёстко форматированных данных. Для текстовых файлов — конфигураций, логов, CSV, всего, что писал человек, — читайте целыми строками. Вот канонический цикл:
Почему fgets — правильный выбор по умолчанию:
- Он принимает размер буфера, поэтому не может его переполнить. Передавайте
sizeof line— и вызов останется корректным, даже если вы измените размер массива. - Он возвращает
NULLв конце файла или при ошибке, что даёт чистое условие цикла. - Он сохраняет перевод строки, когда строка поместилась в буфер. Это полезно: отсутствие
'\n'в полученном означает, что строка оказалась длиннее буфера и остаток ещё ждёт.strcspn(line, "\n")находит индекс перевода строки (или длину строки, если его нет), поэтому запись'\0'по этому индексу обрезает её в обоих случаях.
Чтобы отличить настоящий конец файла от ошибки, спросите после цикла:
if (ferror(in)) {
fprintf(stderr, "ошибка чтения\n");
} else if (feof(in)) {
/* нормальное завершение */
}
Не пишите while (!feof(fp)) как условие цикла. feof становится истинным только после того, как чтение уже завершилось неудачей, поэтому такой цикл обрабатывает содержимое последнего буфера лишний раз. Проверяйте вместо этого возвращаемое значение функции чтения — как делают оба цикла выше.
По одному символу: fgetc и fputc
Для побайтовой работы — подсчёта символов, преобразования файла, копирования — fgetc и fputc обрабатывают по одному символу за вызов.
Одна деталь, которая важна: c объявлен как int, а не как char. fgetc возвращает int, чтобы иметь возможность вернуть любое возможное значение байта и отдельный признак EOF (равный -1). Если хранить результат в char, сравнение с EOF окажется либо всегда ложным, либо ложно-истинным для байта 0xFF — в зависимости от того, знаковый ли char на вашей платформе. Это одна из старейших ловушек C.
Как правильно проверять ошибки
Промышленное чтение выглядит так:
errno хранит код, описывающий последнюю неудачу, а strerror превращает его во фразу; perror выводит ваше сообщение вместе с этой фразой за один вызов. Им нужны <errno.h> и <string.h> соответственно.
fclose тоже может завершиться неудачей — он сбрасывает буферизованные данные, а запись может не поместиться на диск, — поэтому для чего-то важного проверяйте и его:
if (fclose(fp) != 0) {
fprintf(stderr, "не удалось сбросить буфер и закрыть файл\n");
}
Бинарный режим в одном абзаце
Текстовый режим может преобразовывать окончания строк (в Windows \n превращается в \r\n при записи и обратно при чтении) и может особым образом трактовать некоторые байты. Для данных, которые не являются текстом — изображения, структуры, выгруженной как есть, сжатого блоба, — открывайте с b и используйте fread/fwrite, которые двигают сырые байты:
fwrite(ptr, size, count, fp) записывает count элементов по size байт и возвращает, сколько ему удалось записать; fread работает зеркально. Учтите, что записанный так файл привязан к машине, которая его записала: выравнивание в структуре, размер целых и порядок байтов — всё это просачивается в байты, — поэтому он годится для кеша или временного файла и не годится для формата, который должны читать другие программы.
Частые ошибки
- Не проверять результат
fopenнаNULL. В последующем падении винят чтение, а не открытие. - Открыть с
"w", когда имелось в виду"r". Файл опустеет раньше, чем вы это заметите. - Забыть
fclose. Буферизованный вывод теряется, а каждый незакрытый файл утекает дескриптором. while (!feof(fp)). Обрабатывает последнюю строку дважды. Проверяйте вместо этого вызов чтения.char c = fgetc(fp). Ломает сравнение сEOF. Используйтеint.fscanf("%s", buf)без ширины. То же переполнение буфера, что и у scanf с клавиатуры.- Относительные пути.
fopen("data.txt", "r")ищет в рабочем каталоге, а не рядом с исполняемым файлом. Если файл «пропал», обычно причина именно в этом.
Часто задаваемые вопросы
Как открыть файл в C?
FILE *fp = fopen("data.txt", "r"); открывает файл на чтение и возвращает дескриптор FILE * либо NULL, если открыть не удалось. Всегда проверяйте результат на NULL перед использованием и вызывайте fclose(fp), когда закончили.
Какие бывают режимы fopen в C?
"r" — чтение (файл должен существовать), "w" — запись (создаёт файл или обрезает существующий до нуля), "a" — дозапись (создаёт файл, пишет в конец). Добавление + делает каждый режим чтением-и-записью: "r+", "w+", "a+". Добавление b ("rb", "wb") открывает файл в бинарном режиме.
Как прочитать файл построчно в C?
Используйте fgets в цикле while: while (fgets(line, sizeof line, fp) != NULL) { ... }. Функция останавливается на каждом переводе строки или когда буфер заполнен, возвращает NULL в конце файла и не может переполнить буфер, потому что вы передаёте его размер.
Почему мой файл пустой после записи в C?
Чаще всего вы забыли fclose. Вывод буферизуется, поэтому данные могут всё ещё лежать в памяти, когда программа аварийно завершается. fclose сбрасывает буфер и закрывает файл; fflush(fp) сбрасывает буфер без закрытия. Вторая причина — повторное открытие с "w", которое обрезает файл, только что вами записанный.