Файловые функции: fopen(), fclose(), fgetc(), fputc()

Практический курс по C/C++: https://stepik.org/course/193691

Смотреть материал на YouTube | RuTube

С этого занятия мы начинаем большую тему по работе с файлами. Думаю, не надо объяснять, что такое файлы, где они располагаются и для чего нужны. Было бы странно видеть человека, который смотрит курс по языку Си и не знает этих базовых вещей. Также, я полагаю, что вы все умеете прописывать абсолютные и относительные пути к файлам. Вот здесь, наверное, надо сказать пару слов. Предположим, у нас с вами имеется следующая структура каталогов и файлов:

с рабочим каталогом "d:/app/". Тогда относительные и абсолютные пути можно к разным файлам можно прописывать следующим образом:

Относительные пути

Абсолютные пути

"my_file.txt"

"d:\\app\\my_file.txt"

или

"d:/app/my_file.txt"

"images/img.png"

"d:/app/images/img.png"

"../out.txt"

"d:/out.txt"

"../parent/prt.dat"

"d:/parent/prt.dat"

Варианты

"d:\\app\\my_file.txt" и "d:/app/my_file.txt"

представляют собой абсолютный путь к файлу, то есть, полный путь, начиная с указания диска. Причем, обычно используют слеш в качестве разделителя: так короче писать и такой путь будет корректно восприниматься как под ОС Windows, так и Linux.

Для доступа к файлу out.txt пути могут быть записаны как:

"../out.txt"
"d:/out.txt"

Обратите внимание, здесь две точки означают переход к родительскому каталогу, то есть, выход из каталога app на один уровень вверх. И, наконец, для доступа к файлу prt.dat пути пишутся так:

"../parent/prt.dat"
"d:/parent/prt.dat"

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

Функции fopen() и fclose()

Философия работы с файлами заключается в открытии потока, связанного с тем или иным файлом. Затем, через открытый поток выполняется чтение или запись информации в файл. Что это за поток, какую роль он играет и как его открыть? Начну с последнего. Для открытия потока, связанного с файлом, используется функция:

FILE* fopen(const char* path, const char* mode);

Первый параметр path – это путь к файлу; второй параметр mode – режим доступа к файлу. Как прописывать путь, мы с вами уже знаем, а режимы доступа могут быть следующими:

Режим

Описание

"r"

Открытие текстового файла только на чтение.

"w"

Открытие текстового файла только на запись. Если файл не существует, то он создается. Если существует, то все его прежнее содержимое удаляется.

"a"

Открытие текстового файла на дозапись (новые данные добавляются в конец файла). Если файл не существует, то он создается.

"r+"

Открытие текстового файла для чтения и записи одновременно.

"w+"

Открытие текстового файла для чтения и записи. Если файл не существует, то он создается. Если существовал, то все его прежнее содержимое удаляется.

"a+"

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

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

"rb", "wb", "ab", "rb+", "wb+", "ab+"

То есть, здесь просто добавляется буква "b". В ОС Unix текстовый режим и бинарный работают одинаково. Но в ОС Windows по-разному. Что это за режим и как с ним работать мы будем говорить позже.

Функция fopen() возвращает указатель типа FILE на открытый поток, либо значение NULL, если файл по каким-либо причинам открыть не удалось. Надо сказать, ошибки открытия файла встречаются очень часто. Например, мы пытаемся открыть на чтение не существующий файл. Тогда функция fopen() вернет значение NULL. Поэтому в программе нужно обязательно делать проверку на корректность открытия файлового потока и только после этого с ним работать.

Теперь, что из себя представляет файловый поток. Когда отрабатывает функция fopen() она делает системный запрос к ОС на открытие указанного файла. ОС выполняет необходимые действия и в случае успеха возвращает дескриптор файлового потока. Дополнительно функция fopen() создает входной буфер, при чтении данных из файла, или выходной, при записи данных в файл, или на чтение и записи одновременно:

