たのしい工学

プログラミングを学んで、モノをつくりたいひと、効率的に仕事をしたい人のための硬派なブログになりました

【コンピュータサイエンスの独学】テキストファイルの読み書き

   

テキスト・ファイルの書き込み

ファイルへの書き込みはストリームを介す事は説明しました。また標準出力もストリームを介している事も同じく説明しました。つまり標準出力へ出力する手順でファイルへも書き込めます。標準出力は一般には画面ですしファイルは外部記憶装置です。ストリームを介すので異なる装置でも手順は変わりません。
単純な文字列をテキスト形式でファイルに書き込むには次の標準関数を使用すると便利です。


ヘッダー・ファイル   stdio.h
プロトタイプ  int fputs( const char * s, FILE * stream );

stream には FILE 型へのポインターを指定します。
指定したストリームに対し s の指し示す文字列を出力します。文字列はNULL文字が出るまで出力され、NULL文字自身は出力されません。
似た関数に puts がありましたね。標準出力専用で文字列出力の最後に自動的に改行されましたが fputs 関数ではその様な処理はありません。
戻り値は成功すれば 0 以上の値、エラーが発生すれば EOF です。

書き込み方法は分かりましたのでストリームを開く方法を選択しなければいけません。ここは書き込み専用の "w" を指定しましょう。
以上でテキスト・ファイルの書き込みが可能です。

テキスト・ファイルの読み込み

今度は読み込みです。既に使用している関数 fgets で可能です。標準入力もファイルも同じ様に扱えるのでストリームの有り難さが分かるのではないでしょうか。
念の為 fgets について説明します。プロトタイプは

char * fgets( char * s, int n, FILE * stream );
です。 s がストリームから入力したデータを格納する先頭アドレス、 stream は FILE 型へのポインター、 n は ( 読み込むバイト数 - 1 ) を指定します。実際に読む込むデータは n - 1 に達するか改行コードが現れるか、或いはファイルの終端に達するかするまで続き、改行コードもデータとして格納されます。処理が成功すれば s のアドレスが、失敗すればヌル・ポインターが戻ってきます。
一度開いたファイルは閉じるまでは何度でもアクセス出来ます。しかし書き込んだファイルを読み込むには今のままでは読み込めません。先ほど書き込み専用の "w" を指定する、としたからです。そこで更新新規作成の "w+" を指定します。
これでもまだ不足です。ファイルを読み書きすると現在の読み書き位置が更新されます。書き込むと書き込んだデータの分だけその位置が進みます。書き込んだ後に読み込もうと思っても読み込み位置はデータの最後になっています。そこから更に読み込み処理してもファイルは終わっているという合図が返ってくるだけです。これを End Of File と呼び、所謂 EOF です。この仕組みの利点は連続処理が簡単になる事です。連続して書き続ければファイルの最後に必ず追加されます。連続して読み続ければデータを順に読み出せます。
ですが書き込んだ後、直ぐデータを読み込もうと思っても以上の理由から上手くいきません。そこで読み書きする位置を指定する標準関数が用意されています。


ヘッダー・ファイル   stdio.h
プロトタイプ  int fseek( FILE * stream, long offset, int whence );

whence の固定値 
マクロ名    値   意味
SEEK_SET    0   ファイル先頭
SEEK_CUR    1   現在の位置
SEEK_END    2   ファイル終端 ( EOF )
 

プログラムの例


  /* 42_01.c テキスト・ファイルの書き込み */
#include 
#include "c_prac_base2.h"

/* マクロ定義 */
#define MacS0   "abc文字列      左はタブです。\\これは円記号です。\n"
#define MacS1   "     先頭五文字は半角空白です。\n"
#define MacS2   "     先頭と末尾に五文字ずつ半角空白。     \n"
#define MacS3   "次行は改行だけです。\n"
#define MacS4   "\n"
#define MacS5   "改行だけの後です。\n"
#define MacFileName     "textfile"
#define MacOpenMode     "w+"
#define MacBufferSize   128

/* プロトタイプ宣言 */
int text_write( FILE * fp, const char * s[], size_t sz );
int text_read( FILE * fp, char * buf, int buf_sz );

