WisdomSoft - for your serial experiences.

9.4 ファイルとストリーム

標準 C の関数を用いてディスク上の任意のファイルから文字データを読み書きする方法を紹介します。

9.4.1 データの読み込み

これまでは、メモリ(主記憶装置)に対してデータの読み書きを行ってきました。数値や文字配列などを変数として保存したり、参照することができました。変数を扱うという作業は、プログラムの最も基本的な項目であり複雑なものではありません。

しかし、メモリに保存した情報はプログラムを終了させると失われてしまいます。ユーザーが編集ソフトウェアで作成したデータやプログラムのログ、設定情報などをプログラム終了後にも保存しておきたい場合、補助記憶装置に記録しなければなりません。つまり、ハードディスクやフロッピーディスクなどです。こうした補助記憶装置にデータを保存するためにはファイルを操作しなければなりません。

ファイルとはデータを扱う際のひとまとまりの単位で、ディスク上にあるプログラムやデータを指します。ディスクにデータを保存するには、変数にではなくこのファイルに対してデータを保存しなければなりません。

高水準プログラミング言語では、どのような入出力デバイスであろうとも同じ手法で操作することができます。もし、ハードウェアごとに異なる操作を行わなければならないとすれば、プログラマには大きな負担となってしまいます。そのため、システムやプログラミング言語がこれを隠蔽するのです。アクセスする対象がディスクファイルでも、ネットワークでも、スクリーンだとしても、データの入出力という目的は同じであることから一貫した操作で行えるべきであるという考え方です。

これは、ストリームと呼ばれる抽象的なインターフェイスを操作することで実現されています。C言語プログラマは、論理的なインターフェイスである「ストリーム」を通してファイルにアクセスします。開発者はネットワークや他のデバイスと入出力を行う場合も、ストリームを用いるだけでプログラムできるため、コードの再利用や保守が容易になるのです。

そのため、ファイル操作を行うには、まずストリームとファイルを結びつける必要があります。そのためには FILE 構造体を使用します。FILE 構造体はストリームの状態に関する情報を格納し、ファイル関数で使います。FILE 構造体は stdio.h ヘッダファイルで定義されている typedef 名です。

FILE *fp;

このようにファイル構造体のポインタ変数を宣言します。この構造体がどのような宣言になっているかは開発者が気にする必要はありません。この構造体を利用するのは標準関数であり、開発者がメンバを呼び出すようなことはないからです。ファイルを開くのには fopen() 関数を使用します。標準ライブラリのファイル関係関数は起首が f で始まるという命名規則が設けられています。

fopen() 関数
FILE *fopen( const char *filename, const char *mode );

この関数は FILE 構造体へのポインタを返します。このように FILE 構造体のメモリを割り当て初期化するのは fopen() 関数の仕事であり、開発者が FILE 構造体を初期化するようなことはありません。私たちは FILE 構造体へのポインタを受け取り、これをファイルとして扱うだけでよいのです。

filename には、開きたいファイル名を指定します。ファイル名だけの場合は、デフォルトで対象となるカレントディレクトリのファイルを検索します。他のディレクトリを検索する場合は、そのシステム固有の方法でディレクトリを指定します。例えば Microsoft MS-DOS ならば C:\DirName\test.txt というように指定します。

mode には、ファイルを開く時に読み込み専用、書き込み専用、または両方などのアクセスモードを文字列で指定します。ASCII文字として文字変換(改行コードなど)するテキストファイル と、変換が行われないバイナリファイルの選択なども指定できます。バイナリファイルについては「9.7 バイナリファイル」で解説します。

表1 アクセスモード
モード 意味
"r" 読み込みようとしてファイルを開きます。ファイルが存在しない場合は NULL を返します。
"w" 書き込みモードでファイルを開きます。 ファイルが存在している場合はその内容を破壊し、存在しない場合は新しく作成します。
"a" 追加モードでファイルを開きます。 ファイルが存在しない場合は作成します。
"r+" 既存ファイルを対象に、読み込み/書き込みの両方のモードで開きます。ファイルが存在しない場合は NULL を返します。
"w+" ファイルを作成し、読み込み/書き込みの両方のモードで開きます。ファイルが存在している場合はその内容を破壊します。
"a+" 読み込み/書き込みモードの両方のモードで開きます。 ファイルが存在する場合は追加、存在しない場合は作成します。
"t" テキストモードで開きます。
"b" バイナリモードで開きます。

fopen()関数は、ファイルのオープンに失敗するとNULLを返します。

FILE 構造体を初期化するのは標準関数の役割なので、これを解放するのも標準関数の役割となります。必要な処理を終えてストリームが不要になった場合は最後に解放しなければなりません。ファイルを閉じるには fclose() 関数を使用します。

fclose() 関数
int fclose( FILE *stream );

