閉じる

C言語のferrorとfeofでEOFとエラーを正しく判定する

ファイルや標準入出力から読み書きをすると、データが尽きたのか、エラーが起きたのかを正しく見分ける必要があります。

C言語ではferrorfeofがその判断の鍵です。

本記事では、EOFとエラーの区別を基礎から丁寧に解説し、各入力関数ごとの正しい判定パターンありがちな落とし穴、そしてclearerrや標準ストリームでの実践的な使い方までをまとめます。

ferrorとfeofの基本

EOFとは何か

EOFはEnd Of File(ファイル終端)を表す概念と、Cライブラリ関数が返すEOFという整数マクロの2つを指すことがあります。

概念としてのEOFは「読み進めた結果、もはやデータが残っていない状態」を意味します。

一方、fgetcなどが返すEOFは多くの実装で-1のint値です。

バイト値0(ヌル文字)とは別物であり、混同しないことが大切です。

エラーとは何か

エラーはデバイス障害、権限不足、メディアの満杯、通信路の切断など、入出力操作そのものが失敗した状態です。

関数はしばしば特別な戻り値(例: EOFNULL、負値、読み取り個数0)で失敗を示しますが、その原因が終端なのかエラーなのかは戻り値だけでは区別できないことがあります。

そこでferrorfeofを使います。

ストリームの状態フラグを理解する

CのFILEストリームは、エラーフラグEOFフラグという2つの「状態ビット」を持ちます。

これらは「スティッキー(一度立つとそのまま)」で、clearerrを呼ぶまで保持されます。

つまり、

  • 終端に到達したらfeof(fp)が非0になり、その後の読み取りは継続してEOFを返すことがあります。
  • 入出力エラーが起きたらferror(fp)が非0になります。
  • どちらか一方、あるいは稀に両方が同時に立つ可能性があります。

フラグが立っているだけではファイル位置は変わりません

フラグをクリアしても、読み書き位置(ファイルオフセット)は元に戻らない点に注意してください。

戻り値とferror/feofの関係

多くの標準関数は「成功時の通常値」と「失敗や終端を示す特殊値」を持ちますが、特殊値が返ったときにfeofferrorを見て原因を判定します。

代表的な関数の戻り値は次のとおりです。

関数成功時終端/データなし真のエラー判別方法
fgetc0〜255のint(実際はunsigned charintに拡張)EOFEOFfeof(fp)ferror(fp)を見る
fgetsバッファ先頭アドレスNULLNULLfeof(fp)ferror(fp)で区別
fscanf成功変換数(>=0)入力枯渇でEOF入力エラーでもEOFになることありfeof(fp)/ferror(fp)で区別。0は「書式不一致」
fread読めた要素数(0〜要求数)通常0(または短読み)0(または短読み)要求数より小さければfeof/ferrorで判定
fprintf/fputs/fwrite非負・要求数負値や少ない書込数書いた直後やfflush後にferrorを確認

「戻り値だけで終端とエラーを確実に区別できない関数がある」ことを覚えておくと、原因究明がスムーズになります。

読み込み後の判定パターン

fgetcでEOFとエラーを判定

1文字ずつ読む最小パターンです。

ループの条件にfgetcを置き、終了後にferrorfeofを調べます。

C言語
#include <stdio.h>
#include <stdlib.h>

// 文字数を数えつつ、EOFとエラーを区別するデモ
int main(void) {
    const char *path = "sample.txt";
    FILE *fp = fopen(path, "r");
    if (!fp) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    long count = 0;
    int ch;

    // ループ条件でfgetcがEOFかどうかを判定
    while ((ch = fgetc(fp)) != EOF) {
        // ここに処理を書く(例: 出力しないでカウントだけ)
        count++;
    }

    // ループを抜けた理由を特定
    if (ferror(fp)) {
        // 入出力エラー
        perror("read error");
        fclose(fp);
        return EXIT_FAILURE;
    } else if (feof(fp)) {
        // 終端に達しただけ
        printf("OK: %ld bytes read (EOF reached)\n", count);
    } else {
        // 通常ここには来ない(保険)
        fprintf(stderr, "Unknown reason to stop reading.\n");
    }

    fclose(fp);
    return EXIT_SUCCESS;
}
実行結果
OK: 57 bytes read (EOF reached)

