「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

抽象的でハイレベルな視点を提供したいと思います。

並行性と同時性

I / O操作は環境と相互作用します。環境はプログラムの一部ではなく、管理下にもありません。環境は、プログラムと「同時に」本当に存在します。同時発生するすべてのものと同様に、「現在の状態」についての質問は意味を成しません。同時発生イベント間での「同時性」の概念はありません。状態の多くのプロパティは単に存在しませ同時にし。

より正確に説明しましょう。「もっとデータがありますか」と質問したいとします。これを並行コンテナーまたはI / Oシステムに要求できます。しかし、答えは一般に実行不可能であり、したがって意味がありません。だから、もしコンテナが「はい」と言ったらどうなるでしょう–読んでみると、データがなくなっているかもしれません。同様に、答えが「いいえ」の場合、読み込もうとした時点で、データが到着した可能性があります。結論は、単に存在していることです「データがあります」などのプロパティはありません。考えられる答えに応じて有意義な行動を取ることができないためです。(状況は、バッファされた入力で少し良くなります。ある種の保証を構成する「はい、私はデータがあります」と思われるかもしれませんが、それでも逆のケースに対処できる必要があります。そして、出力の状況確かに、私が説明したのと同じくらい悪いです。そのディスクまたはそのネットワークバッファーがいっぱいであるかどうかはわかりません。)

それは不可能であり、実際には国連と結論我々は、だから、合理的なかどうかをI / Oシステムに依頼する、となります I / O操作を実行することができます。それとやり取りできる唯一の可能な方法は(同時コンテナーと同様に)、、操作試行して、操作が成功したか失敗したかを確認することです。環境と対話するその瞬間に、そのとき初めて、対話が実際に可能であったかどうかを知ることができます。その時点で、対話の実行にコミットする必要があります。(もしそうなら、これは「同期ポイント」です。)

EOF

これでEOFに到達しました。EOFは、試行された I / O操作から得られる応答です。これは、何かを読み書きしようとしたが、そうするときにデータの読み取りまたは書き込みに失敗し、代わりに入力または出力の終わりが検出されたことを意味します。これは、C標準ライブラリ、C ++ iostream、その他のライブラリのいずれであっても、本質的にすべてのI / O APIに当てはまります。I / O操作が成功する限り、将来の操作が成功するかどうかはわかりません。あなたがしなければならない、常に最初の操作を試してみて、その後、成功または失敗に応じます。

例のそれぞれにおいて、ノート我々は慎重にことを最初の I / O操作を試みて、その後、それが有効であれば、結果を消費します。さらに、I / O操作の結果は常に使用する必要がありますが、結果はそれぞれの例で異なる形状と形式をとります。

  • 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」という実際の単語を綴ることはほとんどありません。通常、エラー条件は、他の方法でより興味深い方法で検出されます(たとえば、必要なだけのI / Oを実行できないなど)。すべての例で、EOF状態が発生したことを明示的に通知できるAPI機能がいくつかありますが、これは実際にはそれほど有用な情報ではありません。それは、私たちがよく気にするよりもはるかに詳細です。重要なのは、I / Oが失敗したかどうかよりも、I / Oが成功したかどうかです。

  • EOF状態を実際に照会する最後の例:文字列があり、それが整数を表し、空白以外の最後に余分なビットがないことをテストしたいとします。C ++ iostreamを使用すると、次のようになります。

    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
    }

    ここでは2つの結果を使用します。1つ目はiss、ストリームオブジェクト自体であり、フォーマットされた抽出がvalue成功したことを確認します。しかし、次に、空白も消費した後、別のI / O /操作を実行iss.get()し、EOFとして失敗することを期待します。これは、フォーマットされた抽出によって文字列全体がすでに消費されている場合です。

    C標準ライブラリstrto*lでは、終了ポインタが入力文字列の最後に到達したことを確認することで、関数を使用して同様のことを実現できます。

答え