int main( void )
{
              char   buf[ MacBufferSize + 4 ];
        const char * s[] = { MacS0, MacS1, MacS2, MacS3, MacS4, MacS5 };
        const size_t sz  = sizeof( s ) / sizeof( s[ 0 ] );
              FILE * fp  = fopen( MacFileName, MacOpenMode );

        if ( fp == NULL )
        {
                fputs( "fopen error\n", stderr );
                goto err_exit;
        }

        /* 書き込み */
        if ( text_write( fp, s, sz ) == MacFalse )
        {
                goto err_exit;
        }

        /* ファイルの読み書き位置を先頭へ移動 */
        if ( fseek( fp, 0, SEEK_SET ) != 0 )
        {
                fputs( "fseek error\n", stderr );
                goto err_exit;
        }

        /* 読み込みと標準出力へ出力 */
        if ( text_read( fp, buf, MacBufferSize ) == MacFalse )
        {
                goto err_exit;
        }

        /* 後始末 */
        if ( fclose( fp ) == 0 ) /* プログラム終了前に必ず閉じる */
        {
                return ( MacReturnOk );
        }
        fputs( "fclose error\n", stderr );
        return ( MacReturnErr );

        /* エラー発生、ファイルを閉じて終了 */
err_exit :
        if ( fp != NULL )
        {
                fclose( fp );   /* プログラム終了前に必ず閉じる */
        }
        return ( MacReturnErr );
}

  

まずは main 関数までです。
MacS で始まるマクロ名はテキスト・ファイルに書き込む内容です。必ず \n で終わっています。テキスト・ファイルは行で管理し、行の最後は \n で終わります。 fputs はヌル文字が出現するまでストリームに対して指定内容をそのまま書き込みます。 puts 関数の様に改行処理はしません。ですからデータには意識して \n を付けます。
MacFileName は読み書きするストリーム名です。よってこれが作成されるテキスト・ファイル名になります。
MacOpenMode はストリームを開く形態の指示です。先に説明した通り更新新規作成です。
MacBufferSize はテキスト・ファイルから読み込んだデータを格納する領域の大きさです。

プロトタイプ宣言はその関数名が示す通りの動作です。

main 関数先頭での変数宣言にて

FILE * fp = fopen( MacFileName, MacOpenMode );
としています。ファイルを開き、その戻り値である FILE 型へのポインターを、 FILE 型へのポインター変数 fp に代入初期化しています。
fopen 関数が必ず成功するとは限らないのできちんと戻り値を調べます。その後の if ( fp == NULL ) がそれです。
久しぶりに登場の goto 文です。 main 関数の四箇所で使用しその飛び先は全て err_exit です。この err_exit の飛び先では、何等かのエラーが発生した時にファイルを閉じてその旨の値を main 関数の返し値としています。ただ fopen 失敗時もここで処理しますから fp がヌル・ポインターでない事を確認した後 fclose でファイルを閉じます。処理自体は単純ですね。

ファイルを開いた後はファイルを作成しますが関数側に全て処理させています。ファイルを書き込むには FILE 型へのポインターと書き込むデータに関する情報が必要です。それらを引数に指定します。
ファイルを順に書き込むとファイルの現在位置を示す値は常にファイルの最後になっています。ファイルにデータを書き込んだ後にそのファイルを読み込む場合、ファイルの現在位置を適正なところに移動しなければなりません。それを fseek 関数で処理しています。先に説明した通りにファイルの先頭に移動させています。

その後で書き込んだテキスト・ファイルの読み込みを行います。読み込みは書き込み同様関数側で行っています。読み込む際に必要になるのも FILE 型へのポインターです。テキスト・ファイルを行単位で処理するので、読み込んだ行データを格納するバッファーに関する情報も必要です。それらを引数にして関数を呼び出します。
最後はファイルを閉じてテキスト・ファイルの読み書きは終了です。


 /* 書き込み */
