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

473

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

код

#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))?

Показать ещё 3 комментария
Теги:
file
while-loop
feof

5 ответов

384
Лучший ответ

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

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

  • 0
    Заголовок элемента списка '• C stdio, scanf ' неправильный (или, по крайней мере, неполный). scanf самом деле scanf - это C stdio, однако его контекст использования - C ++: C не позволяет объявлять переменные в выражении инициализации for() .
  • 26
    @ CiaPan: я не думаю, что это правда. И C99, и C11 позволяют это.
Показать ещё 19 комментариев
211

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

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

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

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

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

  • 14
    Mutlitple downvotes сегодня: есть объяснение? Если вы не согласны, пожалуйста, объясните свои причины.
  • 6
    Конечно, это неправильно, но, кроме того, это не «ужасно уродливо».
Показать ещё 6 комментариев
60

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

  • 1
    Интересно ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ } или (собираюсь проверить это) f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
  • 1
    @pmg: Как уже говорилось, "не обычное состояние цикла" хе-хе Я не могу придумать ни одного случая, в котором я нуждался, обычно меня интересует «могу ли я прочитать то, что я хотел» со всеми вытекающими последствиями обработки ошибок
Показать ещё 2 комментария
28

feof() указывает, попытался ли прочесть конец файла. Это означает, что он имеет мало прогнозирующего эффекта: если это правда, вы уверены, что следующая операция ввода завершится неудачно (вы не уверены, что предыдущая неудачная BTW), но если она ложна, вы не уверены, что следующий вход операция будет успешной. Более того, операции ввода могут завершиться по другим причинам, кроме конца файла (ошибка формата для форматированного ввода, чистый сбой ввода-вывода - сбой диска, сетевой тайм-аут - для всех типов ввода), поэтому, даже если вы можете быть прогностическим конец файла (и любой, кто попытался реализовать 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) */
}
  • 2
    Попадание в конец файла не является ошибкой, поэтому я подвергаю сомнению формулировку «операции ввода могут завершиться неудачей по другим причинам, чем конец файла».
  • 0
    @WilliamPursell, достижение eof не обязательно является ошибкой, но неспособность выполнить операцию ввода из-за eof - одна. И в С невозможно надежно обнаружить eof, не сделав операцию ввода неудачной.
Показать ещё 4 комментария
9

Отличный ответ, я просто заметил то же самое, потому что я пытался сделать такой цикл. Итак, это неправильно в этом сценарии, но если вы хотите, чтобы цикл, который изящно заканчивается на 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
}
  • 0
    Это интересный подход, но не работает на fifo. Кажется, он не дает никакой выгоды в while( fgetc(fp) != EOF )
  • 1
    Правда, но иногда вы не используете fgetc () для чтения файлов. Например, при чтении структурированных записей у меня есть функция чтения (в этом примере, где есть fgetc), которая обнаруживает ошибки и читает только одну запись, но не знает, сколько записей в файле. Да, это неправильно для fifos или любого другого файла, который может измениться, когда он у вас открыт.
Показать ещё 5 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню