C言語編 第40章 テキストファイルの読み書き

先頭へ戻る

この章の概要

この章の概要です。


読み書き両用のオープンモード

前章で、fputs関数を使って、"Hello, World" という文字列をファイルへ書き込むプログラムを試しました。また、fgets関数を使って、ファイルから文字列を読み取れることも確認しました。

ここでは、同じファイルに対して、書き込みと読み込みを両方とも行うプログラムを考えてみます。 つまり、"Hello, World" という文字列を書き込んだ後、続けざまに、そのファイルの内容を読み取ってみます。

読み書きの両方を行うためには、読み書き両用のオープンモードでファイルを開く必要があります。前章のオープンモードの一覧表にあるように、読み書き両用でファイルを開くことができるオープンモードは多数あります。書き出してみると、以下のものがあります。

まず "w" で開いて書き込みを行った後、一旦ファイルを閉じて、再度 "r" で開きなおすことでも実現できますが、厳密にいえば、ファイルを閉じて開きなおす隙間の時間ができますから、余計なトラブルを招く恐れがあります。例えば、隙間の時間のあいだに、ファイルが削除されたり、ほかのプログラムに書き換えられたりするかもしれません。

オープンモード 意味
r+ テキストファイルを読み書き両用に開く
w+ テキストファイルを読み書き両用に開く
a+ テキストファイルを読み書き両用で追加あるいは作成する
rb+ または r+b バイナリファイルを読み書き両用に開く
wb+ または w+b バイナリファイルを読み書き両用に開く
ab+ または a+b バイナリファイルを読み書き両用で追加あるいは作成する

fputs関数や fgets関数を使うのなら、行単位での操作ですから、テキストファイルとして開けるものを選択することになります。 そのため、"r+"、"w+"、"a+" のいずれかを使います。 このうち、"a+" は追記書き込みのためのモードであり、目的に合わないので候補から外します (追記書き込みについては、第41章で取り上げます)。

残りの "r+" と "w+" の違いは、以下の2点です。

"r+" の方は基本的に読み込み操作に主眼を置いており、一旦読み込みを行った後で、書き込み処理も行うことを想定しています。 "w+" の方は、書き込み操作に主眼を置いており、一旦書き込みを行った後で、読み込み処理も行うことを想定しています。
今回は、"Hello, World" という文字列を書き込んだ後で、それを読み込もうとしていますから、"w+" の方が適切ということになります。

"w+"モードを使って、プログラムを書いてみます。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE* fp;
    char buf[80];


    /* ファイルを読み書き両用でオープン */
    fp = fopen( "hello.txt", "w+" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }


    /* ファイルへ書き出す */
    fputs( "Hello, World\n", fp );

    /* ファイルから読み込む */
    fgets( buf, sizeof(buf), fp );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    /* 読み込んだ文字列を、標準出力へ出力 */
    puts( buf );

    return 0;
}

ところが、このプログラムではうまく動作しません。 ここで問題なのは、fputs関数を呼び出した直後に、そのまま fgets関数を呼び出してしまっている点にあります。 何が問題なのか、そしてどう解決するかは、次の項で説明します。

ファイルポジション

前の項からの続きです。

fputs関数にせよ、fgets関数にせよ、読み書きの対象がファイル内のどの部分(どの位置)なのかという情報が必要です。 この情報は、ファイルポジションカレントポジションなどと呼ばれており、 FILEオブジェクトの中で管理されています。 ファイルに対する読み書きを行う標準ライブラリ関数は、ファイルポジションの位置に対して処理を行います。

FILEオブジェクトの内容は実装依存になっており、直接中身を使うべきではありません。

ファイルポジションは、読み書きを行うたびに移動します。 例えば、fputs関数による書き込みでは、最後に書き込まれた文字の直後の位置が、新しいファイルポジションになります。 ですから、再度 fputs関数で書き込みを行うと、その続きの位置へ書き込まれます。

前の項のサンプルプログラムの問題は、fputs関数によってファイルポジションが移動した後、 その位置を対象にして fgets関数を呼び出してしまっている点にあります。 fputs関数の呼び出しの後、ファイルポジションは書き込んだ最後の文字の直後にあるので、 この位置に対して fgets関数を呼び出しても、そこには有効なデータが何も無い訳です。

そこで、fgets関数を呼び出す前に、ファイルポジションをファイルの先頭に戻してやる必要があります。この操作には、rewind関数を使います(「rewind」とは「巻き戻す」という意味です)。rewind関数は、<stdio.h> に以下のように宣言されています。

void rewind(FILE* stream);

rewind関数の引数には、FILEオブジェクトへのポインタを渡します。戻り値はありません。

