Почему "while (! Feof (file))" всегда неверно?

В последнее время я видел людей, пытающихся читать подобные файлы во многих сообщениях.

Код

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    char *path = argc > 1 ? argv[1] : "input.txt";

    FILE *fp = fopen(path, "r");
    if( fp == NULL ) {
        perror(path);
        return EXIT_FAILURE;
    }

    while( !feof(fp) ) {  /* THIS IS WRONG */
        /* Read and process data from file… */
    }
    if( fclose(fp) == 0 ) {
        return EXIT_SUCCESS;
    } else {
        perror(path);
        return EXIT_FAILURE;
    }
}

Что не так с этим циклом?

Ответ 1

Я хотел бы предоставить абстрактную, высокоуровневую перспективу.

Concurrency и одновременность

Операции ввода/вывода взаимодействуют с окружающей средой. Окружающая среда не является частью вашей программы, а не под вашим контролем. Окружающая среда действительно существует "одновременно" с вашей программой. Как и во всех параллельных вещах, вопросы о "текущем состоянии" не имеют смысла: нет понятия "одновременность" в параллельных событиях. Многие свойства состояния просто не существуют одновременно.

Позвольте мне сделать это более точным: предположим, вы хотите спросить: "У вас больше данных". Вы можете задать это из параллельного контейнера или вашей системы ввода-вывода. Но ответ, как правило, невозможен и, следовательно, бессмыслен. Так что, если контейнер говорит "да" – к моменту, когда вы попытаетесь прочитать, у него больше нет данных. Аналогичным образом, если ответ "нет", к моменту попытки чтения данные могут быть получены. Вывод заключается в том, что просто нет такого свойства, как "у меня есть данные", поскольку вы не можете действовать значимо в ответ на любой возможный ответ. (Ситуация немного лучше с буферизованным входом, где вы, возможно, можете получить "да, у меня есть данные", который представляет собой какую-то гарантию, но вам все равно придется иметь дело с противоположным случаем. конечно же так же плохо, как я описал: вы никогда не знаете, заполнен ли этот диск или этот сетевой буфер.)

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

EOF

Теперь мы попадаем в EOF. EOF - это ответ, который вы получаете от попытки ввода-вывода. Это означает, что вы пытались что-то прочитать или написать, но при этом вам не удалось прочитать или написать какие-либо данные, а вместо этого столкнулся конец ввода или вывода. Это справедливо для практически всех API ввода-вывода, будь то стандартная C-библиотека, С++ iostreams или другие библиотеки. Пока операции ввода-вывода преуспевают, вы просто не можете знать, будут ли дальнейшие дальнейшие операции успешными. Вы всегда должны сначала попробовать операцию, а затем ответить на успех или неудачу.

Примеры

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

  • C stdio, чтение из файла:

    for (;;) {
        size_t n = fread(buf, 1, bufsize, infile);
        consume(buf, n);
        if (n < bufsize) { break; }
    }
    

    В результате мы должны использовать n, число прочитанных элементов (которое может быть равно нулю).

  • C stdio, scanf:

    for (int a, b, c; scanf("%d %d %d", &a, &b, &c) == 3; ) {
        consume(a, b, c);
    }
    

    В результате мы должны использовать возвращаемое значение scanf, число преобразованных элементов.

  • С++, форматированное извлечение iostreams:

    for (int n; std::cin >> n; ) {
        consume(n);
    }
    

    В результате мы должны использовать сам std::cin, который может быть оценен в булевом контексте и сообщает нам, находится ли поток в состоянии good().

  • С++, iostreams getline:

    for (std::string line; std::getline(std::cin, line); ) {
        consume(line);
    }
    

    Результат, который мы должны использовать, снова std::cin, как и раньше.

  • POSIX, write(2), чтобы очистить буфер:

    char const * p = buf;
    ssize_t n = bufsize;
    for (ssize_t k = bufsize; (k = write(fd, p, n)) > 0; p += k, n -= k) {}
    if (n != 0) { /* error, failed to write complete buffer */ }
    

    В результате мы используем k, количество записанных байтов. Дело здесь в том, что мы можем знать только, сколько байтов было записано после операции записи.

  • POSIX getline()

    char *buffer = NULL;
    size_t bufsiz = 0;
    ssize_t nbytes;
    while ((nbytes = getline(&buffer, &bufsiz, fp)) != -1)
    {
        /* Use nbytes of data in buffer */
    }
    free(buffer);
    

    В результате мы должны использовать nbytes, количество байтов до и включая новую строку (или EOF, если файл не заканчивается новой строкой).

    Обратите внимание, что функция явно возвращает -1 (а не EOF!) при возникновении ошибки или достигает EOF.

