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

この章の概要

この章の概要です。

fputs関数と fgets関数

前章では、fputs関数(⇒リファレンス)を使って、 "Hello, World" という文字列をファイルへ書き込むプログラムを試しました。

今度は、テキストファイルへ書き出した文字列を読み込んでみましょう。 出力が fputs関数ならば、入力は fgets関数(⇒リファレンス)で行えます。

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

#define FILE_NAME  "hello.txt"

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


	/* ファイルへ書き出す */
	fp = fopen( FILE_NAME, "w" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}
	fputs( "Hello, World\n", fp );
	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	/* ファイルから読み込む */
	fp = fopen( FILE_NAME, "r" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}
	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

まず、オープンモードに "w" を指定した fopen関数(⇒リファレンス)でファイルを開き、文字列を書き出しています。 fclose関数(⇒リファレンス)でファイルを閉じた後、今度はオープンモードを "r" に変えてオープンし直して、今度は読み込み操作を行っています。

ファイルからの読み込みに使った fgets関数は、今までの章の中で、標準入力からの読み込みに使っていた関数と同一のものです。 標準入力からの読み込みのときには、第3引数を stdin(⇒リファレンス)にしていましたが、 これを、fopen関数が返した FILEオブジェクトへのポインタに変えただけです。

fgets関数は、末尾の改行文字もそのまま受け取るので、受け取った文字列を puts関数(⇒リファレンス)で出力すると、更に改行文字が追加で付加されてしまいます。 一方、fputs関数の方は改行文字を付加しません。

関数 改行文字の扱い
gets 受け取らない
fgets 受け取る
puts 付加する
fputs 付加しない

一応確認しておきますが、gets関数(⇒リファレンス)は使ってはいけません第6章参照)。

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

先ほどのサンプルプログラムでは、読み込み用にオープンしたファイルを一旦クローズし、改めて書き込み用でオープンし直すという操作を行いました。
しかし、前章のオープンモードの一覧表にあるように、読み書き両用でオープンできるモードは存在します。 これを使ってはいけないのでしょうか?

まず、テキストファイルを読み書き両用でオープンするには、"r+"、"w+"、"a+" のいずれかが使えます。 このうち、"a+" は追記書き込みのためのモードであり、目的に合わないので候補から外します。 残りの "r+" と "w+" の違いは、以下の2点です。

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

ということで、"w+"モードを使って、先ほどのサンプルプログラムを書き換えてみます。

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

