Функции perror(), fseek() и ftell()

Практический курс по 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:

#include <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;
}

После открытия файлового потока, мы устанавливаем его указатель на самую последнюю позицию путем вызова функции:

fseek(fp, 0, SEEK_END);

и, затем, читаем ее значение:

int length = ftell(fp);

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

fseek(fp, -i, SEEK_END);

и читая символ с выводом его на экран:

Как видите, все достаточно просто. После запуска программы увидим следующий результат ее работы:

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

Видео по теме