Вы можете заметить, что мы очень редко излагаем фактическое слово "EOF". Обычно мы обнаруживаем условие ошибки каким-либо другим способом, что более интересно для нас (например, отказ выполнить столько операций ввода-вывода, сколько нам было необходимо). В каждом примере есть некоторая функция API, которая может прямо сказать нам, что состояние EOF встречается, но на самом деле это не очень полезная информация. Это гораздо более подробно, чем мы часто заботимся. Важно то, что I/O преуспел, более того, чем это не удалось.

  • Последний пример, который фактически запрашивает состояние EOF: предположим, что у вас есть строка и вы хотите проверить, что она представляет целое целое, без лишних бит в конце, кроме пробелов. Используя iostreams на С++, он выглядит следующим образом:

    std::string input = "   123   ";   // example
    
    std::istringstream iss(input);
    int value;
    if (iss >> value >> std::ws && iss.get() == EOF) {
        consume(value);
    } else {
        // error, "input" is not parsable as an integer
    }
    

    Здесь мы используем два результата. Первым является iss, сам объект потока, чтобы убедиться, что отформатированное извлечение до value выполнено успешно. Но затем, после использования пробелов, мы выполняем другую операцию ввода/вывода/iss.get() и ожидаем, что она завершится с ошибкой как EOF, что имеет место, если вся строка уже была израсходована форматированным извлечением.

    В стандартной библиотеке C вы можете добиться чего-то подобного с функциями strto*l, проверив, что конечный указатель достиг конца строки ввода.

Ответ

while(!eof) неверен, потому что он проверяет что-то, что не имеет значения, и не может проверить что-то, что вам нужно знать. В результате вы ошибочно выполняете код, который предполагает, что он обращается к данным, которые были прочитаны успешно, а на самом деле этого не произошло.

Ответ 2

Это неправильно, потому что (при отсутствии ошибки чтения) он входит в цикл еще раз, чем ожидает автор. Если есть ошибка чтения, цикл никогда не прекращается.

Рассмотрим следующий код:

/* WARNING: demonstration of bad coding technique!! */

#include <stdio.h>
#include <stdlib.h>

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

int main(int argc, char **argv)
{
    FILE *in;
    unsigned count;

    in = argc > 1 ? Fopen(argv[1], "r") : stdin;
    count = 0;

    /* WARNING: this is a bug */
    while (!feof(in)) {  /* This is WRONG! */
        fgetc(in);
        count++;
    }
    printf("Number of characters read: %u\n", count);
    return EXIT_SUCCESS;
}

FILE * Fopen(const char *path, const char *mode)
{
    FILE *f = fopen(path, mode);
    if (f == NULL) {
        perror(path);
        exit(EXIT_FAILURE);
    }
    return f;
}

Эта программа будет последовательно печатать на единицу больше, чем количество символов во входном потоке (при условии отсутствия ошибок чтения). Рассмотрим случай, когда входной поток пуст:

$ ./a.out < /dev/null
Number of characters read: 1

В этом случае feof() вызывается до того, как какие-либо данные были прочитаны, поэтому возвращает false. Цикл вводится, вызывается fgetc() (и возвращается EOF), и счет увеличивается. Затем вызывается feof() и возвращается значение true, что приводит к прерыванию цикла.

