Почему "пока (!feof (файл))" всегда неправ?

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

код

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

что не так с этим while( !feof(fp)) петли?

5 ответов


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

параллелизма и одновременности

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

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

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

EOF

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

примеры

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

  • 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, количество преобразованных элементов.

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

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

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

  • C++, iostreams getline:

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

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

  • POSIX,write(2) to промывочный буфер:

    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: Предположим, у вас есть строка и вы хотите проверить, что она представляет целое число целиком, без дополнительных битов в конце, кроме пробелов. Используя C++ 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
    }
    

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

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

ответ

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


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

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

/* 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! */
        (void) 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().

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

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

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


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) */
}

отличный ответ, я просто заметил то же самое, потому что я пытался сделать такой цикл. Итак, это неправильно в этом сценарии, но если вы хотите иметь цикл, который изящно заканчивается на 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
}