それでは、先ほどのサンプルプログラムに rewind関数を追加してみましょう。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE* fp;
    char buf[80];


    /* ファイルを読み書き両用でオープン */
    fp = fopen( "hello.txt", "w+" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }


    /* ファイルへ書き出す */
    fputs( "Hello, World\n", fp );

    /* ファイルポジションを先頭に戻す */
    rewind( fp );

    /* ファイルから読み込む */
    fgets( buf, sizeof(buf), fp );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    /* 読み込んだ文字列を、標準出力へ出力 */
    puts( buf );

    return 0;
}

実行結果(標準出力)

Hello, World

実行結果(hello.txt)

Hello, World

今度は正しく動作しました。

ファイルの終わりの検出

今度は、複数行が書き込まれているテキストファイルの内容をすべて読み込んで、標準出力に出力することを考えてみましょう。fgets関数で1行ずつの入力を繰り返していけば、いずれはファイルの終わりに到達するはずですが、それをどう判断すればいいのかが問題になります。

fgets関数は、正常にデータを読み取ったときには、第1引数に指定したポインタと同じものを返します。一方、読み取れなかった場合には、ヌルポインタを返します。
そのため、fgets関数がヌルポインタを返したらファイルの終わりに到達したと言えるようにも思えますが、そうとは限りません。何らかのエラーが発生してしまったときにも、ヌルポインタを返すことがあるためです。

よくあるサンプルプログラムでは、「単に fgets関数がヌルポインタを返したらプログラム終了」という感覚で書かれていて、ファイルの終わりに到達したのか、エラーが発生したのかを区別していないことがあります。それで構わないのなら良いのですが、本来、ファイルの終わりに到達することは正常な動作であり、エラーが発生することは異常な動作のはずですから、区別を付けるべきです。

ファイルの終わりに達したかどうかを判定する正しい方法は、feof関数を使うことです。feof関数は、<stdio.h> に以下のように宣言されています。

int feof(FILE* stream);

feof関数の引数には、FILEオブジェクトへのポインタを渡します。 ファイルポジションが、ファイルの終わりに達していたら 0以外の値を返し、達していなければ 0 を返します。

手順としては、まず fgets関数がヌルポインタを返してきたことを確認した後、feof関数を呼び出します。feof関数が 0以外の値を返したのならファイルの終わりに達したのであり、0 を返したのならエラーが発生しています。

ferror関数という標準ライブラリ関数があり、これでもチェックできそうに思えますが、その保証はありません。ferror関数は、FILEオブジェクトの中にあるエラー表示子というフラグのようなものを参照して、エラー発生の有無を返します。fgetc関数などのいくつかの関数がエラー発生時にエラー表示子をセットすることになっていますが、fgets関数にはそのような規定がありません。

