ファイルや標準入出力から読み書きをすると、データが尽きたのか、エラーが起きたのかを正しく見分ける必要があります。
C言語ではferror
とfeof
がその判断の鍵です。
本記事では、EOFとエラーの区別を基礎から丁寧に解説し、各入力関数ごとの正しい判定パターン、ありがちな落とし穴、そしてclearerr
や標準ストリームでの実践的な使い方までをまとめます。
ferrorとfeofの基本
EOFとは何か
EOFはEnd Of File
(ファイル終端)を表す概念と、Cライブラリ関数が返すEOF
という整数マクロの2つを指すことがあります。
概念としてのEOFは「読み進めた結果、もはやデータが残っていない状態」を意味します。
一方、fgetc
などが返すEOF
は多くの実装で-1のint
値です。
バイト値0(ヌル文字)とは別物であり、混同しないことが大切です。
エラーとは何か
エラーはデバイス障害、権限不足、メディアの満杯、通信路の切断など、入出力操作そのものが失敗した状態です。
関数はしばしば特別な戻り値(例: EOF
やNULL
、負値、読み取り個数0)で失敗を示しますが、その原因が終端なのかエラーなのかは戻り値だけでは区別できないことがあります。
そこでferror
とfeof
を使います。
ストリームの状態フラグを理解する
CのFILE
ストリームは、エラーフラグとEOFフラグという2つの「状態ビット」を持ちます。
これらは「スティッキー(一度立つとそのまま)」で、clearerr
を呼ぶまで保持されます。
つまり、
- 終端に到達したら
feof(fp)
が非0になり、その後の読み取りは継続してEOF
を返すことがあります。 - 入出力エラーが起きたら
ferror(fp)
が非0になります。 - どちらか一方、あるいは稀に両方が同時に立つ可能性があります。
フラグが立っているだけではファイル位置は変わりません。
フラグをクリアしても、読み書き位置(ファイルオフセット)は元に戻らない点に注意してください。
戻り値とferror/feofの関係
多くの標準関数は「成功時の通常値」と「失敗や終端を示す特殊値」を持ちますが、特殊値が返ったときにfeof
とferror
を見て原因を判定します。
代表的な関数の戻り値は次のとおりです。
関数 | 成功時 | 終端/データなし | 真のエラー | 判別方法 |
---|---|---|---|---|
fgetc | 0〜255のint (実際はunsigned char をint に拡張) | EOF | EOF | feof(fp) かferror(fp) を見る |
fgets | バッファ先頭アドレス | NULL | NULL | feof(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
を置き、終了後にferror
とfeof
を調べます。
#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
で理由を判定します。
#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
は「入力枯渇や入出力エラー」を意味します。
終了時にfeof
かferror
で理由を判断します。
#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)
標準入力stdin
でscanf
を使う場合も同様で、rc == 0
は書式不一致、rc == EOF
は終端/エラーです。
freadで短読みを判定
fread
は要求より少ないバイト数しか返さない「短読み」が普通に発生します。
返った要素数が要求数より小さい場合、feof
かferror
を確認します。
#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
でストリームにエラーが残っていないか確認します。
バッファリングのため、エラーはfflush
やfclose
まで遅延して現れることがあります。
#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
書き込みエラーは気づきにくいため、重要な出力の直後はfflush
とferror
を確認するのが堅実です。
よくある間違いと回避策
while(!feof(fp))は使わない
典型的な誤りがwhile (!feof(fp)) { ... }
です。
feof(fp)
は「終端を超えて読もうとして初めて立つ」ため、この書き方ではfgets
などがNULL
を返した後にループ本体が実行されてしまい、最後の行を二重処理したり、ゴミを処理したりします。
誤りの例:
// NG: feofで回すのは間違い
while (!feof(fp)) {
if (fgets(buf, sizeof buf, fp) == NULL) {
// ここに来る時点で既に読めていない
// なのにループ本体が1回余分に回る
process(buf); // ゴミ
}
}
正しいループ構造
「入力関数の戻り値でループを回し、抜けたらferror
/feof
を調べる」のが鉄則です。
代表パターンをまとめます。
// 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’は別物
EOF
はint
の負値(多くは-1)で、'\0'
(ヌル文字)はchar
の0です。
バイナリデータには0バイトが普通に含まれますが、これは終端を意味しません。
常にint
で受け取ってEOF
と比較します。
#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フラグを両方クリアします。
終端に達してから同じストリームで処理を続けたい場合や、エラー後に再試行する場合に必要です。
#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が目安です。
以下は整数をカウントし、終了理由を判定する例です。
#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
は使えます。
パイプの相手が終了していたり、端末/ファイルへの書き込みに失敗した場合にエラーフラグが立ちます。
#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のストリームはfeof
とferror
というスティッキーな状態を持ち、戻り値だけでは原因を判断できない場面が多くあります。
正しいループ構造は、「入力関数の戻り値で回し、抜けたらfeof
/ferror
で理由を切り分ける」ことです。
while(!feof(fp))は避け、clearerr
で必要に応じて状態をリセットし、標準ストリームでもferror
を活用して遅延エラーを検出してください。
これらを徹底することで、初心者の方でも予期せぬ不具合を大幅に減らし、安心してファイル操作や標準入出力を扱えるようになります。