ポイントは、読み込みの「後」ではなく「最後の読み込み直後」にferror/feofを確認することです。

別の操作(例: printf)を挟むと、何が原因だったのか追跡しづらくなります。

fgetsで行読み込みの成否を判定

fgetsは成功時にバッファ先頭を返し、失敗(終端含む)でNULLを返します。

したがって、ループ条件はwhile (fgets(...) != NULL)とし、抜けたときにferror/feofで理由を判定します。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    const char *path = "lines.txt";
    FILE *fp = fopen(path, "r");
    if (!fp) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    char buf[128];
    long lines = 0;

    // 正しいループ: fgetsの戻り値で回す
    while (fgets(buf, sizeof buf, fp) != NULL) {
        // 読んだ行をそのまま出力(末尾の改行はそのまま)
        fputs(buf, stdout);
        lines++;
    }

    if (ferror(fp)) {
        perror("read error");
        fclose(fp);
        return EXIT_FAILURE;
    } else if (feof(fp)) {
        printf("Total lines: %ld\n", lines);
    }

    fclose(fp);
    return EXIT_SUCCESS;
}
実行結果
first line
second line
Total lines: 2

行が長すぎてバッファに収まらない場合、同一行が複数回に分割されて読み込まれます。

これはエラーではありません。

fscanfの戻り値とferror/feofを併用

fscanfは「成功して変換・格納できた項目数」を返します。

0は「書式不一致」(入力はあるが数値などに解釈できない)EOFは「入力枯渇や入出力エラー」を意味します。

終了時にfeofferrorで理由を判断します。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

// ファイルから整数を読み取り、書式不一致は1文字捨てて継続する例
int main(void) {
    const char *path = "nums.txt"; // 例: "10 20 X 30" のような内容
    FILE *fp = fopen(path, "r");
    if (!fp) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    long sum = 0;
    int value;
    for (;;) {
        int rc = fscanf(fp, "%d", &value);
        if (rc == 1) {
            sum += value;
        } else if (rc == 0) {
            // 書式不一致: 1文字読み捨てて再試行
            int ch = fgetc(fp);
            if (ch == EOF) break;  // ここでループを抜け、下でfeof/ferrorを判定
        } else { // rc == EOF
            break;
        }
    }

    if (ferror(fp)) {
        perror("read error");
        fclose(fp);
        return EXIT_FAILURE;
    } else if (feof(fp)) {
        printf("Sum = %ld (EOF)\n", sum);
    } else {
        printf("Stopped for unknown reason. Sum = %ld\n", sum);
    }

    fclose(fp);
    return EXIT_SUCCESS;
}
実行結果
Sum = 60 (EOF)

標準入力stdinscanfを使う場合も同様で、rc == 0は書式不一致、rc == EOFは終端/エラーです。

freadで短読みを判定

freadは要求より少ないバイト数しか返さない「短読み」が普通に発生します。

返った要素数が要求数より小さい場合、feofferrorを確認します。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    const char *path = "data.bin";
    FILE *fp = fopen(path, "rb");
    if (!fp) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    unsigned char buf[4096];
    size_t total = 0;

    for (;;) {
        size_t n = fread(buf, 1, sizeof buf, fp);
        if (n > 0) {
            total += n;
            // buf[0..n-1]を処理する
        }
        if (n < sizeof buf) { // 短読みが起きた
            if (ferror(fp)) {
                perror("read error");
                fclose(fp);
                return EXIT_FAILURE;
            } else if (feof(fp)) {
                printf("Read %zu bytes (EOF)\n", total);
                break;
            } else {
                // 非ブロッキング等の特殊ケースで起き得るが、通常は来ない
                fprintf(stderr, "Short read for unknown reason.\n");
                break;
            }
        }
    }

    fclose(fp);
    return EXIT_SUCCESS;
}
実行結果
Read 8192 bytes (EOF)

