Практический курс по C/C++: https://stepik.org/course/193691
На прошлом занятии мы с вами получили программу, которая побайтно читает данные из файла в
массив buff:
#include <stdio.h>
int main(void)
{
char buff[100];
FILE* in = fopen("my_file.txt", "r");
if(in == NULL) {
puts("File open error");
return 1;
}
char ch;
int i = 0;
while((ch = fgetc(in)) != EOF)
buff[i++] = ch;
buff[i] = '\0';
puts(buff);
fclose(in);
return 0;
}
В случае, если
функция fopen() не может
открыть поток для работы с файлом, выводится сообщение "File open error". Это
достаточно общая фраза, не сообщающая о конкретной ошибке. Как бы можно было бы
уточнить ее? Для этого в языке Си определен специальный макрос errno. Чтобы
использовать его в своей программе нужно подключить заголовочный файл errno.h:
Что это за
макрос? Он возвращает различные коды ошибок при их возникновении в результате
системных вызовов (любых системных вызовов), в том числе и тех, что связаны с
файлами. Значения всех этих кодов можно посмотреть в соответствующей
литературе, я приводить их не буду, в этом нет особого смысла, и вы сейчас
узнаете почему. Но для начала, давайте просто выведем код ошибки при попытке
открыть несуществующий файл:
FILE* in = fopen("my_file2.txt", "r");
if(in == NULL) {
printf("errno: %d\n", errno);
return 1;
}
После запуска
программы, увидим:
errno: 2
То есть, код
ошибки системного вызова, связанного с открытием не существующего файла, равен
2. Однако число выглядит совсем не информативно. Хотелось бы получить
сообщение, расшифровывающее этот код. Для этого можно воспользоваться
специальной функцией, которая называется:
void perror(const char* );
и определена в
заголовочном файле stdio.h. Давайте
воспользуемся ей в нашей программе:
FILE* in = fopen("my_file2.txt", "r");
if(in == NULL) {
printf("errno: %d\n", errno);
perror("my_file2.txt");
return 1;
}
Увидим сообщения:
my_file2.txt: No such file or directory
errno: 2
То есть, код
ошибки 2 означает, что файл не был найден. Теперь все предельно ясно. Мало
того, функция perror() выводит
сообщения в стандартный поток ошибок stderr, а не в поток stdout. По умолчанию,
оба потока связаны с монитором, поэтому особых отличий в выводе информации мы
не наблюдаем.
Файловые указатели позиций
Давайте теперь внимательнее
посмотрим на фрагмент нашей программы:
while((ch = fgetc(in)) != EOF)
buff[i++] = ch;
и зададимся
вопросом: откуда функция fgetc() знает, какой следующий символ из
файла читать? Мы же этого нигде не указываем, и при этом она прочитывает все
символы из файла от начала и до конца. Вся магия в том, что указатель in на файловый
поток содержит в себе специальный указатель позиции, который указывает на
очередной для считывания байт. И после его чтения, автоматически переходит к
следующему символу. Благодаря этому функция fgetc() последовательно
читает байт за байтом из файла.
Такой же
указатель имеется и для потока записи данных в файл. Очередная порция
записывается, начиная с места, на которое он указывает. Причем, эти два
указателя работают совершенно независимо друг от друга.
При желании мы
можем управлять указателем файла: получать значение его позиции и устанавливать
в любую доступную позицию. Для этого в языке Си имеются следующие две функции:
int fseek(FILE* stream, long offset, int whence);
long ftell(FILE* stream);
Первая функция fseek() позволяет
задавать файловую позицию, а вторая ftell() – получать
значение файловой позиции.
Параметр offset функции fseek() определяет
смещение файловой позиции относительно места, указанного в следующем параметре whence, который может
принимать следующие значения в виде предопределенных констант:
-
SEEK_SET – от начала
файла;
-
SEEK_CUR – от текущей
позиции;
-
SEEK_END – от конца
файла.
Причем, если мы
позиционируемся с начала файла, то offset может быть
только положительным. Для двух других флагов и положительным и отрицательным.
Давайте в
качестве примера прочитаем содержимое файла my_file.txt в обратном
порядке:
#include <stdio.h>
int main(void)
{
FILE* fp = fopen("my_file.txt", "r");
if(fp == NULL) {
perror("my_file2.txt");
return 1;
}
fseek(fp, 0, SEEK_END);
int length = ftell(fp);
printf("Length of the string: %d\n", length);
for (int i = 1; i <= length; ++i) {
fseek(fp, -i, SEEK_END);
putchar(fgetc(fp));
}
putchar('\n');
fclose(fp);
return 0;
}
После открытия
файлового потока, мы устанавливаем его указатель на самую последнюю позицию
путем вызова функции:
и, затем, читаем
ее значение:
В результате,
переменная length содержит число
байтов, которые требуется прочитать из файла. Само чтение происходит в цикле for, устанавливая
каждый раз нужную позицию файла:
и читая символ с
выводом его на экран:
Как видите, все
достаточно просто. После запуска программы увидим следующий результат ее
работы:
Length
of the string: 27
.noitca ni
)(ctupf noitcnuF
Вот пример того,
как можно осуществлять произвольный доступ к файлу и считывать из него
информацию в произвольном порядке.
В заключение
этого занятия приведу еще один пример использования функции fseek(). Предположим,
мы посимвольно записываем информацию в файл следующим образом:
#include <stdio.h>
int main(void)
{
char str[] = "Function fputc() in action.";
FILE* fp = fopen("my_file.txt", "w");
if(fp == NULL) {
perror("my_file2.txt");
return 1;
}
for(int i = 0; str[i]; ++i) {
fseek(fp, 0, SEEK_SET);
fputc(str[i], fp);
}
fclose(fp);
return 0;
}
На каждой
итерации цикла позиция файла устанавливается в его начало. В результате, файл
будет содержать только один последний символ, так как запись строки
производилась в одну и ту же позицию.
Практический курс по C/C++: https://stepik.org/course/193691