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


574

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

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

int
main(int argc, char **argv)
{
    char *path = "stdin";
    FILE *fp = argc > 1 ? fopen(path=argv[1], "r") : stdin;

    if( fp == NULL ) {
        perror(path);
        return EXIT_FAILURE;
    }

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

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



Ответы:


454

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

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

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

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

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

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


34
@ CiaPan: я не думаю, что это правда. И C99, и C11 позволяют это.
Керрек С.Б.

11
Но ANSI C этого не делает.
CiaPan

3
@JonathanMee: Это плохо по всем причинам, которые я упомянул: вы не можете смотреть в будущее. Вы не можете сказать, что произойдет в будущем.
Kerrek SB

3
@JonathanMee: Да, это было бы уместно, хотя обычно вы можете объединить эту проверку в операции (так как большинство операций iostreams возвращают объект потока, который сам имеет логическое преобразование), и таким образом вы делаете очевидным, что вы не игнорируя возвращаемое значение.
Kerrek SB

4
Третий параграф вводит в заблуждение / неточен для принятого и высоко оцененного ответа. feof()не "спрашивает систему ввода / вывода, есть ли у нее больше данных". feof()в соответствии с man-страницей (Linux) : «проверяет индикатор конца файла для потока, на который указывает поток, и возвращает ненулевое значение, если оно установлено». (также, clearerr()единственный способ сбросить этот индикатор - явный вызов ); В этом отношении ответ Уильяма Перселла намного лучше.
Арне Фогель

234

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

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

/* 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()вызывается и возвращает истину, вызывая прерывание цикла.

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

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

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

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


7
Конечно, это неправильно, но, кроме того, это не «ужасно уродливо».
Нобар

89
Вы должны добавить пример правильного кода, так как я думаю, что многие люди придут сюда в поисках быстрого решения.
Джели

6
@Thomas: я не эксперт по C ++, но я считаю, что file.eof () эффективно возвращает тот же результат feof(file) || ferror(file), что и он, поэтому он сильно отличается. Но этот вопрос не предназначен для применения к C ++.
Уильям Перселл

6
@ m-ric, это тоже неправильно, потому что вы все равно попытаетесь обработать чтение, которое не удалось.
Марк Рэнсом

4
это фактический правильный ответ. feof () используется, чтобы узнать результат предыдущей попытки чтения. Таким образом, вероятно, вы не хотите использовать его в качестве условия прерывания цикла. +1
Джек

63

Нет, это не всегда неправильно. Если ваше условие цикла "пока мы не пытались прочитать конец файла", тогда вы используете 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 */ }
pmg

1
@pmg: Как уже говорилось, "не обычное состояние цикла" хе-хе. Я не могу придумать ни одного случая, в котором я нуждался, обычно меня интересует, «могу ли я прочитать то, что я хотел» со всеми вытекающими
Эрик

@pmg: Как уже говорилось, вы редко хотитеwhile(!eof(f))
Эрик

9
Точнее говоря, условие «пока мы не пытались прочитать после конца файла и не было ошибки чтения» feofне относится к обнаружению конца файла; речь идет об определении того, было ли чтение коротким из-за ошибки или из-за исчерпания ввода.
Уильям Перселл

35

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

2
Попадание в конец файла не является ошибкой, поэтому я подвергаю сомнению формулировку «операции ввода могут завершиться неудачей по другим причинам, чем конец файла».
Уильям Перселл

@WilliamPursell, достижение eof не обязательно является ошибкой, но невозможность выполнить операцию ввода из-за eof - одна. И в С невозможно надежно обнаружить eof, не сделав операцию ввода неудачной.
AProgrammer

Согласитесь , последний elseне представляется возможным с sizeof(line) >= 2и , fgets(line, sizeof(line), file)но возможно с патологическим size <= 0и fgets(line, size, file). Может быть, даже возможно с sizeof(line) == 1.
chux - Восстановить Монику

1
Все эти разговоры о «предсказательной ценности» ... Я никогда не думал об этом таким образом. В моем мире feof(f)ничего не ПРОГНОЗИТЬ. В нем говорится, что ПРЕДЫДУЩАЯ операция достигла конца файла. Ни больше ни меньше. И если предыдущей операции не было (только что открыли), она не сообщает о конце файла, даже если файл был пуст для начала. Таким образом, кроме объяснения параллелизма в другом ответе выше, я не думаю, что есть какая-либо причина, чтобы не зацикливаться feof(f).
BitTickler

@AProgrammer: А «дочитать до N байт» запрос , который дает ноль, из - за «постоянного» EOF , или потому , что больше нет данных нет пока , это не ошибка. Хотя функция feof () не может надежно предсказать, что будущие запросы будут давать данные, она может достоверно указывать, что будущие запросы не будут . Возможно, должна существовать функция состояния, которая бы указывала «Возможно, что будущие запросы на чтение будут успешными», с семантикой, согласно которой после чтения в конец обычного файла качественная реализация должна говорить, что будущие чтения вряд ли будут успешными, без какой-либо причины для верю, что могут .
Суперкат

0

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

1
printf("%c", fgetc(in));? Это неопределенное поведение. fgetc()возвращается int, нет char.
Эндрю Хенле

Мне кажется, что стандартная идиома while( (c = getchar()) != EOF)очень похожа на это.
Уильям Перселл

while( (c = getchar()) != EOF)работает на одном из моих компьютеров под управлением GNU C 10.1.0, но не работает на Raspberry Pi 4 под управлением GNU C 9.3.0. На моем RPi4 он не определяет конец файла и просто продолжает работу.
Скотт Диган

@AndrewHenle Ты прав! Меняется char cна int cработу! Спасибо!!
Скотт Диган
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.