書き込み時はferrorでエラー判定

書き込み系(例: fputc, fputs, fprintf, fwrite)も戻り値で失敗を検出し、さらにferrorでストリームにエラーが残っていないか確認します。

バッファリングのため、エラーはfflushfcloseまで遅延して現れることがあります。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // 例: stdoutに書いてから明示的にフラッシュしてエラー確認
    if (fprintf(stdout, "hello\n") < 0) {
        perror("fprintf");
        return EXIT_FAILURE;
    }
    if (fflush(stdout) == EOF || ferror(stdout)) {
        // パイプ切断やディスク満杯など
        perror("flush/write error");
        return EXIT_FAILURE;
    }
    // ファイルに書く場合も、同様にferror(fp)やfcloseの戻り値を確認する
    printf("write ok\n");
    return EXIT_SUCCESS;
}
実行結果
hello
write ok

書き込みエラーは気づきにくいため、重要な出力の直後はfflushferrorを確認するのが堅実です。

よくある間違いと回避策

while(!feof(fp))は使わない

典型的な誤りwhile (!feof(fp)) { ... }です。

feof(fp)は「終端を超えて読もうとして初めて立つ」ため、この書き方ではfgetsなどがNULLを返した後にループ本体が実行されてしまい、最後の行を二重処理したり、ゴミを処理したりします。

誤りの例:

C言語
// NG: feofで回すのは間違い
while (!feof(fp)) {
    if (fgets(buf, sizeof buf, fp) == NULL) {
        // ここに来る時点で既に読めていない
        // なのにループ本体が1回余分に回る
        process(buf); // ゴミ
    }
}

正しいループ構造

「入力関数の戻り値でループを回し、抜けたらferror/feofを調べる」のが鉄則です。

代表パターンをまとめます。

C言語
// fgetc
int ch;
while ((ch = fgetc(fp)) != EOF) { /* use ch */ }
if (ferror(fp)) { /* error */ } else if (feof(fp)) { /* eof */ }

// fgets
while (fgets(buf, sizeof buf, fp) != NULL) { /* use buf */ }
if (ferror(fp)) { /* error */ } else if (feof(fp)) { /* eof */ }

// fscanf
int rc;
while ((rc = fscanf(fp, "%d", &x)) == 1) { /* use x */ }
if (rc == EOF) {
    if (ferror(fp)) { /* error */ } else if (feof(fp)) { /* eof */ }
} else { /* rc == 0: format mismatch */ }

// fread (要素数Nを読む)
size_t n;
while ((n = fread(buf, 1, sizeof buf, fp)) > 0) { /* use n bytes */ }
if (ferror(fp)) { /* error */ } else if (feof(fp)) { /* eof */ }

EOFと文字’\0’は別物

EOFintの負値(多くは-1)で、'\0'(ヌル文字)はcharの0です。

バイナリデータには0バイトが普通に含まれますが、これは終端を意味しません。

常にintで受け取ってEOFと比較します。

C言語
#include <stdio.h>

