入出力処理について,これまでは, キーボードからの入力と端末画面への出力だけだった. 今回は,ファイルからの入力およびファイルへの出力について理解しよう.
データを入力する場合, これまでに作ってきたプログラムでは, 次のような対話的な方法を使ってきた:
$ プログラム データを入力せよ > データ1 データを入力せよ > データ2 ...
この方法だと, すでに入力してしまったデータを訂正したい場合には, プログラムを再実行して, すべてのデータを最初から入力し直さなければならない.
Unix ではその代わりに, データをあらかじめファイルに書き込んでおき, それらを一括して入力する方法が好んで利用される. この方法ならば, エディタ(vi,emacs,など)の機能を利用すれば, 入力データを訂正できるわけだ.
また,Unix では,キーボードも端末画面も実は, ファイルの一種とみなされている. たとえば,次のような入出力リダイレクト (「<」および「>」) を指定してプログラムを実行することによって, ファイルからの入力およびファイルへの出力を簡単に実現できる. 一般型は次の通り:
$ プログラム < 入力ファイル # データをファイルから入力 $ プログラム > 出力ファイル # 実行結果をファイルへ出力 $ プログラム < 入力ファイル > 出力ファイル
なお,この場合, scanf( ) 関数などの入力処理では, キーボードからではなく,入力ファイルからデータを読み取ることになる. また,printf( ) 関数などの出力処理では, 端末画面ではなく,出力ファイルへデータを書き込むことになる. つまり,入力リダイレクトとキーボード入力とは同じことであり, また,出力リダイレクトと画面出力も同じことになる. そのため,これらはひと括りに 標準入力(キーボードと入力ファイル)および 標準出力(端末画面と出力ファイル)と呼ばれている.
では,List 1 を利用して,これらの意味を理解しよう.
#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 の内容が入力され,表示された
次は,複数のデータを入出力してみよう.
入力データの個数が未知の場合,これまでは, 番兵法を使っていた. 今回からは,List 2 のように,よりスマートな方法を使って行こう.
#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 も参照せよ.
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
さて,以上の例のように単純な入出力処理であれば, 標準入出力とリダイレクトだけで対応できるのだが, 次のように複雑な処理の場合には対応できない:
これらのような場合には,以下で説明するようなファイル入出力を使うことになる.
C言語でファイルにアクセスするには, ファイルポインタ(FILE 構造体へのポインタ)と ファイル関数とを利用することになる.
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");
...
fp = fopen(...); // ファイルをオープン if (fp == NULL) ... // オープン失敗の場合...
ファイルの内容にアクセスするために, 標準入出力関数 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, ... ) の方が確実.
標準入力・標準出力・標準エラー出力の関係を理解するために, List 3 の文字変換プログラム toupper を利用してみよう. このプログラムは,標準入力に英文を与えると, すべての小文字(lowercase)を大文字(uppercase)に変換し,標準出力する. また,変換した文字の個数を標準エラー出力する.
#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 へ, それぞれ出力する.
#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);
}
エラー処理について, 入り口が ERROR1 と ERROR2 とに分かれており, 混乱しそうだ. 次のようにまとめてもよい:
... 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 ...
アドバイス:
ファイルをクローズせずに何個もオープンし続けるとどうなってしまうのか? 次のプログラム 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時間365日間無休で動作し続ける. なお,強制終了は [Ctrl]+[C] キー.