Чому "while (! Feof (file))" завжди помиляється?


573

Останнім часом я бачив людей, які намагаються читати такі файли у багатьох публікаціях:

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

Що не так з цією петлею?



Відповіді:


453

Я хотів би надати абстрактну перспективу високого рівня.

Паралельність і одночасність

Операції вводу / виводу взаємодіють із середовищем. Навколишнє середовище не є частиною вашої програми і не під вашим контролем. Навколишнє середовище справді існує "одночасно" з вашою програмою. Як і у всіх ситуаціях, що паралельно ставляться, питання щодо "поточного стану" не мають сенсу: Не існує поняття "одночасність" у різних одночасних подіях. Багато властивостей держави просто не існують одночасно.

Дозвольте зробити це більш точним: припустимо, ви хочете запитати, "чи є у вас більше даних". Ви можете запитати про це одночасно з контейнером або з вашої системи вводу / виводу. Але відповідь, як правило, неприйнятна, а значить, безглузда. Що робити, якщо контейнер каже "так" - до моменту, коли ви спробуєте прочитати, він може більше не мати даних. Аналогічно, якщо відповідь "ні", до моменту спроби читання дані, можливо, надійшли. Висновок такий просто єнемає властивості на зразок "У мене є дані", оскільки ви не можете діяти змістовно у відповідь на будь-яку можливу відповідь. (Ситуація дещо краща з буферним вкладом, де ви, можливо, отримаєте "так, у мене є дані", які є певною гарантією, але ви все одно повинні мати можливість вирішувати протилежний випадок. це, звичайно, так само погано, як я описав: ви ніколи не знаєте, чи цей диск чи мережевий буфер повно.)

Таким чином , ми приходимо до висновку , що це неможливо, і справді ип розумного , щоб задати систему введення / виведення він буде в змозі виконати операцію вводу / виводу. Єдиний можливий спосіб, з яким ми можемо взаємодіяти з ним (так само, як і з одночасним контейнером), - це зробити спробу операцію і перевірити, чи вдалося це чи не вдалося. У той момент, коли ви взаємодієте з оточенням, тоді і лише тоді ви зможете дізнатися, чи була взаємодія реально можливою, і в цей момент ви повинні взяти на себе зобов'язання виконувати взаємодію. (Це "точка синхронізації", якщо ви хочете.)

EOF

Тепер ми переходимо до EOF. EOF - це відповідь, яку ви отримуєте від спроби операції вводу / виводу. Це означає, що ви намагалися щось прочитати чи написати, але при цьому вам не вдалося прочитати чи записати будь-які дані, і натомість виник кінець вводу чи виводу. Це стосується практично всіх API API вводу / виводу, будь то стандартна бібліотека C, іострими 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:

    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 ++, це виглядає так:

    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, що є тим випадком, коли вся струна вже була використана форматованим вилученням.

    У стандартній бібліотеці С ви можете досягти чогось подібного з strto*lфункціями, перевіривши, чи кінцевий покажчик досяг кінця рядка введення.

Відповідь

while(!feof)помиляється, оскільки перевіряє щось, що не має значення, і не може перевірити те, що вам потрібно знати. У результаті ви помилково виконуєте код, який передбачає, що він отримує доступ до даних, які були успішно прочитані, адже насправді цього ніколи не було.


34
@CiaPan: Я не думаю, що це правда. І C99, і C11 дозволяють це зробити.
Керрек СБ

11
Але ANSI C цього не робить.
CiaPan

3
@JonathanMee: Це погано з усіх причин, про які я згадую: ви не можете дивитись у майбутнє. Ви не можете сказати, що буде в майбутньому.
Керрек СБ

3
@JonathanMee: Так, це було б доречно, хоча зазвичай ви можете об'єднати цю перевірку в операцію (оскільки більшість операцій iostreams повертає об'єкт потоку, який сам має булеве перетворення), і таким чином ви даєте зрозуміти, що ви не ігнорування повернутого значення.
Керрек СБ

4
Третій параграф є надзвичайно оманливим / неточним для прийнятої та висококваліфікованої відповіді. feof()не "запитує систему вводу / виводу, чи має вона більше даних". feof(), В відповідно до (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()викликається до того, як будь-які дані будуть прочитані, тому він повертає помилкові. Цикл вводиться, fgetc()називається (і повертається EOF), а кількість збільшується. Потім feof()викликається і повертає істину, в результаті чого цикл переривається.

Це відбувається у всіх таких випадках. feof()не повертає істину, поки після прочитання в потоці не зустрінеться кінець файлу. Мета 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(!feof(p) && !ferror(p))або існує цілком реальна можливість нескінченного циклу, ймовірно, виводячи всі види сміття як недійсні дані обробляються.

Отже, підсумовуючи це, хоча я не можу з впевненістю стверджувати, що ніколи не буває ситуації, в якій може бути семантично правильним написання " while(!feof(f))" (хоча повинно бути бути ще одна перевірка всередині циклу з розривом, щоб уникнути нескінченного циклу на помилку читання ), так буває, що майже напевно завжди неправильно. І навіть якщо коли-небудь виник випадок, коли це було б правильно, це настільки ідіоматично неправильно, що це був би не правильний спосіб написання коду. Кожен, хто бачить цей код, повинен негайно вагатися і сказати: "це помилка". І, можливо, ляпніть автора (якщо автор не є вашим начальником, і в цьому випадку не рекомендується розсуд.)


7
Впевнені, що це неправильно - але окрім того, що це не "некрасиво потворно".
nobar

89
Вам слід додати приклад правильного коду, оскільки я думаю, що багато людей приїдуть сюди в пошуках швидкого виправлення.
jleahy

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()вказує, чи намагався прочитати останній кінець файлу. Це означає, що він має невеликий прогнозуючий ефект: якщо це правда, ви впевнені, що наступна операція введення не вдасться (ви не впевнені, що попередня помилка 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
Добігати до кінця файлу не є помилкою, тому я ставлю під сумнів фразу: "Операції введення можуть закінчуватися з інших причин, ніж кінець файлу".
Вільям Перселл

@WilliamPursell, досягнення eof не обов'язково є помилкою, але неможливо виконати операцію введення через eof - це одна. І в C неможливо надійно виявити 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 () може не достовірно прогнозувати, що майбутні запити будуть отримувати дані, це може надійно вказувати, що майбутні запити не будуть . Можливо, має існувати функція статусу, яка вказуватиме "Цілком імовірно, що майбутні запити читання будуть успішними", з семантикою, що після прочитання до кінця звичайного файлу, якісна реалізація повинна сказати, що майбутні читання навряд чи зможуть відмовитись з якоїсь причини для вірити, що вони можуть .
Supercat

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.