Работа с файлами в C: как создать, прочитать, дописать и удалить файл

Чтобы работать с файлом в C, откройте его через fopen, проверьте указатель на NULL, читайте и пишите через fgets, fprintf, fread и fwrite, а затем закройте fclose.

В статье 12 программ с настоящим выводом: создание и дозапись, размер, копирование, удаление, разбор CSV и ошибка с while (!feof).

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 3057 статей автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3711 экспертных мнений Подписаться на Телеграм-канал
Обложка: Работа с файлами в C: как создать, прочитать, дописать и удалить файл

Работа с файлами в C строится на шести вызовах из <stdio.h>. Откройте файл функцией fopen(путь, режим), проверьте, что она вернула не NULL, читайте или пишите через fgets, fprintf, fread, fwrite и закройте файл fclose. Режим "w" создаёт файл или стирает старый, "a" дописывает в конец, "r" только читает.

Ниже рабочие примеры на каждую задачу: создать файл, прочитать его построчно, узнать размер, скопировать, удалить и разобрать CSV. Код написан на стандартном C11 и подходит для Linux, macOS и Windows, а особенность Visual Studio разобрана в разделе об ошибках. Если компилятора ещё нет, начните с инструкции как выбрать и установить компилятор C.

CheckroiCheckroiПодборка курсов по C (язык Си)15 курсов • 6 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Как создать файл в C и записать в него текст

Рой пишет программу на C, которая создаёт текстовый файл

Отдельной функции «создать файл» в стандартной библиотеке Си нет. Файл появляется, когда вы открываете его на запись.

  1. Подключите <stdio.h> и объявите указатель FILE *.
  2. Вызовите fopen("notes.txt", "w"). Если файла нет, он будет создан, если есть, его содержимое удалится.
  3. Сравните результат с NULL. При ошибке perror напечатает причину.
  4. Пишите строки через fprintf (с форматом, как printf) или fputs (готовую строку).
  5. Закройте файл fclose(f), чтобы данные из буфера ушли на диск.
#include <stdio.h>

int main(void) {
    FILE *f = fopen("notes.txt", "w");
    if (f == NULL) {
        perror("notes.txt");
        return 1;
    }
    fprintf(f, "Задача: %s\n", "купить молоко");
    fprintf(f, "Срок: %d дня\n", 2);
    fputs("Готово: нет\n", f);
    fclose(f);
    puts("Файл записан");
    return 0;
}

Программа печатает Файл записан, а рядом с ней появляется notes.txt из трёх строк. Относительный путь отсчитывается от текущей рабочей папки процесса, то есть от места, откуда вы запустили программу. Не нашли файл, ищите его там или передайте полный путь.

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

Для текста удобнее всего fgets. Она читает строку вместе с символом \n в буфер заданного размера и возвращает NULL, когда строки кончились.

#include <stdio.h>

int main(void) {
    FILE *f = fopen("notes.txt", "r");
    if (f == NULL) {
        perror("notes.txt");
        return 1;
    }
    char line[256];
    int n = 0;
    while (fgets(line, sizeof line, f) != NULL) {
        printf("%d: %s", ++n, line);
    }
    fclose(f);
    return 0;
}

Вывод:

1: Задача: купить молоко
2: Срок: 2 дня
3: Готово: нет

Буфер line здесь обычный массив символов. Как массив передаётся в функцию и почему sizeof line работает только рядом с объявлением, разобрано в статье о массивах и указателях в C. Если строка в файле длиннее буфера, fgets отдаст её частями за несколько вызовов, ничего не потеряется.

Кроме fgets, есть ещё два способа чтения:

  • fgetc читает по одному символу, подходит для подсчёта или посимвольной обработки;
  • fscanf читает по формату ("%d %s"), но на кривой строке легко сбивается, поэтому надёжнее связка fgets + sscanf, как в примере с CSV ниже.
Канал основателя Checkroi Вани Буявца3 700 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Режимы fopen: как дописать, очистить и не затереть файл

Второй аргумент fopen решает, что станет с файлом. Режимы описаны в справке cppreference к fopen:

Режим Что делает Файл есть Файла нет
"r" чтение читает с начала ошибка, NULL
"w" запись стирает содержимое создаёт
"a" дозапись в конец пишет в конец создаёт
"r+" чтение и запись читает с начала ошибка
"w+" чтение и запись стирает содержимое создаёт
"a+" чтение и дозапись пишет в конец создаёт

