ファイルからの入力とファイルへの出力について, 例題を通じて理解しよう.
ファイル入出力の機能を実装すれば, データを保存し何度でも再利用できるようになり, プログラムの実用性が向上する.
データファイル 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); }
新しいライブラリ関数やデータ型が出て来た...
通常は,ポインタとして利用する.例:FILE *fp;
モード名:
ファイルが存在しない等で失敗した場合, 特殊な戻り値 NULL を返す.
指定した文字列を表示するだけでなく, エラーの理由も自動生成してくれる.
なお,これらを使うために必要はヘッダファイルは stdio.h です.
コンパイル・実行:
$ 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 の内容が入力され,そのまま出力された.
次は逆に,キーボードから整数データを入力し, データファイルへ出力してみる:
$ 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 の内容が書き換えられた.
さらに,キーボードから整数データを入力し, データファイルへ追加出力してみる:
$ 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 の末尾に入力データが追加された.
使ったファイルを片付けずに復数のファイルを使い続けるとどうなる? とりあえず,同じファイルをクローズせずに複数回オープンしてみる:
$ 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] # 強制終了
# ...おー,無限にオープンできますね.
利用完了したファイルは必ずクローズすること!!
ぶっちゃけ,処理対象のファイルが少数なら同時にオープンのまま放置でも問題ないし, プログラム終了時には自動的にクローズされる.
しかし,大規模・長期間に運用されるシステムでは, 当初の小規模なテストでは大丈夫だった〜,よしこれで完成だ〜,と油断していると, 実運用では突然死が頻発し大問題となったりする.
要するに... 「使い終わったら片付けなさーい💢」母より. 後処理の重要性を躾けてくれていたんですね.
ファイルのクローズが多過ぎるとどうなる? まだオープンしていないものや, 既にクローズしたものや, オープンできなかったものをクローズしてみる:
$ 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に対応付けること!!
同時に 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; // これでないとNG } 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 にコピーされた
実行例:
$ 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 col1.txt 1 2 3 $ cat col2.txt 89 32 5
ヒント:これも copy.c を元にして, 出力ファイルを2個に増やす.(ファイルは合計3個とする.) データを2個ずつ入力し,1個ずつ別々のファイルへ出力する.