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


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

код

#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 470

5 ответов:

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

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

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

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

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

EOF

теперь мы добираемся до EOF. EOF - это ответ вам попытка операции ввода-вывода. Это означает, что вы пытались что-то прочитать или написать, но при этом вам не удалось прочитать или записать какие-либо данные, и вместо этого был обнаружен конец ввода или вывода. Это верно по существу для всех API ввода-вывода, будь то C стандартная библиотека, 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, количество преобразованных элементов.

  • 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". Мы обычно обнаруживаем состояние ошибки каким-то другим способом, который нам более интересен (например, неспособность выполнить столько операций ввода-вывода, сколько мы хотели). В каждом примере есть некоторая функция API, которая может явно сказать нам, что состояние EOF было обнаружено, но это на самом деле не a очень полезная информация. Это гораздо больше деталей, чем мы часто заботимся. Важно ли ввода/вывода удалось, более-так как это не удалось.

  • последний пример, который фактически запрашивает состояние 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
    }
    

    здесь мы используем два результата. Первый это 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.

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

еще хуже, рассмотрим случай, когда происходит ошибка чтения. В таком случае, fgetc() возвращает 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
}