これを踏まえて、プログラムを作成してみます。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE* fp;
    char buf[80];


    /* 読み取り用にテキストファイルをオープン */
    fp = fopen( "test.txt", "r" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    /* 各行を読み取りながら、標準出力へ出力 */
    for( ;; ){
        
        /* fgets関数を呼び出して、1行分読み取る。
           NULL が返されたら、ファイルの終わりかエラー発生。
        */
        if( fgets( buf, sizeof(buf), fp ) == NULL ){
            
            /* feof関数を呼び出して、ファイルの終わりかどうか問い合わせる。
               戻り値が 0以外ならファイルの終わり、そうでなければエラー発生。
            */
            if( feof( fp ) ){
                break;
            }
            else{
                fputs( "エラーが発生しました。\n", stderr );
                exit( EXIT_FAILURE );
            }
        }

        fputs( buf, stdout );
    }

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

入力ファイル(test.txt)

1行目
2行目
3行目

実行結果(標準出力)

1行目
2行目
3行目

あらかじめ、test.txt を準備してから実行してください。

ところで、fgets関数でエラーが発生したとき、fclose関数を呼ばずにプログラムが exit関数によって終了させられてしまいます。exit関数は、その時点で開かれたままのファイルを自動的に閉じてくれるので、これで問題ありません。fopen関数と fclose関数が対応付いていないと気持ちが悪いと感じる人もいるかもしれませんが、その場合は、明示的に fclose関数を呼び出しても構いません。ただ、そういうプログラムは結局、fclose関数を呼び出す箇所があちこちに散らばったり、それを回避するために、無理にコードを整理した結果、かえって読みづらくなったりしがちです。

fputc関数と fgetc関数

次は、1文字ずつの読み書きをしてみます。 ここで使うのは、fputc関数fgetc関数です。

fputc関数と fgetc関数はそれぞれ、<stdio.h> に以下のように宣言されています。

int fputc(int c, FILE* stream);
int fgetc(FILE* stream);

fputc関数は、第1引数に書き出したい文字を指定し、第2引数に FILEオブジェクトへのポインタを指定します。正常に書き込めた場合は書き込んだ文字を返し、失敗した場合は EOF を返します。

fgetc関数は、引数に FILEオブジェクトへのポインタを指定します。 正常に読み取れた場合はその文字を返し、ファイルの終わりに達していたり、エラーが発生したりした場合は EOF を返します。

読み書き両用モード "w+" を使って、書き込みを行った後、その内容を読み取ってみることにします。 多少それらしくするため、今回は、書き込み部分と読み込み部分を関数化してみます。

#include <stdio.h>
#include <stdlib.h>

static int writeFile(FILE* fp, const char* text);
static int readFileAndPuts(FILE* fp);

int main(void)
{
    FILE* fp;

    fp = fopen( "hello.txt", "w+" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    if( writeFile( fp, "Hello, World" ) == 0 ){
        fputs( "書き込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    if( readFileAndPuts( fp ) == 0 ){
        fputs( "読み込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

/*
    ファイルに文字列を書き込む。
    引数:
        fp:   書き込み先のストリーム
        text: 書き込む文字列
    戻り値:
        成功したら 0以外、失敗したら 0
*/
static int writeFile(FILE* fp, const char* text)
{
    const char* p;

    /* 1文字ずつ書き込む */
    for( p = text; *p != '\0'; ++p ){
        fputc( *p, fp );
    }

    /* エラーの有無を返す */
    return !ferror( fp );
}

/*
    ファイルの内容を読み取って、標準出力へ出力する
    引数:
        fp:   読み込み先のストリーム
    戻り値:
        成功したら 0以外、失敗したら 0
*/
static int readFileAndPuts(FILE* fp)
{
    int c;
    int ret = 0;

    /* ファイルポジションを先頭に巻き戻す */
    rewind( fp );

    for( ;; ){

        /* fgetc関数を呼び出して、1文字読み取る。
           EOF が返されたら、ファイルの終わりかエラー発生。
        */
        c = fgetc( fp );
        if( c == EOF ){

            /* feof関数を呼び出して、ファイルの終わりかどうかを、
               ferror関数を呼び出して、エラー発生かどうかを問い合わせる。
               どちらでもない可能性もあり得るかも知れない。
            */
            if( feof(fp) ){
                ret = 1;
                break;
            }
            else if( ferror(fp) ){
                ret = 0;
                break;
            }
            else {
                /* 何もしない */			
            }
        }
        printf( "%c", (char)c );
    }

    printf( "\n" );
    return ret;
}

実行結果(標準出力)

Hello, World

実行結果(hello.txt)

Hello, World

今回は、書き込みに関してもエラーチェックを行っています。 実のところ、書き込み時にエラーチェックを行っているプログラムはあまり多くないのですが、 もちろんエラーが起こり得ない訳ではありません。 特に、ファイルを閉じるに引き続き使用する場合は、書き込み後にきちんとチェックしておくべきでしょう。

fputc関数は失敗すると EOF を返すので、エラーチェックにはこれを使ってもいいのですが、 ここでは ferror関数を使いました。 fputc関数のエラーを戻り値で調べると、呼び出し回数によっては膨大な数の条件判定を行うことになるため、 ひととおりの書き込み作業を終えてから、1度の確認で済ませるためです。

ferror関数は、<stdio.h> に以下のように宣言されています。

int ferror(FILE* stream);

ferror関数は、引数に FILEオブジェクトへのポインタを指定します。 戻り値は、エラーが発生していたら 0以外、発生していなければ 0 です。 厄介なことに、ferror関数がファイルに関するエラーを確実にすべて報告してくれる訳ではありません。 fgets関数のときに使わなかったのはそのためです。

読み込みに関しては、fgetc関数を使っています。まず注意すべき点として、fgetc関数の戻り値を char型の変数で受け取らないようにして下さい。文字なのでついやってしまいがちですが、char型で受け取ると、EOF との比較が正しく行えない可能性があります。

EOF の置換結果は -1 なので、char型が符号なしの環境(第19章)では、0xff に変換されるかもしれません。しかし、0xff で表される有効な文字が存在する可能性があるため、有効な文字としての 0xff なのか、EOF としての 0xff なのかを区別できないことがあります。fgetc関数の戻り値を int型で扱っていれば、0x000000ff と -1 の比較になるため、確実に正しい比較が行えます。

fgetc関数も fgets関数と同様、戻り値だけでは、ファイルの終わりなのか、エラーが発生したのかを区別できません。手順としては、fgetc関数の戻り値が EOF になった後、feof関数と ferror関数を使って調べます。どういうケースかは不明ですが、feof関数と ferror関数がいずれも 0以外の値を返さない可能性も無くは無いので、そこも考慮しましょう。


fprintf関数と fscanf関数

これまでに1行単位の読み書き、1文字単位の読み書きが登場しました。 最後に、printf関数や scanf関数のような、変換指定を伴う文字列の扱いです。

これは簡単で、printf関数や、scanf関数に、ストリームの指定が加わった、fprintf関数fscanf関数を使うだけです。

fprintf関数と fscanf関数は、<stdio.h> に以下のように宣言されています。

int fprintf(FILE* stream, const char* format, ...);
int fscanf(FILE* stream, const char* format, ...);

「...」は初めて見るかもしれませんが、0個以上の仮引数がそこに並ぶことを意味しています。printf関数や sscanf関数の使い方を考えれば、その意味が分かると思います。この記法の詳しい話題は、第52章で取り上げます。

#include <stdio.h>
#include <stdlib.h>

struct Data_tag {
    char text[32];
    int  value;
};

static int writeFile(FILE* fp, const struct Data_tag* data, size_t size);
static int readFileAndPuts(FILE* fp);

int main(void)
{
    const struct Data_tag data[] = {
        "abc", 10,
        "defg", 20,
        "hijkl", 30
    };
    FILE* fp;

    fp = fopen( "test.txt", "w+" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    if( writeFile( fp, data, sizeof(data)/sizeof(data[0]) ) == 0 ){
        fputs( "書き込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    if( readFileAndPuts( fp ) == 0 ){
        fputs( "読み込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

static int writeFile(FILE* fp, const struct Data_tag* data, size_t size)
{
    size_t i;

    for( i = 0; i < size; ++i ){
        if( fprintf( fp, "%s %d\n", data[i].text, data[i].value ) < 0 ){
            return 0;
        }
    }

    return 1;
}

static int readFileAndPuts(FILE* fp)
{
    struct Data_tag data;

    rewind( fp );

    for( ;; ){
        if( fscanf( fp, "%31s %d", data.text, &data.value ) == EOF ){
            if( feof( fp ) ){
                return 1;
            }
            else {
               return 0;
            }
        }
        printf( "%s %d\n", data.text, data.value );
    }
}

実行結果(標準出力)

abc 10
defg 20
hijkl 30

実行結果(test.txt)

abc 10
defg 20
hijkl 30

fprintf関数、fscanf関数のどちらも、第1引数に FILEオブジェクトへのポインタの指定が加わっているだけであり、それ以外に違いはありません。fprintf関数であれば stdout を、fscanf関数であれば stdin を指定すれば、それぞれ printf関数や scanf関数と同じ結果になります。

printf系の関数は、成功した場合には出力された文字数を、失敗した場合には負数を返します。

scanf系の関数は、成功した場合には受け取ることができた値の個数を、失敗した場合には EOF を返します。
fscanf関数の場合、ファイルの終わりに達した場合にも、エラーが発生した場合にも EOF を返しますから、いつものように区別が必要です。fgets関数のときと同じく、ファイルの終わりを feof関数で確認します。feof関数が 0以外を返すならファイルの終わりに達しており、0 を返すならエラーが発生しています。


練習問題

問題① 標準入力から受け取った内容を、テキストファイルへ書き出すプログラムを作成して下さい。細かい仕様はお任せします。

問題② 標準入力から、テキストファイルのファイルパスを受け取り、そのファイルの内容を標準出力へ出力するプログラムを作成して下さい。

問題③ 問題②のプログラムを改造して、標準入力から指示された行の内容だけを、標準出力へ出力するようにして下さい。

問題④ 1文字ずつ読み書きする標準関数を使って、既存のテキストファイルのコピーを作り出すプログラムを作成して下さい。

問題⑤ テキストファイルのデータ中に含まれているタブ文字を、半角空白に置き換えた結果を、別のファイルへ出力するプログラムを作成して下さい。 ただし、タブの幅は自由に決めて下さい。


解答ページはこちら

参考リンク



更新履歴

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/3/16 全面的に文章を見直し、修正を行った。
章のタイトルを変更(テキストファイルの読み書き① -> テキストファイルの読み書き)
「シーク」の項を、第41章へ移動。
第41章から、「fputc関数と fgetc関数」、 「fprintf関数と fscanf関数」の項を移動してきた。
第41章から、練習問題②③を持ってきて、④⑤とした。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2018/2/21 文章中の表記を統一(bit、Byte -> ビット、バイト)

'2017/5/11 FILE周りの用語について、表現を見直した。

≪更に古い更新履歴を展開する≫



前の章へ(第39章 ファイルの利用)

次の章へ(第41章 ランダムアクセス)

C言語編のトップページへ

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー