Практический курс по C/C++: https://stepik.org/course/193691
С этого занятия мы начинаем большую тему по работе с файлами. Думаю, не надо объяснять, что такое файлы, где они
располагаются и для чего нужны. Было бы странно видеть человека, который
смотрит курс по языку Си и не знает этих базовых вещей. Также, я полагаю, что
вы все умеете прописывать абсолютные и относительные пути к файлам. Вот здесь,
наверное, надо сказать пару слов. Предположим, у нас с вами имеется следующая
структура каталогов и файлов:
с рабочим
каталогом "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