ファイル処理

ファイルからの入力とファイルへの出力について, 例題を通じて理解しよう.

ファイル入出力の機能を実装すれば, データを保存し何度でも再利用できるようになり, プログラムの実用性が向上する.

教科書の該当範囲:第9章

本日の作業

タスク1:単独ファイル処理の例
Step 1. ファイルからの入力,基礎知識

データファイル data.txt から整数データを入力し そのまま端末画面へ表示するプログラム read.c を作成してみる:

$ cp  ~/tmpl.c  read.c	# テンプレートを元に作成...
$ vim  read.c
#include <stdio.h>

int main(void)
{
	FILE	*fp;	// ファイルポインタの宣言.どのファイルにアクセスするか指定するよ
	int	x;	// 入力データ
	int	s;	// 入力完了判断用

	fp = fopen("data.txt", "r");	// data.txt からの入力の準備
	if (fp == NULL) {		// fopen() 失敗の場合
		perror("fopen() 失敗");		// エラーメッセージの表示
		return (1);
	}

	while (1) {
		s = fscanf(fp, "%d", &x);	// ファイルから入力
		if (s == EOF) break;		// 読み尽くした場合
		
		printf("%d\n", x);		// 画面へ表示
	}
	fclose(fp);			// ファイルの片付け
	return (0);
}

新しいライブラリ関数やデータ型が出て来た...

なお,これらを使うために必要はヘッダファイルは stdio.h です.

これまでに使ってきた printf()scanf() と同様... というか実は,こいつらは fprintf()fscanf() と 外見は違うが中身は同じ物なんです. システム内部で,端末画面は出力ファイルの一種, キーボードは入力ファイルの一種, として統一的に取り扱われています.

コンパイル・実行:

$ cc  read.c  -o  read

$ ls
read	read.c
	# まだ入力データファイル data.txt が存在しないので...

$ ./read
fopen() 失敗: No such file or directory
	# perror() によりエラーメッセージが生成・表示された.

$ vim  data.txt	# 入力データファイルを作成
89	32	5
...
	# 複数の適当な整数値を空白や改行で区切って書き込んでおこう.

$ ls
data.txt	read	read.c

$ ./read
89
32
5
...
	# data.txt の内容が入力され,そのまま出力された.
Step 2. ファイルへの出力

次は逆に,キーボードから整数データを入力し, データファイルへ出力してみる:

$ cp  read.c  write.c	# read.c を元に改造...
$ vim  write.c
...

int main(void)
{
	...
	fp = fopen("data.txt", "w");	// data.txt への出力の準備
	...

	printf("整数データ(最後に Ctrl+D)> ");	// プロンプトの表示
	while (1) {
		s = scanf("%d", &x);		// キーボードから入力
		if (s == EOF) break;		// 読み尽くした場合
		
		fprintf(fp, "%d\n", x);		// ファイルへ出力
	}
	fclose(fp);			// ファイルの片付け
	return (0);
}

ここで,fprintf()printf() の仲間ね. 画面ではなくファイルへ出力する奴.

$ cc  write.c  -o  write

$ ./write
整数データ(最後に Ctrl+D)> 1  2  3 
[Ctrl]+[D]

$ ./read	# data.txt の内容を確認
1
2
3

# data.txt の内容が書き換えられた.
Step 3. ファイルへの追加出力

さらに,キーボードから整数データを入力し, データファイルへ追加出力してみる:

$ cp  write.c  append.c	# write.c を元に改造...
$ vim  append.c
...

int main(void)
{
	...
	fp = fopen("data.txt", "a");	// data.txt への追加出力の準備
	...
	while (1) {
		...
		fprintf(fp, "%d\n", x);		// ファイルへ出力
	}
	fclose(fp);			// ファイルの片付け
	return (0);
}
$ cc  append.c  -o  append

$ ./append
整数データ(最後に Ctrl+D)> 4  5  6

$ ./read	# data.txt の内容を確認
1
2
3
4
5
6

# data.txt の末尾に入力データが追加された.

タスク2:ファイル処理のありがちな失敗例
Step 1. クローズが不足

使ったファイルを片付けずに復数のファイルを使い続けるとどうなる? とりあえず,同じファイルをクローズせずに複数回オープンしてみる:

$ cp  ~/tmpl.c  fclose1.c
$ vim  fclose1.c
#include <stdio.h>

int main(void)
{
	FILE	*fp;
	int	n = 0;

	while (1) {
		fp = fopen("fclose1.c", "r");
		if (fp == NULL) break;
		n++;
		printf("%d回OK\n", n);
//		fclose(fp);	// わざとクローズしないでおく
	}
	return (0);
}
$ cc  fclose1.c  -o  fclose1
$ ./fclose1
1回OK
2回OK
3回OK
...
1021回OK

# えー,オープン回数に限界がありますね.

# 正しく,クローズすれば...

$ vim  fclose1.c

# fclose() を有効化し,再コンパイル...

$ cc  fclose1.c  -o  fclose1
$ ./fclose1
...
[Ctrl] + [C]	# 強制終了

# ...おー,無限にオープンできますね.

