Файлы в C: fopen, fgets, fprintf и проверка ошибок

Работа с файлами в C хорошо показывает общий стиль языка: функция возвращает результат, вы проверяете ошибку, работаете с ресурсом и закрываете его. Здесь нет магии: если fopen не смог открыть файл, вы получите NULL, и дальше нужно остановиться, а не продолжать запись в пустой указатель

В этом уроке мы создадим файл notes.txt, запишем в него строки через fprintf, закроем, снова откроем и прочитаем построчно через fgets

Что получится в конце

Файл file_io.c:

#include <stdio.h>

int main(void)
{
    FILE *out = fopen("notes.txt", "w");
    if (out == NULL) {
        printf("Cannot open file for writing\n");
        return 1;
    }

    fprintf(out, "C file lesson\n");
    fprintf(out, "Line number: %d\n", 2);

    fclose(out);

    FILE *in = fopen("notes.txt", "r");
    if (in == NULL) {
        printf("Cannot open file for reading\n");
        return 1;
    }

    char line[128];
    while (fgets(line, sizeof(line), in) != NULL) {
        printf("%s", line);
    }

    fclose(in);

    return 0;
}

Сборка:

gcc -Wall -Wextra -std=c17 file_io.c -o file_io
./file_io

После запуска рядом с программой появится файл notes.txt

FILE* — это указатель на поток

Строка:

FILE *out = fopen("notes.txt", "w");

открывает файл и возвращает указатель на поток. Через этот указатель функции стандартной библиотеки понимают, куда писать или откуда читать

FILE — тип из <stdio.h>. Внутреннее устройство FILE не нужно трогать руками. Вы работаете с ним через функции: fopen, fprintf, fgets, fclose

Режим "w" означает запись. Если файл уже существует, он будет перезаписан. Это важный момент: "w" может уничтожить старое содержимое файла

Проверка fopen

После fopen проверка обязательна:

if (out == NULL) {
    printf("Cannot open file for writing\n");
    return 1;
}

Файл может не открыться по разным причинам: нет прав, неверный путь, папки не существует, файл занят, диск недоступен. В C функция не бросает исключение, а возвращает значение, которое нужно проверить

Если пропустить проверку и вызвать fprintf(out, ...), когда out == NULL, программа может упасть

fprintf: printf, но в файл

fprintf похож на printf, только первым аргументом получает поток:

fprintf(out, "Line number: %d\n", 2);

printf фактически пишет в стандартный вывод, а fprintf может писать в файл, stderr или другой поток

После записи мы закрываем файл:

fclose(out);

Закрытие важно не только «для порядка». Библиотека может буферизовать вывод, то есть временно держать часть данных в памяти. fclose завершает работу с потоком и дает библиотеке шанс корректно дописать данные

fgets: читаем строку с ограничением

Для чтения используем:

char line[128];
while (fgets(line, sizeof(line), in) != NULL) {
    printf("%s", line);
}

fgets получает буфер, размер буфера и поток. Она читает не больше указанного размера, поэтому лучше подходит для учебного безопасного чтения, чем функции без ограничения длины

Если строка в файле длиннее буфера, fgets прочитает ее кусками. Для первого урока это не проблема, но в реальной программе нужно учитывать длинные строки

fgets сохраняет перевод строки, если он поместился в буфер. Поэтому в примере printf("%s", line); идет без дополнительного \n

Режимы открытия файла

Минимально нужно знать три режима:

РежимЧто делает
"r"открыть существующий файл для чтения
"w"открыть файл для записи и очистить старое содержимое
"a"открыть файл для добавления в конец

Если хотите добавить строку в конец, используйте:

FILE *log = fopen("notes.txt", "a");

Но проверка остается такой же:

if (log == NULL) {
    printf("Cannot open file\n");
    return 1;
}

Как изменить пример под мини-проект

Сделайте программу дневника:

  1. Открыть notes.txt в режиме "a"
  2. Спросить у пользователя одну строку
  3. Записать ее через fprintf
  4. Закрыть файл
  5. Открыть файл в режиме "r"
  6. Напечатать все строки

Для строки используйте:

char note[128];

printf("Note: ");
if (fgets(note, sizeof(note), stdin) == NULL) {
    printf("Input error\n");
    return 1;
}

Так вы соедините ввод с клавиатуры и файловый вывод, не выходя за пределы уже знакомых функций