stream には、閉じるストリームを指定します。これは fopen() 関数で取得した有効な FILE 構造体へのポインタでなければなりません。また、fclose() 関数にはバッファをフラッシュする役割もあります。

多くのファイルシステムでは、データの書き込みは遅延です。ディスクファイルとメモリの大きな違いのひとつにアクセス速度の格差があり、ディスクへのアクセスはメモリのアクセスよりもかなり遅いのです。そのため、データを書き込むときは、書き込みが終わるまでプログラムは処理を停止して待つ必要があるのです。これを効率的にするために、ファイルシステムは一定のデータ量が溜まるまではディスクに書き込まず、バッファと呼ばれる一時保存領域にデータを待機させるのです。バッファに残っているデータを書き込む作業を、バッファをフラッシュすると表現します。

因みに、fclose() 関数以外で、明示的にフラッシュを行いたい場合は fflush() 関数を使います。

fflush() 関数
int fflush( FILE *stream );

stream にはフラッシュする対象のストリームを指定します。この関数を呼び出せば、これまでのファイル操作が履行されることを保証されます。ストリームが入力用に開いている場合は、バッファの内容をクリアします。また NULL を渡せば出力用に開いているすべてのストリームをフラッシュします。

コード1
#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
	FILE *file;

	file = fopen(FILENAME , "r");

	if (file == NULL) {
		printf("%s: ファイルが開けませんでした\n" , FILENAME);
		return 0;
	}

	printf("%s: ファイルを開くことができました\n" , FILENAME);
	fclose(file);
	return 0;
}
実行結果
コード1 実行結果

コード1は、fopen() 関数を使ってファイルを開いています。特にファイル操作は行っていないのでファイルに影響を与えることはありません。データの入出力は何も行わず、ファイルが開けるかどうかを確認するだけです。ファイルを開くことができれば fclose() 関数を使ってストリームを閉じた後、プログラムを終了させます。このプログラムを実行するには test.txt という名前のファイルを、実行ファイルと同じディレクトリに配置してください。

ファイルに記録されている情報を利用するには、ファイルのデータを一度メモリに(すなわち変数に)読み込む必要があります。読み込んだファイルの内容を取得する方法はいくつかありますが、そのうちのひとつが fgetc() 関数です。fgetc() 関数はファイルのデータを文字として取得することができるため、テキストデータの読み込みに使います。

fgetc() 関数
int fgetc( FILE *stream );

この関数は、指定されたストリームの次の文字を int 型で返します。正確には unsigned char 型で読み込んだ文字をint型にキャストして返しています。ストリームは、このようなファイル操作関数を使うと、データの読み書きの対象となるファイル位置を自動的に移動します。

エラーが発生するか、ファイルの最後に達した場合 EOF を返します。EOF はマクロとして定義されており、通常 -1 の値です。EOF は End Of File の省略です。fgetc() 関数が返すのはint型ですが、下位 8 バイトはファイルから読み込まれた内容なので char 型にキャストして利用することができます。

コード2
#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
	FILE *file;

	file = fopen(FILENAME , "r");

	if (file == NULL) {
		printf("%s: ファイルが開けませんでした\n" , FILENAME);
		return 0;
	}

	while(1) {
		int iValue = fgetc(file);
		if (iValue == EOF) break;
		else printf("%c" , iValue);
	}
	printf("\n");

	fclose(file);
	return 0;
}
実行結果
コード2 実行結果

コード2は test.txt という名前のテキストファイルを読み込み、画面に表示します。テキストファイルはテキストエディタなどを使って適当なものを用意してください。while ステートメントのループで fgetc() 関数を使って文字を読み取り、EOF でなければ printf() 関数を使って1文字ずつ表示しています。

ただし、このようにファイル入出力関数が返す EOF を検出する方法は確実ではありません。この方法では、EOF がエラーを表しているのかファイルの終端まで達したのかを確認することができないのです。

そこで、指定されたストリームの位置がファイルの最後かを調べるために feof() 関数を使用します。この関数を使用すれば、確実にファイルの終端を求めることができます。

feof() 関数
int feof( FILE *stream );

stream には有効な FILE 構造体へのポインタを指定します。指定されたストリームの位置がEOFならば 0 以外を、そうでなければ 0 を返します。

また、エラーをチェックするには ferror() 関数を使います。

ferror() 関数
int ferror( FILE *stream );

stream には有効な FILE 構造体へのポインタを指定します。この関数は、ストリームでエラーが発生すると 0 以外の値を返し、そうでなければ 0 を返します。

コード3
#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
	FILE *file;

	file = fopen(FILENAME , "r");

	if (file == NULL) {
		printf("%s: ファイルが開けませんでした\n" , FILENAME);
		return 0;
	}

	while(1) {
		int iValue = fgetc(file);
		if (feof(file)) break;

		if (ferror(file)) {
			printf("ストリームでエラーが発生しました\n");
			break;
		}
		else printf("%c" , iValue);
	}
	printf("\n");

	fclose(file);
	return 0;
}