К режиму можно добавить b (двоичный файл) и, начиная с C11, x после w или w+. С флагом x вызов завершится ошибкой, если файл уже существует.

Дописать строку в конец

Режим "a" подходит для журналов: каждая запись встаёт в конец, старые строки остаются. По тому же принципу устроены лог-файлы программ и серверов. В Python у функции open() те же буквы режимов, это видно по статье как открыть файл в Python.

#include <stdio.h>

int main(void) {
    FILE *f = fopen("notes.txt", "a");
    if (f == NULL) {
        perror("notes.txt");
        return 1;
    }
    fputs("Добавлено позже\n", f);
    fclose(f);
    return 0;
}

После запуска append программа чтения выводит уже четыре строки:

1: Задача: купить молоко
2: Срок: 2 дня
3: Готово: нет
4: Добавлено позже

Очистить файл

Достаточно открыть его в режиме "w" и сразу закрыть: содержимое удалится, файл останется с размером 0 байт.

#include <stdio.h>

int main(void) {
    FILE *f = fopen("notes.txt", "w");
    if (f == NULL) {
        perror("notes.txt");
        return 1;
    }
    fclose(f);
    return 0;
}

Создать файл, только если его ещё нет

Режим "wx" защищает от случайной перезаписи, например конфига. Первый запуск создаёт файл, второй останавливается с ошибкой:

#include <stdio.h>

int main(void) {
    FILE *f = fopen("config.txt", "wx");
    if (f == NULL) {
        perror("config.txt уже есть");
        return 1;
    }
    fputs("port=8080\n", f);
    fclose(f);
    puts("config.txt создан");
    return 0;
}
config.txt создан
config.txt уже есть: File exists

Двоичные файлы: fwrite и fread

Две папки-файла: текстовый с листом и двоичный с ячейками байтов

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

  • fwrite(адрес, размер_элемента, количество, файл) возвращает, сколько элементов записано;
  • fread с теми же аргументами возвращает, сколько элементов прочитано, и это число стоит проверять.

Размер элемента считает оператор sizeof, поэтому код не зависит от того, сколько байт занимает int на вашей платформе.

#include <stdio.h>

struct point {
    int x;
    int y;
};

int main(void) {
    struct point out[3] = {{1, 2}, {3, 4}, {5, 6}};
    FILE *f = fopen("points.bin", "wb");
    if (f == NULL) {
        perror("points.bin");
        return 1;
    }
    size_t written = fwrite(out, sizeof out[0], 3, f);
    fclose(f);

    struct point in[3];
    f = fopen("points.bin", "rb");
    if (f == NULL) {
        perror("points.bin");
        return 1;
    }
    size_t read = fread(in, sizeof in[0], 3, f);
    fclose(f);

    printf("Записано %zu, прочитано %zu\n", written, read);
    for (size_t i = 0; i < read; i++) {
        printf("(%d, %d)\n", in[i].x, in[i].y);
    }
    return 0;
}
Записано 3, прочитано 3
(1, 2)
(3, 4)
(5, 6)

Зачем b, если на Linux и macOS разницы нет. По cppreference флаг ничего не меняет в POSIX-системах. А на Windows, по справке Microsoft, в текстовом режиме LF при записи превращается в CR-LF, а Ctrl+Z при чтении считается концом файла. Двоичные данные без b там портятся, поэтому ставьте флаг всегда, код станет переносимым.

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

Размер, копирование, удаление и переименование

Как узнать размер файла

Переставьте позицию в конец через fseek(f, 0, SEEK_END) и спросите её у ftell. Файл открываем в двоичном режиме: только для двоичного потока ftell возвращает число байт от начала файла, так сказано в описании ftell.

#include <stdio.h>

long file_size(const char *path) {
    FILE *f = fopen(path, "rb");
    if (f == NULL) {
        return -1;
    }
    if (fseek(f, 0, SEEK_END) != 0) {
        fclose(f);
        return -1;
    }
    long size = ftell(f);
    fclose(f);
    return size;
}

int main(void) {
    printf("notes.txt: %ld байт\n", file_size("notes.txt"));
    printf("points.bin: %ld байт\n", file_size("points.bin"));
    return 0;
}
notes.txt: 80 байт
points.bin: 24 байт

В notes.txt из первого примера, трёх строк без дозаписи, 46 символов, а размер 80 байт. Кириллица в UTF-8 занимает по два байта на букву, поэтому размер файла не равен длине текста.

Как скопировать файл

Готовой функции копирования в стандарте нет. Читаем исходный файл кусками по 4 КБ в цикле while и пишем каждый кусок в новый.