Зачем нужен этот буфер? Дело в том, что данные из файла читаются не по байту, а сразу порцией байт и помещаются во входной буфер. И то же самое с записью. Данные сначала попадают в выходной буфер и при достижении определенного размера, переносятся в файл. Но все же, почему бы данные сразу не читать и записывать в файлы? Чтобы это делать наша программа каждый раз должна обращаться к ОС, то есть выполнять системные вызовы, а это не быстрый процесс. Да и, к тому же, дополнительная нагрузка на ОС. Поэтому, для ускорения работы с файлами, как правило, формируются входные и выходные буферы и данные проходят через них.

В целом вся работа с получением дескриптора на файловый поток и работа с буферов скрыта от программиста. Все, что от нас требуется, это вызвать сначала функцию fopen(), чтобы получить указатель на файловый поток, а затем, закрыть его с помощью функции:

int fclose(FILE* fp);

Вызывать функцию fclose() строго обязательно после успешного открытия файла. Она, во-первых, помещает все данные в файл из выходного буфера, если они там есть и, во-вторых, освобождает все ресурсы, связанные с открытым файловым потоком, в том числе, память, занимаемую буфером, и формирует системный вызов для освобождения файлового дескриптора.

Данная функция возвращает 0, если закрытие файла прошло успешно и -1, если возникли какие-либо ошибки. Правда, обработать эти ошибки мы все равно вряд ли сможем, т.к. они обычно связаны с системными вызовами, а это уже зона действия ОС.

Давайте для примера, посмотрим, как в программе можно открыть файл на запись и закрыть его:

#include <stdio.h>
 
int main(void)
{
    FILE* fp = fopen("my_file.txt", "w");
    if(fp == NULL)
        return 1;
 
    fclose(fp);
 
    return 0;
}

Если все прошло успешно, то в каталоге рядом с файлом lessons.exe появится еще один пустой файл с именем my_file.txt.

Функции fgetc() и fputc()

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

int fgetc(FILE* stream);
int fputc(int ch, FILE* stream);

Первая функция fgetc() позволяет побайтового читать данные из указанного потока stream, а вторая fputc() побайтно записывает данные в поток stream. Причем, в качестве потоков могут быть как файловые, так и стандартные: stdin, stdout. Вообще эти функции работают аналогично функциям getchar() и putchar(), о которых мы с вами уже говорили.

Давайте с помощью функции fputc() запишем в файл небольшую строку:

#include <stdio.h>
 
int main(void)
{
    char str[] = "Function fputc() in action.";
 
    FILE* fp = fopen("my_file.txt", "w");
    if(fp == NULL)
        return 1;
 
    for(int i = 0; str[i]; ++i)
        fputc(str[i], fp);
 
    fclose(fp);
 
    return 0;
}

Если после выполнения программы открыть файл my_file.txt, то увидим в нем искомую строку.

Очень частая ошибка, когда начинающие программисты забывают закрыть файл, в который производилась запись данных, и обнаруживают, что он либо пустой, либо содержит не полные данные. Все дело в том, что данные не сразу попадают физически в файл, а сначала сохраняются в выходном буфере и только при достижении определенного объема все данные разом заносятся в файл. Поэтому, если в буфере имеется какая-либо несохраненная информация, то при закрытии файла, она автоматически переносится в файл и данные не пропадают. Если же файл не закрыть, то возможна потеря части информации, которая так осталась в буфере. В моем случае компилятор грамотно переводит программу и при ее завершении все данные из буфера сохраняются в файле. Но срабатывает это не всегда.  Давайте я это продемонстрирую на примере, когда данные записываются в файл, а потом тут же, без его закрытия, читаются:

#include <stdio.h>
 
int main(void)
{
    char str[] = "Function fputc() in action.";
    char buff[100];
 
    FILE* fp = fopen("my_file.txt", "w");
    if(fp == NULL)
        return 1;
 
    for(int i = 0; str[i]; ++i)
        fputc(str[i], fp);
 
    FILE* in = fopen("my_file.txt", "r");
    if(in == NULL) {
        puts("File open error");
        return 2;
    }
 
    char ch;
    int i = 0;
    while((ch = fgetc(in)) != EOF)
        buff[i++] = ch;
    buff[i] = '\0';
    puts(buff);
 
    fclose(fp);
    return 0;
}

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

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме