web-dev-qa-db-ja.com

「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))ループの何が問題になっていますか?

510
William Pursell

(読み取りエラーがない場合)作成者が予想するよりも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()は、ストリームの読み取りがファイルの終わりに達するまでafterになるまでtrueを返しません。 feof()の目的は、次の読み取りがファイルの終わりに到達するかどうかをチェックすることではありません。 feof()の目的は、読み取りエラーとファイルの終わりに到達したことを区別することです。 fread()が0を返す場合、feof/ferrorを使用して、エラーが発生したかどうか、またはすべてのデータが消費されたかどうかを判断する必要があります。同様に、fgetcEOFを返す場合。 feof()は有用ですafter freadがゼロを返したか、fgetcEOFを返しました。その前に、feof()は常に0を返します。

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

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

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

224
William Pursell

いいえ、それは必ずしも悪いことではありません。あなたのループ条件が "ファイルの終わりを超えて読み込もうとしていない間"であるなら、あなたはwhile (!feof(f))を使います。しかしながら、これは一般的なループ条件ではありません - 通常、何か他のものをテストしたい(「もっと読むことができますか」など)。 while (!feof(f))は間違っていません、それは used wrongです。

60
Erik

feof()は、ファイルの終わりを超えて読み込もうとしたかどうかを示します。それはそれがほとんど予測効果を持っていないことを意味します:それが本当なら、あなたは次の入力操作が失敗することを確信しています(前のものがBTWに失敗したことを確信できません)操作は成功します。さらに、ファイルの終わり以外の理由で入力操作が失敗する可能性があります(フォーマットされた入力のフォーマットエラー、純粋なIO失敗 - ディスク障害、すべての入力タイプのネットワークタイムアウト)。ファイルの終わりを予測できれば(そしてAda oneを実装しようとした人なら誰でも予測可能ですが、スペースをスキップする必要がある場合は複雑になる可能性があり、対話型デバイスには望ましくない影響があります)。前の行の処理を開始する前に次の行の入力を強制すると、失敗を処理できなければなりません。

そのため、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) */
}
29
AProgrammer

いい答え、私はそのようなループをやろうとしていたので私はちょうど同じことに気づいた。したがって、そのシナリオでは間違っていますが、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
}
10
tesch1