int text_write( FILE * fp, const char * s[], size_t sz )
{
        size_t i;
        int    ret;

        for ( i = 0; i <sz; i++ )
        {
                ret = fputs( s[ i ], fp );
                if ( !( ret >= 0 ) )
                {
                        fputs( "fputs error\n", stderr );
                        return ( MacFalse );
                }
        }
        return ( MacTrue );
}
 

本関数の第一引数が FILE 型へのポインター、第二引数は書き込むデータの配列、第三引数がデータの配列の最大要素数です。

ret = fputs( s[ i ], fp );
この文でファイルの書き込みを行っています。書き込むデータは text_write 関数の第二引数の配列値で、それは文字列リテラルの先頭アドレスです。
ファイルを "w+" の更新新規作成で開いた直後、ファイルの読み書き開始位置はそのファイルの先頭を指しています。データを書き込むとファイルの読み書き開始位置はそのデータの分だけ進みます。ファイルの最後を常に示すので連続して書き込んでもデータは最後に追加される形になります。便利ですね。
fputs の戻り値検査をご覧ください。

if ( !( ret >= 0 ) )
fputs の戻り値は成功すれば 0 以上の整数、失敗すれば EOF です。では何故 EOF で判断しないか、です。成功した時の値が「 EOF ではない値」であれば EOF か否かで判断すべきです。しかし EOF は単一の値で負数です。その他の負数の場合はどうするのか、といった条件判断するには不明確な領域があります。それを排除するには成功した時の否定を用いるのが良いでしょう。成功していない、即ち失敗です。これなら条件が明確になるのでこの様な記述をしてみました。勿論 if ( ret == EOF ) も正しい記述です。


 /* 読み込み */
int text_read( FILE * fp, char * buf, int buf_sz )
{
        int    ret = MacTrue;
        char * st;

        for ( ; ; )     /* 無限ループ */
        { /* 悪例 */
                st = fgets( buf, buf_sz, fp );  /* 読み込み */
                /* ファイル終端なら無限ループ脱出 */
                if ( feof( fp ) != 0 )
                {
                        break;
                }
                /* エラーならその旨を出力の後無限ループ脱出 */
                if ( st == NULL )
                {
                        fputs( "fgets error\n", stderr );
                        ret = MacFalse;
                        break;
                }
                fputs( buf, stdout );   /* 標準出力へ出力 */
        }
        return ( ret );
}
 

本関数の第一引数は FILE 型へのポインターです。第二引数は一行分のデータを格納するバッファーの先頭アドレスです。第三引数はバッファーの大きさです。

st = fgets( buf, buf_sz, fp );
ここでファイルから一行読み込み、読み込んだデータを指定アドレスに対し最大で指定された容量を書き込みます。読み込んだデータ末尾に文字列終端を示すヌル文字を追加するので、読み込む最大容量は指定した大きさより 1 少なくなります。
この関数の戻り値は、成功すれば指定したバッファーの先頭アドレスが、失敗或いはファイルの最後に達した場合はヌル・ポインターです。読み込み失敗なのか或いはファイルの最後に到達したのかを切り分ける必要があります。そこで一旦 st にポインター値を格納します。
fgets 関数の戻り値を条件判断の材料にする場合、判断する順番は大切です。最初に fgets 関数の戻り値を判断してしまっては条件の切り分けが出来ません。
ここではまずファイルの最後かを判断し、その後でエラーか否かを判断しています。今回用いたこの手法はしてはいけません。説明の為に敢えて行ったのです。理由は後述します。
まずは feof 関数を用いてファイルの終端であれば読み込みの無限ループを抜けます。まだファイルの途中であれば退避しておいた fgets 関数の戻り値を調べます。この条件を判断するのはファイルの終端ではない場合です。従って fgets 関数の戻り値がヌル・ポインターならそれはエラーの時だけになります。エラーならばそれに応じた処理をします。
ファイルの終端でもなくエラーでもない。即ち正常にデータを読み込めたので fputs 関数にて標準出力に今読み込んだデータを出力しています。ここでも fputs 関数を使用しています。 puts 関数と異なり改行処理は行われません。しかしデータ内には改行コード '\n' が含まれていますから大丈夫です。

 - コンピュータサイエンス