利用完了したファイルは必ずクローズすること!!

ぶっちゃけ,処理対象のファイルが少数なら同時にオープンのまま放置でも問題ないし, プログラム終了時には自動的にクローズされる.

しかし,大規模・長期間に運用されるシステムでは, 当初の小規模なテストでは大丈夫と油断していると, 実運用では突然死が頻発し大問題となったりする.

要するに... 「使い終わったら片付けなさーい💢」母より.

Step 2. クローズが余計

ファイルのクローズが多過ぎるとどうなる? まだオープンしていないものや, 既にクローズしたものや, オープンできなかったものをクローズしてみる:

$ cp  fclose1.c  fclose2.c	# fclose1.c を元に改造...
$ vim  fclose2.c
...
int main(void)
{
	FILE	*fp;

	fclose(fp);	// オープンせずにクローズ...
		// 多分,実行時エラー発生 → 強制終了
		// fp の初期値が不定(ゴミ)なので,結果も不定.
/*
*/

/*
	fp = fopen("notexist.txt", "r");	// 存在しないファイル...オープン失敗... fp = NULL
	fclose(fp);		// オープンできなかったのにクローズ...
		//  fclose(NULL) → 実行時エラー発生 → 強制終了
*/

/*
	fp = fopen("fclose2.c", "r");
	fclose(fp);
	fclose(fp);	// クローズしすぎ...
		// この単純な例ではエラーなしだが
		// 別の例ではエラーの可能性あり.
		// (クローズとクローズの間で他のファイル処理がある場合など)
*/

	return (0);
}
$ cc  fclose2.c  -o  fclose2

$ ./fclose2
Segmentation fault (コアダンプ)		 # 等,実行時エラーだらけ

...		# コメント /* 〜 */ 部分を有効化/無効化して色々と試してね

ファイルのオープンとクローズの実行は1対1に対応付けること!!


タスク3:復数ファイル処理の例

同時に N個のファイルを処理対象とする場合, ファイルポインタも N個だけ必要となる. 例として,ファイル data.txt から整数データを入力し, 別のファイル copy.txt へ出力してみる:

$ cp  read.c  copy.c
$ vim  copy.c
...

int main(void)
{
	FILE	*fp1 = NULL;	// 入力のファイルポインタ
	FILE	*fp2 = NULL;	// 出力のファイルポインタ
		// ↑ 初期値 NULL を未開封の標識としておく
	int	x;		// 入力データ
	int	s;		// 入力完了判断用
	int	err = 0;	// 終了状態

	fp1 = fopen("data.txt", "r");	// ファイル入力の準備
	if (fp1 == NULL) {		// fopen() 失敗の場合
		perror("入力の fopen() 失敗");	// エラーメッセージの表示
	//	return (1);	// fp1の方は,ここで終了でもOKですが...
		err = 1; goto END;	// これでなくてもOKですが...
	}

	fp2 = fopen("copy.txt", "w");	// ファイル出力の準備
	if (fp2 == NULL) {		// fopen() 失敗の場合
		perror("出力の fopen() 失敗");	// エラーメッセージの表示
	//	return (1);	// オープン済のfp1をクローズしてから終了すべき
		err = 2; goto END;
	}

	while (1) {
		s = fscanf(fp1, "%d", &x);	// ファイルから入力
		if (s == EOF) break;		// 読み尽くした場合

		fprintf(fp2, "%d\n", x);	// ファイルへ出力
	}
END:		// 正常終了も異常終了も一括処理してみた
	if (fp1 != NULL) fclose(fp1);	// オープン済の場合だけfp1をクローズ
	if (fp2 != NULL) fclose(fp2);	// オープン済の場合だけfp2をクローズ

	return (err);
}

エラー処理のコード(NULL 関連)がかなり目障りですが, これらは安全第一のためのお約束事です. 怠りなく記述すること.

$ cc  copy.c  -o  copy

$ ls  *.txt		# ファイル名を表示
data.txt

$ ./copy

$ ls  *.txt
copy.txt
data.txt

$ cat data.txt		# ファイル内容を表示
1
2
3
...

$ cat copy.txt		# ファイル内容を表示
1
2
3
...

# data.txt の内容が copy.txt にコピーされた

練習問題

(1) ファイル内のデータ構成を 1列 → 2列へ変換するプログラム num.c を作成せよ.

実行例:

$ cat  data.txt
89
32
5

$ ./num

$ cat  data2.txt
1	89
2	32
3	5

ヒント:copy.c を元にして,行番号のカウントと出力を追加するだけ. 列の区切りには TAB文字 "\t" を使うとよい.

(2) ファイル内のデータ構成を 2列 → 1列×2ファイルへ分割・変換するプログラム cut.c を作成せよ.

実行例:

$ cat  data2.txt
1	89
2	32
3	5

$ ./cut

$ cat  col1.txt
1
2
3

$ cat  col2.txt
89
32
5

ヒント:これも copy.c を元にして, 出力ファイルを2個に増やす.(ファイルは合計3個とする.) データを2個ずつ入力し,1個ずつ別々のファイルへ出力する.