閉じる

C言語のferrorとfeofを解説|正しいエラー判定とEOF処理

C言語でファイル入出力を行うとき、単にfreadfgetsの戻り値だけを見ていると、EOFなのかエラーなのかが判別できず、不具合の原因になります。

この記事では、標準I/Oで用意されているferrorfeofの正しい使い方を、典型的な誤用例も交えながら詳しく解説します。

ferrorとfeofとは?C言語の標準I/Oにおける役割

標準I/Oライブラリは、ファイルをFILE*という抽象化されたストリームとして扱います。

このストリーム内部には、読み書き位置だけでなく、エラーが発生したかどうかや、ファイルの終端(EOF)に到達したかどうかを表すフラグが保持されています。

ferrorとfeofは、この「エラーフラグ」と「EOFフラグ」の状態を問い合わせるための関数です。

どちらもint型の戻り値を持ち、0か非0かで状態を表しますが、参照しているフラグが異なります。

ferrorの基本動作と戻り値

ferrorは、指定したストリームに対して入出力エラーが発生しているかどうかを調べる関数です。

C言語
#include <stdio.h>

int ferror(FILE *stream);

戻り値の意味は次の通りです。

  • 0 … エラーなし
  • 非0 … 何らかの入出力エラーが発生している

重要なのは、ferrorは「そのストリームで過去に発生したエラー状態」を見る関数だということです。

1回のfreadfwriteだけを対象にしているのではなく、そのストリームについて蓄積されたエラーフラグを参照します。

エラーフラグはclearerrrewindなどを呼び出すまで保持されるため、ferrorを使うときは、どのタイミングでフラグをクリアするかも意識する必要があります。

feofの基本動作と戻り値

feofは、指定したストリームがEOF(End Of File)に到達したかどうかを調べる関数です。

C言語
#include <stdio.h>

int feof(FILE *stream);

戻り値はferrorと同じく、

  • 0 … EOFではない
  • 非0 … EOFに到達している

を意味します。

ただし注意が必要なのは、「EOFに到達した」というフラグが立つのは読み取り関数がEOFを検出した後だという点です。

つまり、feofを使った正しいパターンは「読み取り関数を呼んだあと、その結果を見てからfeofを確認する」形になります。

この点は後のセクションで詳しく解説します。

EOF(End Of File)との関係と違い

C言語ではEOFというマクロも定義されていますが、これはferrorfeofとは用途が異なります。

EOFマクロは、主にfgetcなどの関数が戻り値として返す特殊値です。

一方でfeofは、ストリームの内部状態として「EOFフラグ」が立っているかを調べる関数です。

両者の違いを表にまとめると次のようになります。

要素EOFマクロfeof関数
種類整数値マクロ関数
主な用途読み取り関数の戻り値と比較ストリームのEOF状態の確認
単位1回の関数呼び出しの結果ストリーム全体の蓄積された状態
典型的な使用例if (c == EOF)if (feof(fp))

「戻り値としてEOFかどうか」と「ストリームがEOF状態かどうか」を混同しないことが重要です。

ferrorを使った正しいエラー判定

ferrorで検出できるI/Oエラーの種類

ferrorそのものは、エラーの種類を細かく区別してくれるわけではありません。

戻り値は「0か非0か」だけですが、「そのストリームで、読み取りまたは書き込みのどこかでエラーが発生した」ことを教えてくれます。

C標準だけでは種類までは規定されていませんが、一般的なOSや実装では、次のような原因でエラーが発生します。

  • 読み取り時に、ディスクやデバイスのI/Oエラーが発生した場合
  • 書き込み時に、ディスク容量不足やネットワーク切断などが起きた場合
  • 権限不足により書き込みができなかった場合
  • 既にクローズされたファイルポインタを誤って使った場合(未定義動作ですが、実装によってはエラーとなる)

どの操作で発生したかferrorだけでは分からないため、通常はfreadfwriteの戻り値と組み合わせて判断します。

ferrorの具体的な使用例

まずは、シンプルな読み取り処理にferrorを組み込んだ例を示します。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.bin", "rb");   // バイナリ読み取りモードでオープン
    if (fp == NULL) {                     // オープン自体のエラーはferrorではなく戻り値で判定
        perror("fopen failed");
        return 1;
    }

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

    // 読み取れるだけ読み取るループ
    while (1) {
        size_t n = fread(buf, 1, sizeof(buf), fp);  // 最大1024バイト読み取り
        if (n > 0) {
            total += n;
            // 読み取ったデータを使った処理を書く(ここでは省略)
        }

        if (n < sizeof(buf)) {
            // ここに来たら、EOFかエラーのどちらかを疑う
            if (ferror(fp)) {
                // 入出力エラーが発生している
                perror("I/O error while reading");
                fclose(fp);
                return 1;
            }
            if (feof(fp)) {
                // 正常にEOFに到達
                break;
            }
            // 通常はここには来ないが、保険としてループを抜ける
            break;
        }
    }

    printf("Total bytes read: %zu\n", total);
    fclose(fp);
    return 0;
}