Это происходит во всех таких случаях. feof() не возвращает true до тех пор, пока после чтения потока не встретит конец файла. Цель feof() - НЕ проверять, достигнет ли следующее чтение конца файла. Целью feof() является различие между ошибкой чтения и достижением конца файла. Если fread() возвращает 0, вы должны использовать feof/ferror, чтобы решить, была ли обнаружена ошибка или все данные были использованы. Аналогично, если fgetc возвращает EOF. feof() используется только после того, как fread вернул ноль или fgetc вернул EOF. До этого feof() всегда возвращает 0.

Всегда необходимо проверять возвращаемое значение чтения (либо fread(), либо fscanf(), либо fgetc()) перед вызовом feof().

Еще хуже, рассмотрим случай, когда происходит ошибка чтения. В этом случае fgetc() возвращает EOF, feof() возвращает false, и цикл никогда не завершается. Во всех случаях, когда используется while(!feof(p)), должна быть, по крайней мере, проверка внутри цикла для ferror(), или, по крайней мере, условие while должно быть заменено на while(!feof(p) && !ferror(p)), или существует очень реальная возможность бесконечного цикл, вероятно, извергает все виды мусора, поскольку обрабатываются недействительные данные.

Итак, в заключение, хотя я не могу с уверенностью утверждать, что никогда не бывает ситуации, в которой может быть семантически правильным написать "while(!feof(f))" (хотя должна быть еще одна проверка внутри цикла с разрывом чтобы избежать бесконечного цикла при ошибке чтения), это почти всегда неверно. И даже если когда-либо возникнет случай, когда он будет правильным, это настолько идиоматически неправильно, что это не будет правильным способом написания кода. Любой, кто увидит этот код, должен сразу же подумать и сказать: "Это ошибка". И, возможно, ударить автора (если автор не ваш начальник, в этом случае рекомендуется усмотрение.)

Ответ 3

Нет, это не всегда неправильно. Если ваше условие цикла "пока мы не пытались прочитать прошлый конец файла", вы используете while (!feof(f)). Это, однако, не общее условие цикла - обычно вы хотите проверить что-то еще (например, "могу ли я прочитать больше" ). while (!feof(f)) не ошибается, он просто ошибочный.

Ответ 4

feof() указывает, пытался ли кто-нибудь прочитать после конца файла. Это означает, что он имеет небольшой прогнозирующий эффект: если он истинен, вы уверены, что следующая операция ввода потерпит неудачу (вы не уверены, что предыдущая провалилась, кстати,), но если она ложна, вы не уверены, что следующий ввод операция будет успешной. Более того, операции ввода могут завершаться неудачей по другим причинам, кроме конца файла (ошибка форматирования для форматированного ввода, ошибка чистого ввода-вывода - сбой диска, тайм-аут сети - для всех типов ввода), поэтому даже если вы можете предвидеть конец файла (и любой, кто пытался внедрить Ada one, который является прогностическим, скажет вам, что он может быть сложным, если вам нужно пропустить пробелы, и что это оказывает нежелательное влияние на интерактивные устройства - иногда вынуждая вводить следующее перед началом обработки предыдущего), вы должны быть в состоянии обработать сбой.

Таким образом, правильная идиома в C состоит в том, чтобы выполнить цикл с успешным выполнением операции ввода-вывода в качестве условия цикла, а затем проверить причину сбоя. Например:

while (fgets(line, sizeof(line), file)) {
    /* note that fgets don't strip the terminating \n, checking its
       presence allow to handle lines longer that sizeof(line), not showed here */
    ...
}
if (ferror(file)) {
   /* IO failure */
} else if (feof(file)) {
   /* format error (not possible with fgets, but would be with fscanf) or end of file */
} else {
   /* format error (not possible with fgets, but would be with fscanf) */
}

Ответ 5

Отличный ответ, я просто заметил то же самое, потому что я пытался сделать такой цикл. Итак, это неправильно в этом сценарии, но если вы хотите, чтобы цикл, который изящно заканчивается на EOF, это хороший способ сделать это:

#include <stdio.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
  struct stat buf;
  FILE *fp = fopen(argv[0], "r");
  stat(filename, &buf);
  while (ftello(fp) != buf.st_size) {
    (void)fgetc(fp);
  }
  // all done, read all the bytes
}