C言語のきほん|1文字単位のファイル入出力

1文字ずつ追いかけると、ファイルの中身がもっとよく見えてくる。

これまでに、C言語では fgets や fputs を使って、文字列をまとめてファイルに読み書きする方法を見てきました。
文字列単位の入出力はとても便利ですが、ファイルの中身をもっと細かく扱いたい場面もあります。そんなときに役立つのが、1文字単位のファイル入出力です。

1文字単位で処理できるようになると、ファイルの中にどんな文字が入っているかを細かく調べたり、特定の文字だけを数えたり、必要な文字だけを書き出したりできるようになります。
たとえば、文章の中の改行の数を数える、空白を除いてコピーする、大文字だけ小文字に変える、といった処理は、1文字ずつ扱えるととても考えやすくなります。

C言語では、このような処理に主に fgetc 関数と fputc 関数を使います。
fgetc はファイルから1文字ずつ読み込む関数で、fputc はファイルへ1文字ずつ書き込む関数です。

もちろん、1文字ずつ処理する方法は、1行ずつまとめて処理する方法に比べると、効率の面では不利になることもあります。
それでも、文字を丁寧に見ながら処理したいときには、とてもわかりやすくて便利です。

ここでは、1文字単位のファイル入出力について、流れや考え方がしっかり見えるように、やさしく整理しながら学んでいきましょう。

1文字単位のファイル入出力とは

1文字単位のファイル入出力とは、ファイルの内容を1文字ずつ読み取ったり、1文字ずつ書き込んだりする方法です。

たとえば、次のような場面で使いやすいです。

したいこと1文字単位が向いている理由
特定の文字を探したい文字を1つずつ確認できる
改行や空白を数えたい文字ごとに条件分岐しやすい
特定の文字だけ除外したい文字単位で選別できる
ファイル内容をそのままコピーしたい1文字ずつ確実に読み書きできる

文字列単位の処理では、行ごとにまとめて扱うので全体の見通しはよいです。
一方で、1文字単位の処理では、ファイルの中身をより細かく追いかけられます。

つまり、ざっくり扱うなら文字列単位、細かく扱うなら1文字単位というふうに考えると整理しやすいです。

fgetc関数とは

fgetc は、ファイルから1文字ずつ読み込むための関数です。

関数宣言は次のとおりです。

#include <stdio.h>
int fgetc(FILE *stream);

fgetcの役割

fgetc は、stream が指す入力ストリームから次の1文字を読み込みます。
読み込んだ文字は unsigned char として扱われ、その値が int 型に変換されて返されます。

ここで少し大事なのは、返却値が char 型ではなく int 型であることです。

これは、普通の文字だけでなく、ファイル終端を表す EOF も区別して返す必要があるからです。
もし char 型で受け取ってしまうと、文字と EOF の区別があいまいになる場合があります。

そのため、fgetc の戻り値は、次のように int 型の変数で受け取るのが基本です。

int ch;
ch = fgetc(fp);

fgetcの返却値

fgetc の返却値を整理すると、次のようになります。

返却値意味
読み込んだ文字正常に1文字読めた
EOFファイル終端に達した、またはエラーが起きた

この EOF が、1文字ずつ読み込む処理ではとても重要です。
ファイルの終わりまで来たことを知らせてくれるので、これを使ってループを終了できます。

たとえば、よく使われる形は次のとおりです。

while ((ch = fgetc(fin)) != EOF) {
    /* 読み込んだ文字を処理する */
}

この書き方は、1文字単位のファイル処理でとても基本になります。

fputc関数とは

fputc は、ファイルへ1文字ずつ書き込むための関数です。

関数宣言は次のとおりです。

#include <stdio.h>
int fputc(int c, FILE *stream);

fputcの役割

fputc は、c で指定した文字を stream が指す出力先に書き込みます。
ここで渡す値は int 型ですが、実際には unsigned char 型に変換されて書き込まれます。

つまり、fgetc で読み込んだ1文字をそのまま fputc に渡せば、1文字単位でのコピーができます。

fputc(ch, fout);

この形はとてもシンプルで、読み込んだ文字をそのまま別のファイルへ書く基本形です。

fputcの返却値

fputc の返却値は、次のように理解しておくとよいです。

返却値意味
書き込んだ文字書き込み成功
EOF書き込み失敗

学習の初期段階では、まず「成功なら文字が返り、失敗なら EOF」と覚えておけば十分です。