実行結果の一例(正常に読み終えた場合)は次のようになります。

実行結果
Total bytes read: 8192

この例では、freadの戻り値nsizeof(buf)より小さくなったときに、ferrorとfeofの両方を使って「エラーかEOFか」を判別しています。

読み取り専用の処理であってもferrorを確認することで、ネットワークドライブやUSBメモリの抜去などによるエラーを検出できます。

ferrorとerrnoの違いと使い分け

errnoは、C標準ライブラリやOSのシステムコールが、最後に発生したエラーの詳細な原因を表すためのグローバル変数です。

一方、ferror特定のFILEストリームにひも付いた「エラー発生済みフラグ」です。

役割とスコープの違いをまとめると次のようになります。

項目ferrorerrno
単位1つのFILE*ごとプロセス(スレッド)全体
情報の粒度「エラーがあったかどうか」だけエラー種類を表すエラーコード
リセット方法clearerrなどで明示的に多くは関数呼び出し後に書き換わる
想定用途ループ中に「このストリームに異常が出たか」確認perrorstrerrorでメッセージ表示

実際のコードでは、次のような使い分けが典型的です。

  1. ファイル操作関数(freadなど)の戻り値をチェックする。
  2. その結果がおかしいとき、ferrorで「I/Oエラーかどうか」を確認する。
  3. I/Oエラーであれば、errnoperrorstrerrorで解釈し、原因を詳しく報告する。

ferror発生時のクリア方法

一度エラーが起きると、そのストリームにはエラー状態フラグが残り続けます

このフラグをクリアしないまま再利用すると、ferrorがいつまでも非0を返し続け、後続の処理ロジックが誤動作する可能性があります。

エラー状態やEOF状態をクリアするにはclearerrを使います。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("readonly.txt", "r");
    if (fp == NULL) {
        perror("fopen failed");
        return 1;
    }

    // 誤って書き込みを試みる(多くの環境でエラーになる)
    if (fputc('A', fp) == EOF) {
        if (ferror(fp)) {
            printf("Write error detected.\n");
        }
    }

    // ここでエラー状態をクリアする
    clearerr(fp);

    // 再度、読み取りなどを行うことができる
    int c = fgetc(fp);
    if (c == EOF) {
        if (feof(fp)) {
            printf("EOF reached after clearerr.\n");
        } else if (ferror(fp)) {
            printf("Read error after clearerr.\n");
        }
    }

    fclose(fp);
    return 0;
}

想定される実行結果の一例です。

実行結果
Write error detected.
EOF reached after clearerr.

このようにclearerrエラー状態だけでなくEOF状態もクリアします。

途中でファイル位置を巻き戻して再読み込みする場合など、必要に応じて明示的に状態をリセットすることが重要です。

feofを使った正しいEOF処理

feofでEOFを検出するタイミングと注意点

feofはEOF状態フラグを返すだけなので、「いつそのフラグが立つか」を理解していないと誤用につながります。

フラグが立つタイミングはおおむね次の通りです。

  1. 読み取り関数(fgetcfread)を呼ぶ。
  2. ファイルの終端に達してこれ以上データが読めないと判断されたとき、その呼び出しはEOFや0バイトなどを返す。
  3. 同時に、ストリームの内部EOFフラグがセットされる。
  4. 以降でfeof(fp)を呼ぶと非0が返る。

大切なのは「読み取りを1回試みて失敗してからでないと、EOFフラグは立たない」という点です。

そのためfeof「これから読むべきか」を判定するためではなく「今の読み取りでEOFに達したか」を確認するために用いるべきです。

feofの典型的な誤用パターン(while(!feof(fp))問題)

最も有名な誤用パターンの1つが、次のようなループです。

C言語
// 典型的な誤用例(正しく動かない可能性が高い)
while (!feof(fp)) {
    int c = fgetc(fp);
    if (c == EOF) {
        break;
    }
    putchar(c);
}

一見すると「EOFになるまで1文字ずつ読む」ように見えますが、このコードは余計な読み取りを1回行ってしまうため、特定の状況で不具合を引き起こします。

