Почему "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;
    }
}

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

7 ответов

Решение

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

Параллельность и одновременность

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

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

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

EOF

Теперь мы добрались до 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) очистить буфер:

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

  • Последний пример, который фактически запрашивает состояние 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! */
        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))"(хотя должна быть еще одна проверка внутри цикла с разрывом, чтобы избежать бесконечного цикла при ошибке чтения), это тот случай, когда он почти наверняка всегда неверен. И даже если когда-либо возникнет случай, когда он будет правильным, это настолько идиоматически неправильно, что это был бы неправильный способ написания кода. Любой, кто видит этот код, должен немедленно задуматься и сказать: "Это ошибка". И, возможно, дать пощечину автору (если автор не ваш начальник, в этом случае усмотрение рекомендуется.)

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

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

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

Остальные ответы на этот вопрос очень хорошие, но довольно длинные. Если вам просто нужен TL;DR, вот это:

feof(F)назван неудачно. Это не означает «проверьте, достиг ли сейчас конец файла »; скорее, он говорит вам, почему предыдущая попытка не смогла получить какие-либо данные отF.

Состояние конца файла может легко измениться, поскольку файл может увеличиваться или уменьшаться, а терминал сообщает об этом.EOFодин раз при каждом нажатии^D(в «приготовленном» режиме, в противном случае в пустой строке).

Если вас действительно не волнует , почему предыдущее чтение не вернуло никаких данных, вам лучше забыть, чтоfeofфункция существует.

feof()не очень интуитивно понятен. По моему очень скромному мнению,FILEсостояние конца файла должно быть установлено на trueесли какая-либо операция чтения приводит к достижению конца файла. Вместо этого вы должны вручную проверять, был ли достигнут конец файла после каждой операции чтения. Например, что-то вроде этого будет работать, если читать из текстового файла с помощьюfgetc():

#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *in = fopen("testfile.txt", "r");

  while(1) {
    char c = fgetc(in);
    if (feof(in)) break;
    printf("%c", c);
  }

  fclose(in);
  return 0;
}

Было бы здорово, если бы вместо этого работало что-то вроде этого:

#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *in = fopen("testfile.txt", "r");

  while(!feof(in)) {
    printf("%c", fgetc(in));
  }

  fclose(in);
  return 0;
}
Другие вопросы по тегам