while(!feof)それは無関係な何かをテストし、あなたが知る必要がある何かをテストするのに失敗するので間違っています。その結果、正常に読み取られたデータにアクセスしていると想定するコードが誤って実行され、実際にはこれが発生することはありません。


34
@CiaPan:それは本当だとは思いません。C99とC11の両方でこれが可能です。
Kerrek SB、2015

11
しかし、ANSI Cはそうではありません。
CiaPan 2015年

3
@JonathanMee:私が言及するすべての理由でそれは悪いです:あなたは未来を見ることができません。将来どうなるかわからない。
Kerrek SB、2015

3
@JonathanMee:はい、それは適切ですが、通常はこのチェックを操作に組み合わせることができます(ほとんどのiostreams操作はストリームオブジェクトを返しますが、それ自体にブール変換があります)。戻り値を無視します。
Kerrek SB、2015

4
3番目の段落は、受け入れられ、非常に賛成された回答に対して、著しく誤解を招く/不正確です。feof()「より多くのデータがあるかどうかをI / Oシステムに尋ねる」ことはありません。feof()(Linux)のマンページによれば、「streamが指すストリームのファイルの終わりインジケーターをテストし、設定されている場合はゼロ以外を返します。」(また、明示的な呼び出しがclearerr()このインジケーターをリセットする唯一の方法です)。この点で、ウィリアムパーセルの答えははるかに優れています。
Arne Vogel

234

(読み取りエラーがない場合)作成者の予想よりも1回だけループに入るので、これは誤りです。読み取りエラーがある場合、ループは終了しません。

次のコードを検討してください。

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

このプログラムは、入力ストリームの文字数よりも1つ多い値を一貫して出力します(読み取りエラーがない場合)。入力ストリームが空の場合を考えてみましょう:

$ ./a.out < /dev/null
Number of characters read: 1

この場合、feof()データが読み込まれる前に呼び出されるため、falseを返します。ループに入り、fgetc()呼び出され(そしてを返しEOF)、カウントがインクリメントされます。次にfeof()呼び出されてtrueを返し、ループを中止します。

これは、そのようなすべての場合に発生します。 ストリームの読み取りがファイルの終わりに到達するfeof()まで trueを返しません。の目的はfeof()、次の読み取りがファイルの終わりに到達するかどうかを確認することではありません。の目的はfeof()、読み取りエラーとファイルの終わりに達したことを区別することです。fread()が0を返す場合は、feof/ ferrorを使用して、エラーが発生したかどうか、またはすべてのデータが消費されたかどうかを判断する必要があります。同様にif fgetcが返されますEOF。 freadが0を返した後、またはが戻った後にfeof()のみ役立ちます。その前に、は常に0を返します。fgetcEOFfeof()

呼び出す前に、読み取り(fread()、またはfscanf()、またはfgetc())の戻り値を確認する必要がありますfeof()

さらに悪いことに、読み取りエラーが発生した場合を考えてください。その場合、fgetc()はを返しEOFfeof()falseを返し、ループは終了しません。while(!feof(p))が使用されるすべてのケースで、少なくともforのループ内にチェックがあるferror()か、少なくともwhile条件が置き換えられるwhile(!feof(p) && !ferror(p))か、無限ループの非常に現実的な可能性があり、おそらくすべての種類のゴミを無効なデータが処理されています。

つまり、要約すると、「while(!feof(f))」を書くことが意味的に正しいかもしれない状況が決してないということを確実に述べることはできませが、読み取りエラーでの無限ループを回避するために、ブレーク付きのループ内に別のチェックが必要です)、それはほとんど間違いなく常に間違っているケースです。そして、それが正しいところに事件が起こったとしても、それはコードを書くための正しい方法ではないほど慣用的に間違っている。そのコードを見た人は、すぐにためらって「それはバグだ」と言ってください。そして、おそらく著者を平手打ちします(著者があなたの上司である場合を除き、その場合は裁量が推奨されます)。


7
確かにそれは間違いです-それはさておき、それは「非常に醜い」ではありません。
nobar 2013