問題点を整理すると次の通りです。

  • ループ条件!feof(fp)を評価した時点では、まだEOFフラグは立っていない。
  • 最後の文字を読んだ直後でも、まだEOFフラグは立っていない。
  • そのため、ループがもう1回回ってしまい、追加でfgetcを呼ぶ。
  • この追加のfgetcEOFを返し、そこでようやくEOFフラグが立つ。

このようにwhile (!feof(fp))という書き方はEOFを検出するタイミングが1回分ずれるため、一般に推奨されません

feofを使った正しいループ構造の書き方

正しいパターンは「まず読み取り、その結果をチェックし、その後に必要に応じてfeofやferrorを確認する」という流れです。

1文字ずつ読み取る場合の典型的な書き方です。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("input.txt", "r");
    if (fp == NULL) {
        perror("fopen failed");
        return 1;
    }

    int c;
    // まずfgetcの結果でループを制御する
    while ((c = fgetc(fp)) != EOF) {
        putchar(c);
    }

    // ここに来た時点では「EOFかエラーか」はまだ分からない
    if (ferror(fp)) {
        // 読み取り途中でエラーが発生していた
        perror("Read error");
        fclose(fp);
        return 1;
    } else if (feof(fp)) {
        // 正常にEOFに到達
        printf("\nEOF reached normally.\n");
    }

    fclose(fp);
    return 0;
}

想定される実行結果の一例です。

実行結果
(ファイルの内容が表示される)
EOF reached normally.

バイナリデータをfreadで読むループでも、考え方は同じです。

C言語
unsigned char buf[4096];
size_t n;

while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
    // buf[0..n-1]までが有効データ
    process(buf, n);
}

// ループを抜けた後にferror/feofを確認する
if (ferror(fp)) {
    // エラー処理
} else if (feof(fp)) {
    // 正常に読み終えた
}

「読み取り関数の戻り値をループ条件に使う」というパターンを定石として覚えておくと、feofの誤用を避けやすくなります。

EOFとエラーの区別

freadなどの戻り値だけでは、「データが読めなかった理由がEOFなのかエラーなのか」を区別できません。

ここでfeofとferrorを組み合わせると、原因の切り分けが可能になります。

次のコードはfreadの戻り値が0だった場合に、EOFかエラーかを判定しています。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.bin", "rb");
    if (fp == NULL) {
        perror("fopen failed");
        return 1;
    }

    unsigned char buf[256];
    size_t n = fread(buf, 1, sizeof(buf), fp);

    if (n == 0) {
        if (feof(fp)) {
            printf("No more data (EOF).\n");
        } else if (ferror(fp)) {
            perror("Read failed");
        } else {
            // 理論上あまり起きないが、何らかの理由で0バイトだったケース
            printf("No data read, but not EOF and no error.\n");
        }
    } else {
        printf("Read %zu bytes.\n", n);
    }

    fclose(fp);
    return 0;
}

実行結果のイメージです。

No more data (EOF).

EOFとエラーを区別せずに処理してしまうと、「途中までしか読めていないのに成功と見なしてしまう」といった深刻なバグにつながります。

必ずfeofferrorの両方を確認する習慣をつけることが重要です。

ferrorとfeofを組み合わせた堅牢なファイル処理

ferrorとfeofを併用した判定フロー

堅牢なファイル処理では、次のようなフローが基本パターンになります。

  1. 読み取り(または書き込み)関数を呼び出す。
  2. 戻り値をチェックし、期待したサイズに達しているか確認する。
  3. 期待より少ない、または0だった場合、ferrorを確認して、I/Oエラーがないか調べる。
  4. エラーでなければfeofを確認し、EOFかどうかを判断する。
  5. どちらでもなければ、特殊なケースとして適宜処理する。

このロジックをコードに落とし込むと次のようになります。

C言語
#include <stdio.h>

int robust_read(FILE *fp, void *buffer, size_t item_size, size_t item_count) {
    size_t total = 0;
    unsigned char *p = buffer;

    while (total < item_count) {
        size_t n = fread(p + total * item_size, item_size,
                         item_count - total, fp);

        if (n == 0) {
            // ここでEOFかエラーかを判定する
            if (ferror(fp)) {
                perror("I/O error in robust_read");
                return -1; // エラーを示す
            }
            if (feof(fp)) {
                // EOFに達したので、これ以上は読めない
                break;
            }
            // どちらでもない場合は異常とみなして終了
            fprintf(stderr, "Unknown read condition.\n");
            return -1;
        }

        total += n;
    }

    return (int)total;  // 実際に読めた要素数を返す
}
実行結果
(この関数単体では出力はありません。呼び出し側で戻り値を使って判定します)

