ファイルを1文字ずつ扱えるようになると、テキストのフィルタ処理や簡単なコピー、カウント、置換など幅広い用途に応用できます。
本記事ではC言語のfgetc
とfputc
に絞り、EOFの正しい扱いや戻り値の型、基本ループ、最低限のエラー処理まで、初心者でも安全に使える形で丁寧に解説します。
1文字入出力の基本
fgetcとfputcの役割
関数の概要
fgetc
はファイルから1文字を読み取り、fputc
はファイルへ1文字を書き込みます。
いずれも標準ヘッダstdio.h
に定義されています。
戻り値はいずれもint
型である点が重要です。
以下に要点をまとめます。
関数 | ヘッダ | プロトタイプ | 正常時の戻り値 | エラー時 |
---|---|---|---|---|
fgetc | #include <stdio.h> | int fgetc(FILE *stream); | 読んだ文字をunsigned char としてint に拡張した値 | EOF |
fputc | #include <stdio.h> | int fputc(int c, FILE *stream); | 書き込んだ文字と同じunsigned char 値をint で返す | EOF |
EOF
は多くの処理系で-1ですが、規格上は「負のint
の定数」であり、文字そのものではありません。
サンプルの最小形
読み取りと書き込みの最小例は次の通りです。
#include <stdio.h>
void read_one(FILE *fp) {
// 1文字読む。戻り値はint
int ch = fgetc(fp);
if (ch != EOF) {
// 読めたので標準出力へ出す
fputc(ch, stdout);
}
}
void write_A(FILE *fp) {
// 'A'を書き込む。戻り値もint
if (fputc('A', fp) == EOF) {
// 書き込みエラー
perror("fputc");
}
}
ファイルを開くと閉じる
fopenとfcloseの基本
入出力にはFILE*
が必要です。
ファイルはfopen
で開き、最後にfclose
で閉じます。
テキスト処理なら"r"
や"w"
、内容を厳密にコピーするなら"rb"
や"wb"
を使います。
#include <stdio.h>
int main(void) {
FILE *in = fopen("input.txt", "r"); // テキスト読み取り
FILE *out = fopen("output.txt", "w"); // テキスト書き込み
if (!in || !out) {
perror("fopen");
if (in) fclose(in);
if (out) fclose(out);
return 1;
}
// ... ここでfgetc/fputcを使った処理を行う ...
fclose(in);
fclose(out);
return 0;
}
Windowsで改行の変換を避けたい場合やバイナリを扱う場合は"rb"
と"wb"
を使います。
用途の例
典型的な使いどころ
1文字入出力は、次のような小さな「文字フィルタ」を作る際に向いています。
例えば、ファイルのコピー、行数や文字数のカウント、特定文字の置換、制御文字の可視化などです。
解析を段階的に行う教材としても適しており、ファイルI/Oの基礎理解に役立ちます。
fgetcの使い方とEOF
戻り値はintとEOF
なぜintで受けるのか
fgetc
の戻り値はint
です。
これは「すべてのunsigned char
値」と「EOF
」を区別するためです。
もしchar
で受けると、負の値を取りうる処理系ではEOF
と混同する危険があります。
int ch = fgetc(fp); // 正しい
if (ch == EOF) {
// ファイル終端かエラー
}
実際の文字として使うときは(unsigned char)ch
や(char)ch
に明示的に変換します。
読み取りループの基本形
教科書的なパターン
最も安全で簡潔なパターンは次の形です。
#include <stdio.h>
void echo(FILE *in, FILE *out) {
int ch;
while ((ch = fgetc(in)) != EOF) {
// chはint。文字として使うならキャストする
unsigned char c = (unsigned char)ch;
if (fputc(c, out) == EOF) {
perror("fputc");
break;
}
}
// ループを抜けたら終端かエラーかを判定可能
if (ferror(in)) {
perror("fgetc");
}
}
この形式なら、EOFと実際の文字値が確実に区別されます。
改行や空白も1文字として読む
空白は「データ」です
fgetc
はスペースやタブ、改行'\n'
も1文字として返します。
したがって「単語単位」ではなく「文字単位」で確実に処理したい場合に向いています。
次の例は、行数を数えます。
#include <stdio.h>
long count_lines(FILE *fp) {
long lines = 0;
int ch;
while ((ch = fgetc(fp)) != EOF) {
if (ch == '\n') {
lines++;
}
}
if (ferror(fp)) {
perror("fgetc");
return -1;
}
return lines;
}
読み取り失敗の簡単なチェック
feofとferrorの併用
ループ終了時にfeof
が真なら終端、ferror
が真ならエラーです。
初心者のうちは終端とエラーを分けてメッセージを出すだけでも十分役立ちます。
#include <stdio.h>
void read_all(FILE *fp) {
int ch;
while ((ch = fgetc(fp)) != EOF) {
// 読んだ文字をどこかに使う
(void)ch;
}
if (ferror(fp)) {
perror("read_all: fgetc");
} else if (feof(fp)) {
// 正常に終端まで到達
}
}
fputcの使い方
基本形と戻り値
書けたかどうかの判定
fputc
は、正常時に書き込んだ文字をint
で返し、失敗するとEOF
を返します。
戻り値のチェックが最低限のエラー検出になります。
#include <stdio.h>
int put_char(FILE *fp, int ch) {
if (fputc(ch, fp) == EOF) {
perror("fputc");
return -1;
}
return 0;
}
1文字を書き込む
単純な出力
整数int
で渡した値の下位8ビットが文字として書き込まれます。
#include <stdio.h>
void write_sample(FILE *fp) {
fputc('H', fp);
fputc('i', fp);
fputc('!', fp);
}
改行を書き込む
改行コードの注意
テキストモード("w")
では'\n'
を書けば、OSに応じた適切な改行に変換されます。
バイナリの厳密コピーが必要なら"wb"
を使い、改行の変換を避けてください。
#include <stdio.h>
void write_lines(FILE *fp) {
fputc('A', fp);
fputc('\n', fp); // テキストモードなら環境に合わせて変換
}
サンプルコードと注意点
1文字ずつコピーする
実用的な最小プログラム
次は、入力ファイルから出力ファイルへ1文字ずつ安全にコピーする完全なサンプルです。
バイナリでも正しく動くよう"rb"
と"wb"
を使用します。
#include <stdio.h>
#include <stdlib.h>
// 使い方: ./copy_char input.txt output.txt
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <src> <dst>\n", argv[0]);
return 2; // 使い方エラー
}
const char *src_path = argv[1];
const char *dst_path = argv[2];
// バイナリモードで開くことで、改行などの変換を避ける
FILE *in = fopen(src_path, "rb");
if (!in) {
perror("fopen(src)");
return 1;
}
FILE *out = fopen(dst_path, "wb");
if (!out) {
perror("fopen(dst)");
fclose(in);
return 1;
}
long long count = 0; // コピーしたバイト(文字)数
int ch;
// 典型的なfgetcループ
while ((ch = fgetc(in)) != EOF) {
if (fputc(ch, out) == EOF) {
perror("fputc");
fclose(in);
fclose(out);
return 1;
}
count++;
}
// 入力側でエラーがあったか確認
if (ferror(in)) {
perror("fgetc");
fclose(in);
fclose(out);
return 1;
}
// 後片付け
if (fclose(in) == EOF) {
perror("fclose(in)");
// 続行してoutも閉じる
}
if (fclose(out) == EOF) {
perror("fclose(out)");
return 1;
}
// 結果表示
printf("Copied %lld bytes from %s to %s\n",
count, src_path, dst_path);
return 0;
}
想定するサンプルの動作例を示します。
例えばinput.txt
が次の内容だとします。
Hello
C
上のプログラムを実行した結果は次のとおりです。
$ ./copy_char input.txt output.txt
Copied 7 bytes from input.txt to output.txt
エラー時の最低限の対応
fopenと入出力エラーの扱い
初心者向けには、関数の戻り値を必ず確認することが最重要です。
fopen
がNULL
ならperror
で理由を表示し、fgetc
/fputc
がEOF
を返したらferror
でエラーかどうかを追加確認します。
上のサンプルは最小限のパターンを含んでいます。
発展的にはclearerr
でエラー状態をクリアしたり、errno
を使って詳細な原因分岐を行うこともできます。
EOFは文字ではない
よくある誤解の回避
EOFは文字ではありません。
「ファイル終端」または「エラー」を示す特別な負のint
値です。
次のようにchar
で受けて比較すると動きが不定になります。
// よくある間違い(ダメな例)
char ch;
while ((ch = fgetc(fp)) != EOF) { // chはEOFを表現できない可能性あり
/* ... */
}
正しくはint
で受け、必要に応じて文字型へ変換します。
// 正しいパターン
int ch;
while ((ch = fgetc(fp)) != EOF) {
unsigned char c = (unsigned char)ch; // ここで実際の文字として扱う
/* ... */
}
文字はintで受け取る
ctypeとの連携も安全に
fgetcの戻り値は常にint
で受けるのが基本です。
標準ライブラリのctype.h
系関数isalpha
やisspace
などに渡すときは、(unsigned char)にキャストしてから渡すのが安全です。
これは負のchar
を未定義動作にしないためです。
#include <stdio.h>
#include <ctype.h>
int count_spaces(FILE *fp) {
int ch, spaces = 0;
while ((ch = fgetc(fp)) != EOF) {
if (isspace((unsigned char)ch)) { // unsigned charにする
spaces++;
}
}
if (ferror(fp)) {
perror("fgetc");
return -1;
}
return spaces;
}
まとめの要点として、fgetc
はint
で受けてEOF
と比較、文字として使う際にキャスト、fputc
は戻り値をEOF
と比較してエラー検出、という流れを覚えると安全です。
まとめ
1文字入出力はシンプルですが、戻り値の型とEOFの意味を正しく理解することが肝心です。
fgetc
はint
で受けてEOF
と比較し、必要なときだけ文字型へ変換します。
fputc
は戻り値のEOF
でエラー確認を行います。
ファイルはfopen
で開いたらfclose
で必ず閉じ、可能ならfeof
とferror
で終端かエラーかを区別してください。
まずは本記事のコピーサンプルを動かし、次に文字カウントや置換など小さなフィルタを作ってみると、文字単位のI/Oに自信がつきます。