89
多くの人が簡単な修正を求めてここに来ると思うので、正しいコードの例を追加する必要があります。
jleahy

6
@Thomas:私はC ++のエキスパートではありませんが、file.eof()は事実上と同じ結果を返すと信じているfeof(file) || ferror(file)ため、非常に異なっています。ただし、この質問はC ++に適用することを意図したものではありません。
ウィリアムパーセル

6
@ m-ricも正しくありません。失敗した読み取りを引き続き処理しようとするためです。
Mark Ransom

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:言われたように、「一般的なループ状態ではない」へへ。私はそれが必要だったケースを本当に考えることはできません。通常、私はエラー処理を意味するすべてを備えた「私が欲しいものを読むことができるか」に興味があります
Erik

@pmg:言ったように、あなたはめったに望みませんwhile(!eof(f))
Erik

9
より正確には、「ファイルの終わりを超えて読み取ろうとしたことがなく、読み取りエラーが発生していない」という条件は、ファイルの feof終わりを検出することではありません。エラーのために、または入力が使い果たされたために読み取りが短かったかどうかを判断することについてです。
ウィリアムパーセル2013

35

feof()ファイルの終わりを超えて読み込もうとしたかどうかを示します。つまり、予測効果はほとんどありません。trueの場合、次の入力操作が失敗することは確かです(前の操作がBTWに失敗したことはわかりません)。しかし、falseの場合、次の入力はわかりません。操作は成功します。さらに、ファイルの終わり以外の理由で入力操作が失敗する可能性があります(フォーマットされた入力のフォーマットエラー、純粋なIO障害-ディスク障害、ネットワークタイムアウト-すべての入力の種類)、したがって、予測可能であってもファイルの終わり(および予測的なAdaの実装を試みた人は、スペースをスキップする必要がある場合に複雑になる可能性があり、インタラクティブなデバイスに望ましくない影響があることを通知します-次の入力を強制する場合があります)前のものの処理を開始する前の行)、

したがって、Cでの正しいイディオムは、IO操作の成功をループ条件としてループし、失敗の原因をテストすることです。例えば:

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
ファイルの終わりに到達することはエラーではないので、「入力操作はファイルの終わり以外の理由で失敗する可能性があります」という言い回しに疑問を投げかけます。
William Pursell 2012

@ WilliamPursell、eofへの到達は必ずしもエラーではありませんが、eofが原因で入力操作を実行できないことは1つです。また、Cでは、入力操作を失敗させずにeofを確実に検出することはできません。
AProgrammer 2012

最後の同意elseでは不可能sizeof(line) >= 2fgets(line, sizeof(line), file)病的ではなく可能size <= 0fgets(line, size, file)。たぶんsizeof(line) == 1
chux-モニカを2015年

1
そのすべての「予測値」の話...私はそのようにそれについて考えたことはありません。私の世界でfeof(f)は、何も予測しません。PREVIOUS操作がファイルの終わりに達したことを示しています。それ以上でもそれ以下でもありません。また、前の操作がなかった(開いただけの)場合、ファイルが最初から空であっても、ファイルの終わりは報告されません。したがって、上記の別の回答の同時実行性の説明のほかに、ループしない理由はないと思いますfeof(f)
BitTickler 2017

@AProgrammer:「永続的な」EOFが原因であるか、利用可能なデータがまだないためかを問わず、ゼロを生成する「最大Nバイトまでの読み取り」要求はエラーではありません。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()返します。intchar
Andrew Henle

標準的なイディオムwhile( (c = getchar()) != EOF)は非常に「このようなもの」であるように私には思えます。
ウィリアムパーセル

while( (c = getchar()) != EOF)GNU C 10.1.0を実行しているデスクトップの1つで動作しますが、GNU C 9.3.0を実行している私のRaspberry Pi 4では失敗します。私のRPi4では、ファイルの終わりを検出せず、そのまま続行します。
スコットディーガン

@AndrewHenleそうです!変更char cint c作品!ありがとう!!
Scott Deagan
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.