07 月 22 日(水)1-2h

ファイル処理

入出力処理について,これまでは, キーボードからの入力と端末画面への出力だけだった. 今回は,ファイルからの入力およびファイルへの出力について理解しよう.


標準入出力とリダイレクト

データを入力する場合, これまでに作ってきたプログラムでは, 次のような対話的な方法を使ってきた:

$  プログラム
データを入力せよ > データ1
データを入力せよ > データ2
...

この方法だと, すでに入力してしまったデータを訂正したい場合には, プログラムを再実行して, すべてのデータを最初から入力し直さなければならない.

もちろん,プログラムが訂正機能を備えていれば別だが... すべてのプログラムに訂正機能を実装するのは面倒だ.

Unix ではその代わりに, データをあらかじめファイルに書き込んでおき, それらを一括して入力する方法が好んで利用される. この方法ならば, エディタ(viemacs,など)の機能を利用すれば, 入力データを訂正できるわけだ.

これなら,すべてのプログラムに訂正機能を備えておく必要はなく, 非常に合理的だ.

また,Unix では,キーボードも端末画面も実は, ファイルの一種とみなされている. たとえば,次のような入出力リダイレクト (「<」および「>」) を指定してプログラムを実行することによって, ファイルからの入力およびファイルへの出力を簡単に実現できる. 一般型は次の通り:

$  プログラム < 入力ファイル		# データをファイルから入力
$  プログラム > 出力ファイル		# 実行結果をファイルへ出力
$  プログラム < 入力ファイル > 出力ファイル

なお,この場合, scanf( ) 関数などの入力処理では, キーボードからではなく,入力ファイルからデータを読み取ることになる. また,printf( ) 関数などの出力処理では, 端末画面ではなく,出力ファイルへデータを書き込むことになる. つまり,入力リダイレクトとキーボード入力とは同じことであり, また,出力リダイレクトと画面出力も同じことになる. そのため,これらはひと括りに 標準入力(キーボードと入力ファイル)および 標準出力(端末画面と出力ファイル)と呼ばれている.

プロンプトの「 > 」 (プログラムが表示したもの)と リダイレクトの「 > 」 (コマンドラインでユーザが指定するもの)との区別に注意.

では,List 1 を利用して,これらの意味を理解しよう.

List 1. ひとつの単語を入出力するプログラム io-1.c
#include <stdio.h>

int main()
{
	char	word[256];

//	printf("単語 > ");	// プロンプト(リダイレクトの邪魔になるので今回は不要)
	scanf("%s", word);	// 単語の入力

	printf("%s\n", word);	// 単語の出力
	return (0);
}

List 1 の実行例:

$ ./io-1		# ふつーに実行
単語 >  apple		# てきとーな単語をキーボード入力
apple			# 入力した単語が表示される

$ ./io-1 > word.txt	# 出力リダイレクトを指定して実行
apple			# てきとーな単語をキーボード入力
			# あれ?何も表示されない

$ cat word.txt		# ファイル word.txt の内容を表示するコマンド
apple			# ファイルに書き込まれてたー

$ vim word.txt
applepie		# ファイル内容修正

$ ./io-1 < word.txt	# 入力リダイレクトを指定して実行
applepie		# ファイル word.txt の内容が入力され,表示された

ちなみに,「>」の代わりに「>>」を使えば, 出力ファイルの末尾へのデータの追加も可能.

End of File

次は,複数のデータを入出力してみよう.

入力データの個数が未知の場合,これまでは, 番兵法を使っていた. 今回からは,List 2 のように,よりスマートな方法を使って行こう.

List 2. 複数の単語を入出力するプログラム io-2.c
#include <stdio.h>

int main()
{
	char	word[256];

	while (1) {
//		printf("単語 > ");	// プロンプトは不要(リダイレクトの邪魔...)
		if (scanf("%s", word) == EOF) break;	// 単語の入力,入力が尽きるまで...
		printf("%s\n", word);	// 単語の出力
	}
	return (0);
}

