ファイルからの入力とファイルへの出力について, 例題の作成・実行を通じて理解しよう.
ファイル入出力の機能を実装すれば, データを何時でも何度でも再利用できるようになり, プログラムの実用性が向上する.
ファイル data.txt から整数データを入力し, 画面へ表示してみる:
$ vim read.c
#include <stdio.h> int main(void) { FILE *fp; // ファイルポインタの宣言.どのファイルにアクセスするか指定するよ int x; // 入力データ int s; // 入力完了判断用 fp = fopen("data.txt", "r"); // ファイルからの入力の準備 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); }
新しいライブラリ関数やデータ型が出て来た...
通常は,ポインタとして利用する.例:FILE *fp;
モード名 "r" は入力(read), "w" は出力(write), "a" は追加出力(append).
ファイルが存在しない等で失敗した場合, 特殊な戻り値 NULL を返す.
$ cc read.c -o read $ ls read read.c # まだ data.txt が存在しないので... $ ./read fopen() 失敗: No such file or directory $ vim data.txt # 入力データファイルを作成(適当な整数値) 89 32 5 $ ./read 89 32 5
次は逆に,キーボードから整数データを入力し, ファイル data.txt へ出力してみる:
$ cp read.c write.c # read.c を元に改造... $ vim write.c
... int main(void) { ... fp = fopen("data.txt", "w"); // ファイルへの出力の準備 ... 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 の内容が書き換えられた.
さらに,キーボードから整数データを入力し, ファイル data.txt へ追加出力してみる:
$ cp write.c append.c # write.c を元に改造... $ vim append.c
... int main(void) { ... fp = fopen("data.txt", "a"); // ファイルへの追加出力の準備 ... 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 の末尾に入力データが追加された
使ったファイルを片付けずに復数のファイルを使い続けるとどうなる? とりあえず,同じファイルをクローズせずに複数回オープンしてみる:
$ 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]
# ...おー,無限にオープンできますね.
利用完了したファイルは必ずクローズすること!!
ぶっちゃけ,少数なら同時にオープンのまま放置でも問題ないし, プログラム終了時には自動的にクローズされる.
しかし,大規模・長期間に運用されるシステムでは, 当初の小規模なテストでは大丈夫と油断していると, 実運用では突然死が頻発し大問題となる.
要するに... 「使い終わったら片付けなさーい」
ファイルのクローズが多過ぎるとどうなる? 既にクローズしたものや,まだオープンしていないもの, オープンできなかったものをクローズしてみる:
$ cp fclose1.c fclose2.c # fclose1.c を元に改造... $ vim fclose2.c
... int main(void) { FILE *fp; /* fp = fopen("notexist.txt", "r"); // 存在しないファイル...オープン失敗... fp = NULL fclose(fp); // オープンできなかったのにクローズ... fclose(NULL) */ /* fp = fopen("fclose2.c", "r"); fclose(fp); fclose(fp); // クローズしすぎ */ fclose(fp); // オープンせずにクローズ return (0); }
$ cc fclose2.c -o fclose2 $ ./fclose2 Segmentation fault (コアダンプ) # 等,実行時エラーだらけ ... # コメント /* 〜 */ 部分を色々と試してね
ファイルのオープンとクローズの実行は1対1に対応付けること!!
ファイル 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 関連)がかなり目障りではあるが, これらは安全第一のためのお約束事です.
$ 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 にコピーされた
実行例:
$ cat data.txt 89 32 5 $ ./num $ cat data2.txt 1 89 2 32 3 5
ヒント:copy.c を元にして,行番号のカウントと出力を追加するだけ. 列の区切りには TAB文字 "\t" を使うとよい.
実行例:
$ cat data2.txt 1 89 2 32 3 5 $ ./cut $ cat cut1.txt 1 2 3 $ cat cut2.txt 89 32 5
ヒント:これも copy.c を元にして, 出力ファイルを2個に増やす.(ファイルは合計3個とする.) データを2個ずつ入力し,1個ずつ別々のファイルへ出力する.