Мини-практика: журнал событий

Запись в конец файла часто нужна для логов. Сделаем маленькую функцию append_log, которая добавляет одну строку в файл

#include <stdio.h>

int append_log(const char *message)
{
    FILE *log = fopen("app.log", "a");
    if (log == NULL) {
        return 0;
    }

    fprintf(log, "%s\n", message);
    fclose(log);

    return 1;
}

Использование:

if (!append_log("Program started")) {
    printf("Cannot write log\n");
    return 1;
}

Режим "a" не очищает старый файл, а добавляет новые строки в конец. Это удобно для журнала, но не подходит, если вы хотите каждый запуск создавать файл заново. Тогда нужен режим "w"

Почему текущая папка важна

Когда вы пишете:

fopen("notes.txt", "r");

путь считается относительно текущей рабочей папки процесса, а не обязательно относительно файла file_io.c. Если запускаете программу из терминала, текущая папка обычно понятна. Если запускаете из IDE, она может быть настроена отдельно

Поэтому ошибка «файл не найден» иногда не связана с C-кодом. Программа просто ищет файл не там, где вы думаете. Для проверки временно создайте файл через саму программу в режиме "w" и посмотрите, где он появился

Как аккуратно обрабатывать ошибку записи

В первых примерах мы проверяем только fopen, но запись тоже может завершиться ошибкой. fprintf возвращает отрицательное значение при ошибке вывода

if (fprintf(out, "Hello\n") < 0) {
    printf("Write failed\n");
    fclose(out);
    return 1;
}

Для учебной программы это может быть лишней деталью, но полезно знать направление роста: надежная работа с файлами — это не только открыть и закрыть, но и проверять важные операции между ними

Текстовый файл и бинарный файл

В этом уроке мы работаем с текстом. Его можно открыть в редакторе и прочитать глазами. Для новичка это лучший формат: ошибки видны сразу

Бинарные файлы читают и пишут иначе, часто через fread и fwrite. Их не стоит смешивать с первым уроком по fopen, потому что там быстрее появляются вопросы переносимости, размеров типов и формата данных. Сначала уверенно разберитесь с текстовыми строками

Как читать файл с номерами строк

Иногда полезно печатать не только текст, но и номер строки. Это маленькое улучшение показывает, как соединить fgets, счетчик и вывод:

char line[128];
int line_number = 1;

while (fgets(line, sizeof(line), in) != NULL) {
    printf("%d: %s", line_number, line);
    line_number++;
}

Если файл содержит три строки, вывод покажет 1:, 2:, 3:. Такой прием удобен для простых логов, учебных конфигов и диагностики. Заодно он закрепляет правило: чтение файла — это обычно цикл, который продолжается, пока функция чтения возвращает успешный результат

Что делать с очень длинной строкой

Если строка длиннее буфера, fgets прочитает только часть. Остаток будет прочитан следующим вызовом. Это не ошибка fgets, а нормальное поведение при ограниченном буфере

Для первых материалов достаточно выбрать буфер с запасом и понимать ограничение. В реальном парсере нужно проверять, поместился ли символ \n в прочитанную строку. Если не поместился, значит строка была длиннее буфера, и остаток еще лежит во входном потоке

Частые ошибки и порядок проверки

Не проверили fopen Всегда проверяйте FILE * на NULL. Это главная защита при работе с файлами

Открыли через w и потерли файл Режим "w" очищает файл. Для добавления в конец используйте "a"

Забыли fclose Закрывайте поток после работы. Это освобождает ресурс и помогает корректно завершить запись

Добавили лишний перевод строки при печати fgets fgets часто сохраняет \n, поэтому printf("%s\n", line); может давать пустые строки между строками файла

Что может быть еще интересно по этой теме

Можно ли читать файл посимвольно? Да, для этого есть функции вроде fgetc. Но для текстовых учебных задач построчное чтение через fgets обычно понятнее

Как узнать причину ошибки открытия? Можно подключить <errno.h> и использовать функции для диагностики ошибок. В первом уроке достаточно показать понятное сообщение и остановиться

Где создается notes.txt? В текущей рабочей папке программы. Если запускаете из IDE, текущая папка может отличаться от папки с исходником

Что дальше после файлов? Можно соединить struct и файлы: хранить записи в структуре, печатать их и сохранять в текстовый файл

Что почитать дальше по C

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x