なお,関数 scanf() は, 標準入力(ファイル)からデータをすべて読み尽くすと, int 型の戻り値 -1 を返すように定義されている. そして,この特別な値 -1 は,ヘッダファイル stdio.h 内で, EOF(End of File)としてマクロ定義されている. List 2 では.これを利用して,データ入力の終了を検出している. 教科書 p.191 も参照せよ.

勘違い注意: 変数 wordEOF が代入されるわけではない. 関数 scanf()EOF を返すんだ. なお,EOF の発生時,word には何も代入されず, 元の内容のまま変化しない. 番兵法との違いを理解せよ.

List 2 の実行例:

(まず,キーボード入力で...)
$ ./io-2
apple  bacon  coffee	# 入力
apple			# 出力
bacon
coffee
donut  egg
donut
egg
...
[Ctrl] + [D]	# 入力終了(EOF 発生)→ break

(次に,ファイル入力で...)
$ vim   word.txt
apple
bacon
coffee
...

$ ./io-2  <  word.txt
apple
bacon
coffee
...

# 読み尽くすと EOF → break
ファイルから入力する場合,データを読み尽くすと,自動的に EOF が発生する. 一方,キーボードから入力する場合には, [Ctrl] + [D] によって,人為的に EOF を発生させる.

さて,以上の例のように単純な入出力処理であれば, 標準入出力とリダイレクトだけで対応できるのだが, 次のように複雑な処理の場合には対応できない:

これらのような場合には,以下で説明するようなファイル入出力を使うことになる.


ファイルの準備と後片付け

C言語でファイルにアクセスするには, ファイルポインタFILE 構造体へのポインタ)と ファイル関数とを利用することになる.

構造体とは,複数のデータをひとまとめに取り扱うためのものである. 後期科目「情報工学実験I」で説明予定.

FILE 構造体には, ファイルの大きさや現在の読み書き位置などの情報が記録される. (ファイルの内容が記録されるわけではない.)

ただし,これらの情報を直接的に操作することはない. 必ず,ファイル操作用のライブラリ関数を利用して間接的に操作することになる. したがって,この構造体にどんな情報が記録されているのか? 詳しく知っている必要はない.

ファイル入出力を利用するには,まず, ファイルをオープンし,ファイルポインタを取得しておく必要がある:

FILE *fp;
fp = fopen(ファイル名, アクセスモード);

ここで,ファイル名とアクセスモードは,どちらも文字列として指定される.

ファイル名とアクセスモードの文字列は, もちろん,文字列定数だけでなく, 文字配列や文字列ポインタであっても構わない.

アクセスモードには,次のようなものがある:

詳しくは,教科書 pp.302-303 を参照しよう.

通常,ファイルの内容はハードディスクに記録されている. しかし,ファイルをオープンすると,そのファイルの内容の一部が, ファイルバッファと呼ばれるメモリ領域に一時的にコピーされる. (FILE 構造体に記録されるわけではない.) そして,プログラムによるファイルの操作は,このバッファ領域を介して, メモリ上で実行されることになる.

このように,ファイルに関する情報 (ファイル構造体やファイルバッファ)は, メモリに格納され,変数等のために使用可能なメモリ領域が減ることになる. ファイルの利用が終ったら,メモリを節約するために, 必ずファイルを閉じること:

fclose(ファイルポインタ);

プログラムの「終了時」には,オープンしていたファイルは自動的にクローズされる. だが,これに頼るな.

プログラムの「実行中」には,指定しない限りクローズされない. クローズせずにオープンだけを繰り返してしまうと, メモリは有限なので,いつかオープンできなくなってしまうことになる. (このページの末尾の補足を参照.)

たとえ自動クローズが期待される場合であっても, 必ず,クローズ処理を明示すること.

なお,ファイルオープンに失敗した場合 (たとえば,存在しないファイルをリードモードでオープンしようとした場合など), fopen( ) 関数は NULL を返す. ここで,NULL は, 特別なメモリアドレス 0x00000000 を指すポインタのマクロである. ファイル処理関連の型やマクロは, おなじみのヘッダファイル stdio.h の中で宣言・定義されている.

以上のことから,ファイル処理では, 次のようなパターンのコードがよく利用される:

#include <stdio.h>
...

	FILE *fp;
	char *filename = "infile.txt";

	if ((fp = fopen(filename, "r")) == NULL) goto ERROR;

	ファイルに対する処理
	...
	fclose(fp);	// 閉じ忘れるな
	...