読み取りサイズ不足→ferrorとfeofで原因切り分けというパターンを1つテンプレートとして持っておくと、多くの場面で再利用できます。

テキストファイルとバイナリファイルでの実践例

テキストファイル: 1行ずつ読み込む場合

テキストファイルをfgetsで1行ずつ読み込むときの、代表的なパターンです。

C言語
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("lines.txt", "r");
    if (fp == NULL) {
        perror("fopen failed");
        return 1;
    }

    char line[256];
    while (fgets(line, sizeof(line), fp) != NULL) {
        // 読み取った1行をそのまま表示
        printf("LINE: %s", line);
    }

    // ループを抜けた理由を確認する
    if (ferror(fp)) {
        perror("Error while reading lines");
        fclose(fp);
        return 1;
    } else if (feof(fp)) {
        printf("\nReached EOF while reading lines.\n");
    }

    fclose(fp);
    return 0;
}

想定される実行結果の一例です。

実行結果
LINE: first line
LINE: second line
LINE: third line

Reached EOF while reading lines.

fgetsの戻り値(NULLかどうか)でループを回し、終了後にferror/feofで理由を判定する形になっています。

バイナリファイル: 固定長レコードを読み込む場合

バイナリファイルで一定サイズのレコードを連続して読む場合の例です。

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

typedef struct {
    int id;
    double value;
    char name[32];
} Record;

int main(void) {
    FILE *fp = fopen("records.bin", "rb");
    if (fp == NULL) {
        perror("fopen failed");
        return 1;
    }

    Record rec;
    size_t count = 0;

    while (1) {
        size_t n = fread(&rec, sizeof(Record), 1, fp);
        if (n == 1) {
            printf("Record %zu: id=%d, value=%f, name=%s\n",
                   count, rec.id, rec.value, rec.name);
            count++;
        } else {
            // 読めなかったのでEOFかエラーを判定
            if (ferror(fp)) {
                perror("Error while reading record");
                fclose(fp);
                return 1;
            }
            if (feof(fp)) {
                printf("EOF after %zu records.\n", count);
                break;
            }
            // 想定外の状況
            fprintf(stderr, "Unexpected read condition.\n");
            fclose(fp);
            return 1;
        }
    }

    fclose(fp);
    return 0;
}

想定される実行結果の一例です。

実行結果
Record 0: id=1, value=10.000000, name=foo
Record 1: id=2, value=20.500000, name=bar
EOF after 2 records.

テキスト・バイナリのどちらでも「戻り値→ferror→feof」の順にチェックする基本スタイルは同じであることが分かります。

安全なファイル入出力のためのチェックリスト

安全なファイル入出力を行うために、実装時に意識しておきたいポイントを文章で整理します。

まずファイルを開く段階で必ずfopenの戻り値をチェックし、NULLであれば直ちにエラー処理を行うことが前提になります。

次に、読み取りや書き込みを行う関数を呼んだ直後には、戻り値を必ず確認し、想定したサイズや形式のデータが処理できたかどうかを検証します。

戻り値が想定より少ない場合やEOFに等しい場合は、まずferrorを呼び出して、そのストリームでI/Oエラーが発生していないかを調べます。

エラーが検出された場合はerrnoperrorなどを併用して原因をログに残し、必要であれば処理を中断します。

エラーがなければfeofの結果を確認し、正常なEOF到達かどうかを判断します。

また、一度エラーやEOFが発生したストリームを再利用する場合にはclearerrで状態をリセットしてから次の操作を行うようにします。

最後に、処理が完了したらfcloseで必ずファイルを閉じ、リソースリークを防ぐことも重要です。

これらを意識的に適用することで、「たまたま動いている」ではなく「どの状況でも安定して動作する」ファイル処理に近づけることができます。

まとめ

ferrorとfeofは、C言語の標準I/Oにおける「ストリーム状態の監視ツール」です。

読み書き関数の戻り値だけでは区別できない「EOFかエラーか」を判断するうえで欠かせません。

誤用されがちなwhile (!feof(fp))パターンを避け、戻り値→ferror→feofという順番でチェックするのが基本です。

また、必要に応じてerrnoclearerrも組み合わせることで、より堅牢なファイル入出力が実現できます。

これらのポイントを押さえておけば、実用的なプログラムで起こりがちな「途中で読み書きが止まっていたのに気づけない」問題を防ぐことができます。

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

URLをコピーしました!