#include <stdio.h>

int copy_file(const char *from, const char *to) {
    FILE *src = fopen(from, "rb");
    if (src == NULL) {
        perror(from);
        return -1;
    }
    FILE *dst = fopen(to, "wb");
    if (dst == NULL) {
        perror(to);
        fclose(src);
        return -1;
    }
    char buf[4096];
    size_t n;
    while ((n = fread(buf, 1, sizeof buf, src)) > 0) {
        if (fwrite(buf, 1, n, dst) != n) {
            perror(to);
            fclose(src);
            fclose(dst);
            return -1;
        }
    }
    int err = ferror(src);
    fclose(src);
    if (fclose(dst) != 0 || err) {
        return -1;
    }
    return 0;
}

int main(void) {
    if (copy_file("notes.txt", "notes_copy.txt") == 0) {
        puts("Скопировано");
    }
    return 0;
}

Программа печатает Скопировано. Буфер в 4096 байт копирует и текст, и картинки, потому что оба файла открыты с b.

Как удалить и переименовать файл

Функции remove и rename тоже лежат в <stdio.h>. Обе возвращают 0 при успехе, см. описание удаления и описание переименования в справочнике.

#include <stdio.h>

int main(void) {
    if (rename("notes_copy.txt", "archive.txt") != 0) {
        perror("rename");
        return 1;
    }
    puts("Переименовано в archive.txt");

    if (remove("archive.txt") != 0) {
        perror("remove");
        return 1;
    }
    puts("archive.txt удалён");

    if (remove("archive.txt") != 0) {
        perror("remove второй раз");
    }
    return 0;
}
Переименовано в archive.txt
archive.txt удалён
remove второй раз: No such file or directory

Две оговорки из той же справки. Если файл с новым именем уже существует, поведение rename зависит от реализации. Удалить открытый файл Windows не даст, поэтому сначала fclose, потом remove.

Разбор CSV-файла

CSV это текст, где значения разделены запятыми. Подробнее о самом формате в статье что такое CSV-файл. Возьмём такой файл shop.csv:

name,qty,price
хлеб,2,55.50
молоко,1,89.90
сыр,1,420.00

Читаем строку через fgets и раскладываем её sscanf. Шаблон %63[^,] берёт всё до запятой, но не больше 63 символов, чтобы не переполнить name.

#include <stdio.h>

int main(void) {
    FILE *f = fopen("shop.csv", "r");
    if (f == NULL) {
        perror("shop.csv");
        return 1;
    }
    char line[256];
    fgets(line, sizeof line, f);  /* пропускаем заголовок */

    char name[64];
    int qty;
    double price, total = 0;
    while (fgets(line, sizeof line, f) != NULL) {
        if (sscanf(line, "%63[^,],%d,%lf", name, &qty, &price) == 3) {
            printf("%s: %d x %.2f\n", name, qty, price);
            total += qty * price;
        }
    }
    fclose(f);
    printf("Итого: %.2f\n", total);
    return 0;
}
хлеб: 2 x 55.50
молоко: 1 x 89.90
сыр: 1 x 420.00
Итого: 620.90

Такой разбор годится для простых таблиц без кавычек и запятых внутри полей. Для сложного CSV нужна библиотека или свой парсер с учётом кавычек. В Python для этого есть готовый модуль csv.

Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 700 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

Частые ошибки и что с ними делать

Рой нашёл ошибку в цикле чтения файла и радуется верному результату

Цикл while (!feof(f)) читает лишний раз

Классическая ошибка новичков. Возьмём файл из трёх букв abc и посчитаем символы так:

#include <stdio.h>

int main(void) {
    FILE *f = fopen("abc.txt", "w");
    fputs("abc", f);
    fclose(f);

    f = fopen("abc.txt", "r");
    int count = 0;
    while (!feof(f)) {
        fgetc(f);
        count++;
    }
    fclose(f);
    printf("Прочитано символов: %d\n", count);
    return 0;
}
Прочитано символов: 4

Символов три, а программа насчитала четыре. Как сказано в описании feof, функция сообщает о состоянии потока после последней операции и сам файл не проверяет. После третьего символа флаг конца ещё не поднят, цикл делает лишний круг, и четвёртый fgetc возвращает EOF.

Решение: проверяйте результат самого чтения.

#include <stdio.h>