ERROR :
	printf("そんな名前のファイルはないゼ.\n");
	...
上のコードの if 文はちょっとだけ高度だ. 次の2ステップの処理を1行で記述している.
fp = fopen(...);	// ファイルをオープン
if (fp == NULL) ...	// オープン失敗の場合...
ファイル処理では毎回使うコードなので,1行にまとめてしまおう.

ファイル入出力

ファイルの内容にアクセスするために, 標準入出力関数 scanf( )printf( ) と同様な ファイル入出力関数が用意されている:

fscanf(ファイルポインタ,書式,... );
fprintf(ファイルポインタ,書式,... );

第1引数のファイルポインタには, fp = fopen(...) によって取得した ファイルポインタ fp を指定すること.

当然,オープンする前のファイルポインタを指定しても,入出力は無理. オープン前のポインタは,ファイル構造体ではなく, どこかのゴミを指していることに注意.

ところで,Unix では, 次のような特別な名前のファイルポインタがあらかじめ用意されている:

これらは,オープン済みのファイルポインタ.

まず,標準入力について, fscanf(stdin, ... ) は, scanf(...) と同じことになる. 同様に,標準出力について, fprintf(stdout, ... ) は, printf(...) と同じである.

また,fprintf(stderr, ... ) は, 出力リダイレクト「>」を使っている場合であっても ファイルには書き込まず,端末画面に出力する. したがって,その名の通り,エラーの通知に利用する他, プログラムの進行状況の確認(デバッグ作業)などにも利用できる.

逆に言えば,これからは, 標準出力にエラーメッセージを表示してはダメだ. エラーを標準出力してしまうと:

面倒を避けるために, 標準出力とエラー出力とを使い分けること.

標準出力と標準エラー出力の違いを理解しよう:

$  ls			# ファイルを確認
infile.txt  outfile.txt

$  ls nofile.txt outfile.txt > ls.txt	# 存在しないファイルを ls し,出力リダイレクト
ls: nofile.txt にアクセスできません: No such file or directory
				# ↑ ls のエラー出力(出力リダイレクトされていない)
$  cat < ls.txt
outfile.txt			# ls の標準出力(出力リダイレクトされていた)

補足

標準出力はバッファリング (buffering,一時的に蓄積)されるので, 出力関数を実行してから実際に表示されるまでには, ある程度の時間がかかる. また,その間にプログラムが異常終了してしまうと, バッファの内容は表示されなくなってしまう.

「時間がかかる」といっても,人間が体感できるほどの時間ではない. 「表示処理が完了する前に次の処理が実行される場合がある」という意味.

一方,標準エラー出力は直接に表示されるので, 実行すれば即座に必ず表示される. したがって,たとえば,デバッグ用のテスト出力には, prinff(...) ではなく, fprintf(stderr, ... ) の方が確実.

上級者向け: バッファリングの効果については, 改行記号「\n」を出力せずに大量のデータを出力しておいて, 途中でわざとセグメントエラー等で異常終了させてみる, などとすると確かめられるかもしれない.

例:文字変換プログラム

標準入力・標準出力・標準エラー出力の関係を理解するために, List 3 の文字変換プログラム toupper を利用してみよう. このプログラムは,標準入力に英文を与えると, すべての小文字(lowercase)を大文字(uppercase)に変換し,標準出力する. また,変換した文字の個数を標準エラー出力する.

List 3. 文字変換プログラム toupper.c
#include <stdio.h>

int main()
{
	int n = 0;
	char c;

	while (scanf("%c", &c) != EOF) {
		if (('a' <= c) && (c <= 'z')) {
			c += 'A'-'a'; 
			n++;
		}
		printf("%c", c);
	}
	fprintf(stderr, "%d 文字を大文字化しました.\n\n", n);
	return(n);
}								

適当な内容の英文データファイル infile.txt を用意してから, 実行してみよう:

$  cat < infile.txt
Hello World

$  ./toupper < infile.txt
HELLO WORLD
8 文字を大文字化しました.