int main(void) {
    printf("EOF=%d, '\
#include <stdio.h>
int main(void) {
printf("EOF=%d, '\\0'=%d\n", EOF, '\0');  // 多くの系で -1 と 0
return 0;
}
'=%d\n", EOF, '
#include <stdio.h>
int main(void) {
printf("EOF=%d, '\\0'=%d\n", EOF, '\0');  // 多くの系で -1 と 0
return 0;
}
'); // 多くの系で -1 と 0 return 0; }
実行結果
EOF=-1, '
EOF=-1, '\0'=0
'=0

文字列関数はヌル終端'\0'で文字列の終わりを示しますが、ストリーム入出力の終端はEOFフラグで示されます。

役割が違うことを明確に区別してください。

状態のクリアと標準ストリームでの利用

clearerrでferror/feofをクリア

clearerr(FILE *fp)は、そのストリームのエラーフラグとEOFフラグを両方クリアします。

終端に達してから同じストリームで処理を続けたい場合や、エラー後に再試行する場合に必要です。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    FILE *fp = fopen("sample.txt", "r");
    if (!fp) { perror("fopen"); return EXIT_FAILURE; }

    // ざっくり全部読む
    while (fgetc(fp) != EOF) { /* consume */ }

    if (feof(fp)) {
        puts("EOF reached");
    }

    // 状態フラグをクリア
    clearerr(fp);

    // 注意: 位置は末尾のまま。ここで読み直したければ位置を戻す必要がある(方法の説明は割愛)
    if (!feof(fp) && !ferror(fp)) {
        puts("flags cleared");
    }

    fclose(fp);
    return EXIT_SUCCESS;
}
実行結果
EOF reached
flags cleared

clearerrは位置を動かしません

再読込したい場合は位置を適切に調整してください(位置操作の詳細は別記事の範囲です)。

stdinのEOFとエラーを判定

標準入力からの読み取りで終端を与えるには、UNIX系ではCtrl+D(行頭で押すとEOF)、WindowsではCtrl+Zの後にEnterが目安です。

以下は整数をカウントし、終了理由を判定する例です。

C言語
#include <stdio.h>

int main(void) {
    int x, count = 0;
    for (;;) {
        int rc = scanf("%d", &x);
        if (rc == 1) {
            count++;
        } else if (rc == 0) {
            // 書式不一致(例: 文字が入力された)
            // 1文字捨てて続行
            int ch = getchar();
            if (ch == EOF) break;
        } else { // rc == EOF
            break;
        }
    }

    if (ferror(stdin)) {
        perror("stdin error");
    } else if (feof(stdin)) {
        printf("Read %d integers (stdin EOF)\n", count);
    } else {
        printf("Stopped (unknown reason), count=%d\n", count);
    }
    return 0;
}
実行結果
Read 3 integers (stdin EOF)

対話的入力では、エラーよりも書式不一致(0)やEOFのほうが頻出です。

終了理由を丁寧に切り分けると、ユーザーに適切なメッセージを返せます。

stdout/stderrのferrorで出力エラーを検出

標準出力stdoutや標準エラーstderrでもferrorは使えます。

パイプの相手が終了していたり、端末/ファイルへの書き込みに失敗した場合にエラーフラグが立ちます。

C言語
#include <stdio.h>

int main(void) {
    // 大量出力の例(パイプ切断などで途中失敗する可能性あり)
    for (int i = 0; i < 100000; i++) {
        if (fprintf(stdout, "line %d\n", i) < 0) {
            perror("fprintf");
            return 1;
        }
    }
    // フラッシュ時にもエラーを再確認
    if (fflush(stdout) == EOF || ferror(stdout)) {
        perror("stdout error");
        return 1;
    }

    // stderrも同様にチェック可能
    if (fprintf(stderr, "done\n") < 0 || ferror(stderr)) {
        perror("stderr error");
        return 1;
    }
    return 0;
}
実行結果
line 0
line 1
...
done

エラーは遅延して発生するため、fprintfの戻り値だけでなく、fflushや最終的なfcloseの戻り値、そしてferrorも確認することが重要です。

まとめ

終端(EOF)と入出力エラーの区別は、堅牢なI/O処理の第一歩です。

Cのストリームはfeofferrorというスティッキーな状態を持ち、戻り値だけでは原因を判断できない場面が多くあります。

正しいループ構造は、「入力関数の戻り値で回し、抜けたらfeof/ferrorで理由を切り分ける」ことです。

while(!feof(fp))は避け、clearerrで必要に応じて状態をリセットし、標準ストリームでもferrorを活用して遅延エラーを検出してください。

これらを徹底することで、初心者の方でも予期せぬ不具合を大幅に減らし、安心してファイル操作や標準入出力を扱えるようになります。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!