int main(void) {
    FILE *f = fopen("abc.txt", "r");
    if (f == NULL) {
        perror("abc.txt");
        return 1;
    }
    int c;
    int count = 0;
    while ((c = fgetc(f)) != EOF) {
        count++;
    }
    fclose(f);
    printf("Прочитано символов: %d\n", count);
    return 0;
}
Прочитано символов: 3

Результат fgetc сохраняют в char

fgetc возвращает int: байт файла как unsigned char либо отрицательный EOF. В переменной char конец файла можно спутать с обычным байтом, и цикл while оборвётся раньше или не закончится вовсе. Объявляйте int c, как в примере выше.

Нет проверки на NULL

Если файла нет или не хватает прав, fopen вернёт NULL, и первый же fgets уронит программу. Проверка с perror сразу показывает причину:

Не открылся файл: No such file or directory

По описанию fopen в справочнике, заполнять errno при ошибке fopen требует POSIX. Linux и macOS следуют POSIX, библиотека Microsoft тоже задаёт errno, так что причину вы увидите везде.

Файл не закрыли

Запись идёт через буфер, и часть данных может остаться в памяти, если программа упадёт до fclose. Закрывайте файл в каждой ветке выхода, как в функции копирования выше, а перед долгой работой сбрасывайте буфер через fflush(f).

Visual Studio ругается на fopen: C4996

Компилятор Microsoft помечает fopen как небезопасную и предлагает fopen_s. С ключом /sdl это уже ошибка сборки. Есть два решения, оба описаны в справке к C4996:

  • перейти на fopen_s(&f, "notes.txt", "w"), если код пишется только под Windows;
  • оставить переносимый fopen и добавить #define _CRT_SECURE_NO_WARNINGS перед #include <stdio.h>.

Где освоить C системно

Файлы обычно изучают после операторов, циклов и указателей, а следом идут динамическая память и структуры данных. На C пишут прошивки микроконтроллеров и драйверы, на нём написано ядро Linux. Чем занимается такой специалист, рассказано в статье кто такой разработчик на Си, а план входа в профессию лежит в материале как стать программистом на Си.

Если хотите разобрать язык по порядку и с проверкой заданий, посмотрите онлайн-курсы по C. Вот подборка с ценами и длительностью:

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Инженер-программист
Перейти на сайт курса
GeekBrainsGeekBrains159 408 ₽4428 ₽/мес.12 месяцевОбзор курса
Специалист по информационной безопасности: расширенный курс
Перейти на сайт курса
НетологияНетология117 300 ₽4018 ₽/мес.13 месяцевОбзор курса
Внедрение и работа в DevSecOps
Перейти на сайт курса
OTUSOTUS88 000 ₽8800 ₽/мес.5 месяцевОбзор курса
Блокчейн
Перейти на сайт курса
GeekBrainsGeekBrains168 768 ₽4153 ₽/мес.4 месяцаОбзор курса
Инженер-программист C++
Перейти на сайт курса
GeekBrainsGeekBrains7970 ₽221 ₽/мес.1 месяцОбзор курса

Больше программ — в полном каталоге курсов по C (язык Си)

Часто задаваемые вопросы

Как создать файл в C?

Откройте его функцией fopen с режимом «w»: fopen("notes.txt", "w"). Если файла нет, он появится, если есть, его содержимое удалится. Проверьте, что fopen вернула не NULL, а после записи закройте файл через fclose.

Как закрыть файл в C?

Вызовите fclose(f) для указателя, который вернула fopen. Функция сбрасывает буфер на диск и освобождает поток. Если программа аварийно завершится до fclose, например упадёт, часть записанных данных может не попасть в файл.

Как узнать размер файла в C?

Откройте файл в режиме «rb», переставьте позицию в конец через fseek(f, 0, SEEK_END) и прочитайте её функцией ftell: для двоичного потока это число байт от начала файла. Учтите, что в UTF-8 русская буква занимает два байта.

Как удалить файл в C?

Функцией remove("имя_файла") из . Она возвращает 0 при успехе и ненулевое значение при ошибке, причину покажет perror. На Windows сначала закройте файл: открытый файл система удалить не даст.

Как очистить файл в C, не удаляя его?

Откройте файл в режиме «w» и сразу закройте: fopen сотрёт содержимое, а файл останется с размером 0 байт.

Чем текстовый режим отличается от двоичного?

В Linux и macOS ничем. В Windows текстовый режим при записи превращает перевод строки LF в CR-LF, а при чтении считает Ctrl+Z концом файла. Поэтому структуры, картинки и любые двоичные данные открывайте с флагом «b»: «rb», «wb».

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!