$  ./toupper < infile.txt > outfile.txt
8 文字を大文字化しました.	# 標準エラー出力の内容

$  cat < outfile.txt
HELLO WORLD			# 標準出力の内容

例:データ分割プログラム

次に,ファイル入出力の例を示す. List 4 は,表データを列データに分解するプログラム cuttable のソースである. このプログラムは,列数 2 の表データを標準入力し, 1 列目をファイル list1.txt へ, 2 列目をファイル list2.txt へ, それぞれ出力する.

List 4. データ分割プログラム cuttable.c
#include <stdio.h>

int main()
{
	FILE *fp1, *fp2;
	char word1[256], word2[256];

	if ((fp1 = fopen("list1.txt", "w")) == NULL) goto ERROR1;
	if ((fp2 = fopen("list2.txt", "w")) == NULL) goto ERROR2;

	while (scanf("%s %s", word1, word2) != EOF) {
/*
// または...
	while (1) {
		if (scanf("%s %s", word1, word2) == EOF) break;

// 使い分け基準:
// 反復条件が複雑な場合は while (1) { if (...) break; ... }
// 反復条件が単純な場合は while (...) { ... }
*/
		fprintf(fp1, "%s\n", word1);
		fprintf(fp2, "%s\n", word2);
	}

	fclose(fp1);
	fclose(fp2);
	return (0);

ERROR2 :
	fclose(fp1);		// fp2 はまだ開いていないので fp1 だけ閉じる
ERROR1 :
	fprintf(stderr, "ファイルオープンに失敗.\n");
	return (1);
}								

エラー処理について, 入り口が ERROR1ERROR2 とに分かれており, 混乱しそうだ. 次のようにまとめてもよい:

	...
	FILE *fp1 = NULL, *fp2 = NULL;	// NULL を未開封の標識とする
	...
	if (fp1 = fopen ...) goto ERROR;
	if (fp2 = fopen ...) goto ERROR;
	...
ERROR:
	if (fp1 != NULL) fclose(fp1);	// 開いてる場合だけ閉じる
	if (fp2 != NULL) fclose(fp2);
	fprintf(stderr, ...);
	...

適当な内容の表データファイル table.txt を用意してから, 試してみよう:

$  cat table.txt
toyota	vitz
nissan	march
honda	fit
mazda	demio

$  ./cuttable < table.txt

$  cat list1.txt		# table.txt の 1 列目が入っている
toyota
nissan
honda
mazda

$  cat list2.txt		# table.txt の 2 列目が入っている
vitz
march
fit
demio

# 以下,エラー処理のテスト(わざと失敗させてみる)
$  chmod  -w  list2.txt		# list2.txt を書き込み不可にする
$  ./cuttable < table.txt
ファイルオープンに失敗.

# 元に戻す
$  chmod  +w  list2.txt		# list2.txt を書き込み可能にする
$  ./cuttable < table.txt
$  cat ...

練習問題

ふたつの列データファイル list1.txt および list2.txt の内容を読み取り,(入力ファイル名は決め打ちでよい.) それらを組み合わせた表データを標準出力するようなプログラム (つまり,List 4 の cuttable.c の逆バージョン) pastetable.c を作成せよ.

アドバイス:


補足

ファイルをクローズせずに何個もオープンし続けるとどうなってしまうのか? 次のプログラム no-fclose.c を試してみよう:

#include <stdio.h>

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

	while (1) {
		fp = fopen("no-fclose.c", "r");
		if (fp == NULL) break;
	//	fclose(fp);	// わざと閉じ忘れてみる
		n++;
	}
	printf("%d 回しか開けなかったじゃねーか.\n", n);
	printf("開けたら閉じとけ.\n");
	return (0);
}
ま,短時間で動作を終了するような初歩的なプログラムであれば, fclose() を忘れて,途中停止したとしても, 大して問題とはならないだろう. しかし,24時間無休で大量のファイルを処理するような 実用的なプログラム(例:ウェブサーバ)では, 意図しない停止は大問題である.

正しく直せば(fclose() すれば), このプログラムは24時間365日間無休で動作し続ける. なお,強制終了は [Ctrl]+[C] キー


(c) 2015, yanagawa@kushiro-ct.ac.jp