fgetcとfputcの関係を整理しよう

この2つの関数は、読み込みと書き込みの対になる関数です。

関数役割単位
fgetcファイルから読む1文字
fputcファイルへ書く1文字

とてもシンプルですが、この組み合わせでできることは意外と多いです。

たとえば、

  • 読んだ文字をそのまま書く
  • 読んだ文字が空白なら無視する
  • 読んだ文字が改行なら数を増やす
  • 読んだ文字が英字なら変換して書く

といった処理が可能になります。

この図では、入力ファイルから fgetc で1文字を読み込み、その文字を変数 ch に入れ、必要なら処理してから fputc で出力ファイルへ書く流れを表しています。
文字列単位の処理では配列を使いましたが、1文字単位の処理では、今度は1文字分を入れる変数が中心になります。

また、EOF が返ったら処理を終える、という流れもとても大切です。
つまり、1文字単位の処理では、1文字読む → 必要なら処理する → 1文字書く → EOF まで繰り返すという形が基本になります。

サンプルプログラム

この例では、note.txt を読み込み、note_copy.txt に1文字ずつコピーします。

ファイル名:17_3_1.c

#include <stdio.h>

int main(void)
{
    /* 読み込み用ファイルを開く */
    FILE *fin = fopen("note.txt", "r");
    if (fin == NULL) {
        printf("読み込み用ファイルを開けませんでした。\n");
        return 1;
    }

    /* 書き込み用ファイルを開く */
    FILE *fout = fopen("note_copy.txt", "w");
    if (fout == NULL) {
        printf("書き込み用ファイルを開けませんでした。\n");
        fclose(fin);
        return 1;
    }

    /* 1文字を受け取る変数 */
    int ch;

    /* ファイルの終わりまで1文字ずつ読み込み、書き込む */
    while ((ch = fgetc(fin)) != EOF) {
        fputc(ch, fout);
    }

    /* ファイルを閉じる */
    fclose(fin);
    fclose(fout);

    printf("メモファイルのコピーが完了しました。\n");

    return 0;
}

実行前のファイル内容の例

たとえば、note.txt に次のような内容が入っているとします。

今日はC言語を勉強しました。
1文字ずつ読む仕組みを確認します。

このプログラムを実行すると、note_copy.txt が作られ、同じ内容がそのまま書き込まれます。

画面には次のように表示されます。

メモファイルのコピーが完了しました。

このプログラムの流れを丁寧に見てみよう

このプログラムの基本の流れは、これまでのファイル入出力と同じです。

  • 読み込み用ファイルを開く
  • 書き込み用ファイルを開く
  • 1文字ずつ読み込む
  • 1文字ずつ書き込む
  • 最後に両方のファイルを閉じる

ただし、今回は fgets や fputs ではなく、fgetc と fputc を使っているので、処理の単位が「1行」ではなく「1文字」になっています。

読み込み用ファイルを開く処理

最初に note.txt を読み込みモードで開いています。

FILE *fin = fopen("note.txt", "r");

r モードなので、存在するファイルを読むためのオープンです。
もし note.txt が存在しなければ、fopen は NULL を返します。

そこで、すぐにエラーチェックをしています。

if (fin == NULL) {
    printf("読み込み用ファイルを開けませんでした。\n");
    return 1;
}

この確認はとても大事です。
ファイル入出力では、開けることを前提にせず、開けなかった場合を必ず考える習慣をつけておくと安心です。

書き込み用ファイルを開く処理

続いて、コピー先ファイルを w モードで開いています。

FILE *fout = fopen("note_copy.txt", "w");

w モードなので、note_copy.txt が存在しない場合は新しく作られ、存在する場合は中身を空にして開きます。

ここでもエラーチェックが必要です。

if (fout == NULL) {
    printf("書き込み用ファイルを開けませんでした。\n");
    fclose(fin);
    return 1;
}

ここで注目したいのは、fout を開けなかったときに、すでに開いていた fin を閉じていることです。
途中で失敗したときも、先に確保したものをきちんと片づける、という書き方はとても大切です。

なぜ ch は int 型なのか

ここは1文字単位の入出力で特に大切なポイントです。

int ch;

文字を1つ扱うなら char 型でよさそうに見えますが、ここでは int 型を使います。
理由は、fgetc が文字だけでなく EOF も返すからです。