コード3コード2を改良したもので、ストリーム操作で発生したエラーを検出することができます。 特に問題なくファイルの読み込みを行えれば、実行結果はコード2と同じです。

9.4.2 データの書き込み

データをディスクに保存する場合はファイルへの書き込みを行う必要があります。文字データの保存は fgetc() の変わりに、文字を書き込む関数を使えばよいだけです。文字の書き込みは fputc() 関数を使います。

fputc() 関数
int fputc( int c, FILE *stream );

c には書き込む文字を、stream には書き込み対象となるストリームを指定します。関数が成功すれば書き込んだ文字を、失敗すれば EOF を返します。fputc() はストリームの現在位置に文字を書き込んで、次の位置へ進みます。

ただし、データを書き込む場合は fopen() 関数のアクセスモードが書き込みモードでなければなりません。通常は "w" を指定すればよいでしょう。

コード4
#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
	FILE *file;
	int iCount;
	const char *str = "Stand by Ready!";

	file = fopen(FILENAME , "w");

	if (file == NULL) {
		printf("%s: ファイルが開けませんでした\n" , FILENAME);
		return 0;
	}

	for(iCount = 0 ; str[iCount] ; iCount++) {
		fputc(str[iCount] , file);
	}

	printf("%s: 正常に書き込めました\n", FILENAME);

	fclose(file);
	return 0;
}
実行結果
コード4 実行結果

コード4は、コマンドライン引数で文字列を書き込むファイル名と、ファイルに書き込む文字列を指定します。for ループで文字配列の末尾が検出されるまで、順に fputc() 関数でファイルに文字を書き込んでいることがわかります。

9.4.3 文字列の入出力

これまでのファイルに対する入出力は文字単位で行われていました。確かに、バイト単位の入出力ができれば文字列の入出力が可能となりますが、ループを作成して EOF を検出するなど、面倒なプログラムを書かなければなりません。これは非生産的です。

テキストファイルを扱う場合は、文字列として入出力を行うことが求められます。文字列としての入出力を行うには fgets() 関数fputs() 関数を使います。これらは、fgetc() と fputc() 関数と基本的に同じですが、文字列単位で入出力が行える点で異なります。

fgets() 関数
char *fgets( char *string, int n, FILE *stream );
fputs() 関数
int fputs( const char *string, FILE *stream );

fgets() 関数では、string に文字列を格納するバッファへのポインタを、n には読み込み可能な最大文字数を指定します。fputs() であれば string に書き込む文字列へのポインタを指定します。stream には対象ストリームを指定します。

fgets() 関数は指定した入力ストリーム stream から文字列を読み込んで、string に格納します。文字の読み出しは、現在のストリーム位置から、最初の改行文字が現れるか、ストリームの終端に達するか、あるいは読み込んだ文字数が n -1 になるかのいずれかの条件が満たされるまで続きます。関数が成功すれば string を返し、エラーが発生するかストリームの終端にたどり着いた場合は NULL を返します。

fputs() 関数は、成功すれば負ではない値を、エラーが発生すれば EOF を返します。

コード5
#include <stdio.h>
#define READ_FILENAME "in.txt"
#define WRITE_FILENAME "out.txt"
#define BUFFER_SIZE 1024

int main(int argc , char *argv[]) {
	FILE *readFile, *writeFile;

	readFile = fopen(READ_FILENAME , "r");
	if (readFile == NULL) {
		printf("%s: ファイルが読み込みモードで開けませんでした\n" , READ_FILENAME);
		return 0;
	}

	writeFile = fopen(WRITE_FILENAME , "w");
	if (writeFile == NULL) {
		printf("%s: ファイルが書き込みモードで開けませんでした\n" , WRITE_FILENAME);
		return 0;
	}

	while(1) {
		char chLine[BUFFER_SIZE];
		if (fgets(chLine , BUFFER_SIZE , readFile) != NULL) 
			fputs(chLine , writeFile);
		else break;
	}

	printf("%s -> %s: 正常にファイルを複製できました。\n", READ_FILENAME, WRITE_FILENAME);
	fclose(readFile);
	fclose(writeFile);

	return 0;
}
実行結果
コード5 実行結果

コード5は、単純にテキストファイルをコピーするプログラムです。コマンドライン引数からコピー元のテキストファイルと、出力するファイル名を指定します。すると、プログラムはコピー元のテキストから文字列を読み込み、指定された名前のファイルを作成して文字列をコピーします。プログラムでは fgets() 関数を呼び出して文字列を取得し、fputs() 関数で書き込むという作業を繰り返しています。