#define FILE_NAME  "hello.txt"

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


	/* ファイルを読み書き両用でオープン */
	fp = fopen( FILE_NAME, "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オブジェクトに含まれていますが、 勝手に書き換えてはいけませんし、直接、読み取って使用することもお勧めできません。

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

前の項のサンプルプログラムの問題は、fputs関数によってファイルポジションが移動した後、 その移動した後の位置を対象にして fgets関数を呼び出してしまっている点にあります。
fputs関数の呼び出しの後、ファイルポジションは書き込んだ最後の文字の直後( "Hello, World" の末尾の 'd' の直後)にあるので、 この位置を起点として fgets関数を呼び出しても、そこには何も無い訳です。

今回のサンプルプログラムの場合、fgets関数の呼び出し前に、ファイルポジションをファイルの先頭に戻してやる必要があります。 ファイルポジションをファイルの先頭に戻すには、rewind関数(⇒リファレンス)を使います。 rewind関数の引数には、FILEオブジェクトへのポインタを渡します。戻り値は存在しません。

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

#define FILE_NAME  "hello.txt"

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


	/* ファイルを読み書き両用でオープン */
	fp = fopen( FILE_NAME, "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

シーク

ファイルポジションは、読み書きを行うたびに移動していきます。 これは大体のところ、イメージ通りの自然な動作だと思います。

読み書きを行う以外にも、ファイルポジションを移動させる方法があります。 ファイルポジションを移動させることを一般に、シークと呼びます。
シークを行うための標準関数として、 前の項で登場した rewind関数(⇒リファレンス)がありますが、 これよりも自由度の高い fseek関数(⇒リファレンス)という関数もあります。

fseek関数は、次のように宣言されています。

int fseek(FILE* stream, long int offset, int origin);

第1引数は FILEオブジェクトへのポインタ、第2引数に移動量をバイト単位で指定し、第3引数は移動の基準となる原点位置を指定します。 戻り値は、成功すれば 0 を、失敗すると 0以外の値を返します。

この関数は、rewind関数よりは多くのことができるものの、制限も多くあります。 また、対象のファイルが、テキスト形式なのかバイナリ形式なのかによっても制限事項は異なっており、少々複雑です。 バイナリ形式の場合については、第42章で改めて説明するとして、ここではテキスト形式の場合に限った話をします。

テキスト形式の場合にできることは、以下の4つのいずれかです。

これ以外のこともできる可能性はありますが、C言語の規格上は未定義となります。

  1. 第3引数を SEEK_SET(⇒リファレンス) にし、第2引数に 0L を指定⇒ファイルの先頭へ移動
  2. 第3引数を SEEK_CUR(⇒リファレンス) にし、第2引数に 0L を指定⇒現在位置のまま
  3. 第3引数を SEEK_END(⇒リファレンス) にし、第2引数に 0L を指定⇒ファイルの末尾へ移動
  4. 第3引数を SEEK_SET にし、第2引数に ftell関数(⇒リファレンス)の戻り値を指定⇒ftell関数が返した位置へ移動

第3引数に指定する SEEK_SETSEEK_CURSEEK_END は、それぞれ stdio.h に定義されたマクロです。 それぞれ、「ファイルの先頭」「現在のファイルポジション」「ファイルの末尾」を意味しています。

また、ftell関数は、現在のファイルポジションを表す値を返す関数です。 返される値は、バイナリ形式の場合には、ファイルの先頭からのバイト数ですが、テキスト形式の場合には、どんな数値かは環境によって異なります。 そのため、テキスト形式のファイルに対する ftell関数が返した値は、先ほどの fseek関数の4番目の使い方以外には用いられません。

上記の4つの内容をよく観察すると、結局のところ、あまり自由に動き回れないことが分かります。 4番目の使い方がやや難しそうですが、これは要するに、現在の位置を ftell関数に問い合わせて、その戻り値を変数に保存しておけば、 その後、読み書きを行うなり、先頭や末尾へ移動するなりした後、再び元の位置に戻ってくることができるということです。


ところで、fseek関数や ftell関数の引数や戻り値を見ると分かるように、ファイルポジションを long int型で表現しています。 仮に 32bit環境であれば、この最大値は 2,147,483,647 です(LONG_MAXマクロ(⇒リファレンス)の置換結果)。 これは、ファイルサイズとして考えると、約2GB ということになります。 2GB と言うと、例えば DVD 1枚分のデータ量の半分以下ということですから、万が一、巨大なデータを扱うことがあるとすれば、少々心もとない大きさかも知れません。

より巨大なファイルを扱うには、fgetpos関数(⇒リファレンス)や fsetpos関数(⇒リファレンス)が利用できます。

int fgetpos(FILE* fp, fpos_t* pos);
int fsetpos(FILE* fp, const fpos_t* pos);

fpos_t型(⇒リファレンス)の大きさは通常、long int型よりも大きく定義されており、必要十分なサイズがあると考えられます。

fgetpos関数の役割は ftell関数と同じです。 fgetpos関数で取得したファイルポジションは、fsetpos関数に渡すために使用します。 次のサンプルは使用例です。

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

#define FILE_NAME  "test.txt"

void putFileLine(FILE* fp);

int main(void)
{
	FILE* fp;
	fpos_t pos;


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


	putFileLine( fp );     /* 1行目 */
	fgetpos( fp, &pos );   /* ファイルポジションを保存 */
	putFileLine( fp );     /* 2行目 */
	putFileLine( fp );     /* 3行目 */
	fsetpos( fp, &pos );   /* 保存しておいた位置へ復帰 */
	putFileLine( fp );     /* 2行目 */


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

	return 0;
}

/*
	ファイルから1行読み取って、標準出力へ出力。
	引数
		fp:		FILEオブジェクトへのポインタ。
*/
void putFileLine(FILE* fp)
{
	char buf[80];

	fgets( buf, sizeof(buf), fp );
	fputs( buf, stdout );
}

入力ファイル(test.txt)

1行目
2行目
3行目
4行目
5行目

実行結果(標準出力)

1行目
2行目
3行目
2行目

ファイルの終わりの検出

今度は、テキストファイルの内容をすべて読み込んで、標準出力に出力させることを考えます。

fgets関数(⇒リファレンス)で1行ずつの入力を繰り返していけば、いずれファイルの終わりに達するはずですが、 ファイルの終わりに達したかどうかをどう判断すればいいのかが問題になります。

fgets関数は、正常にデータを読み取ったときには、第1引数に指定したアドレスと同じものを返します。 一方、読み取れなかった場合には、NULL を返します。
これを利用すれば、ファイルの終わりが分かりそうではありますが、単純に NULL が返されたら終わりとはいきません。 何らかのエラーが発生したために NULL を返している可能性もあります。

しかし、戻り値が NULL かどうかを利用すること自体は正解で、これに加えて feof関数(⇒リファレンス)の助けを借ります。 feof関数は、ファイルポジションがファイルの終端に達していれば 0以外の値を返します。 したがって、

ということになります。 これを踏まえて、プログラムを作成してみます。

#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 );
	}

	/* 各行を読み取りながら、標準出力へ出力 */
	while( 1 ){
		if( fgets( buf, sizeof(buf), fp ) == NULL ){
			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行目

feof関数は、必ず fgets関数の呼び出しの後で呼ぶ必要があります
fgets関数のような読み取り系の関数は、ファイルポジションを進めてから読み込みを行っています。 そのため、今回のサンプルプログラムの場合であれば、"3行目" という文字列を読み込み終えた時点では、まだ feof関数は 0 を返します。
その後、もう1度 fgets関数を呼び出したときに、ファイルポジションを進めようとして、ファイルの末尾に達してしまい、NULL を返します。 こうなると、feof関数は 0以外を返すようになります。

なお、この辺りの事情は fgets関数以外の読み取り系関数(次章以降で登場します)を使う場合でも同様です。


練習問題

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

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

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


解答ページはこちら

参考リンク

更新履歴

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

'2015/8/29 flose関数の戻り値もチェックするようにした。

'2014/1/12 「シーク」の項のサンプルで、オープンするファイル名の指定を修正。

'2010/9/12 「FILE構造体」という表記を「ファイル構造体」に改めた。

'2010/5/15 エラー発生時には、exit関数で失敗終了させるようにサンプルプログラムを修正。

'2010/5/10 fgetpos関数、fsetpos関数についての説明を追加。

'2010/5/9 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