EOF はファイルの終わりを表す特別な値です。
これを普通の文字と区別するためには、char 型ではなく int 型で受け取る必要があります。

もし ch を char 型にしてしまうと、環境によっては EOF と通常の文字を正しく区別できない場合があります。
そのため、fgetc の戻り値は int 型で受けるというのは、かなり重要な基本ルールです。

while文の書き方がポイント

1文字ずつ読む処理の中心は、次の部分です。

while ((ch = fgetc(fin)) != EOF) {
    fputc(ch, fout);
}

この1行には、1文字単位のファイル処理の大事な考え方が詰まっています。

まず、fgetc(fin) で1文字読み込みます。
その値を ch に代入します。
そのあとで、その値が EOF でなければループの中へ進みます。

つまり、

  1. 1文字読む
  2. EOF かどうか確認する
  3. EOF でなければ書き込む
  4. これを繰り返す

という流れです。

この書き方は少しだけ慣れが必要ですが、とてもよく使われます。
読み込みと判定を同時に行えるので、無駄のない形です。

fputcで1文字ずつ書き込む処理

ループの中では、読み込んだ文字をそのままコピー先へ書いています。

fputc(ch, fout);

これはとてもシンプルです。
ch に入っている1文字を、fout が指すファイルへ出力しています。

今回はコピーなので、そのまま書いていますが、ここを少し変えると応用ができます。
たとえば、特定の文字を飛ばしたり、英字だけ変換して書いたりすることもできます。

1文字単位の処理が向いている場面

1文字単位の処理は、まとめて処理するより細かく制御できるのが強みです。
特に次のような場面では使いやすいです。

場面理由
文字の種類ごとに分けたい1文字ずつ判定しやすい
改行数を数えたい改行文字を直接見つけられる
空白や記号を除いてコピーしたい条件に合う文字だけ書き込める
特定の1文字を置き換えたい1文字ごとに変更できる

たとえば、スペースを除いてコピーしたいなら、次のような考え方ができます。

if (ch != ' ') {
    fputc(ch, fout);
}

このように、1文字単位の処理は加工と相性がよいです。

文字列単位の処理との違い

ここで、1文字単位と文字列単位の違いも整理しておきましょう。

比較項目1文字単位文字列単位
使う関数fgetc, fputcfgets, fputs
処理の細かさ細かいまとまりがある
向いている処理文字の分析、文字の置換、細かな条件分岐行単位の処理、文章のコピー
効率やや不利なことがある比較的よいことが多い

どちらがよいかは、目的次第です。
文章をそのまま扱うなら文字列単位が便利ですし、文字を細かく見たいなら1文字単位が向いています。

EOFの考え方をしっかり押さえよう

1文字単位の入出力で特に大切なのが EOF です。
EOF は、ファイルの終わりに達したことを表す特別な値です。

fgetc は、読み込める文字があればその文字を返し、もう読めるものがなければ EOF を返します。
この仕組みがあるからこそ、ファイルの終わりまで自然にループできます。

ここで大事なのは、EOF は文字そのものではなく、読み込み終了を知らせる印だということです。
だからこそ、普通の文字と区別できる int 型で扱う必要があります。

開いたファイルは必ず閉じよう

最後には、読み込み用と書き込み用の両方のファイルを閉じています。

fclose(fin);
fclose(fout);

これもファイル入出力ではとても大事です。
閉じ忘れると、書き込み内容が正しく保存されなかったり、ファイルの管理が中途半端になったりすることがあります。

特に書き込みを行ったファイルは、最後まできちんと閉じて、内容を確実に保存させることが大切です。

1文字単位のファイル入出力で身につけたいこと

今回の内容では、次のポイントを押さえておくと理解がしっかりします。

ポイント内容
fgetc は1文字読む戻り値は int 型
fputc は1文字書く1文字ずつ出力する
EOF で終了を判断するファイル終端の確認に使う
ch は int 型で受けるEOF と文字を区別するため
細かな文字処理に向いている条件分岐や文字の加工がしやすい

1文字単位の入出力は、一見地道に見えますが、ファイルの中身を細かく扱うための大事な基礎です。
文字列単位の処理とはまた違った視点でファイルを見られるようになるので、理解しておくと表現の幅が広がります。

小さなテキストファイルを使って、まずは1文字ずつ読み込んで画面に表示するところから試してみると、fgetc と fputc の動きがかなり実感しやすくなります。