ファイルへの出力

この節では、ファイルへデータを書き込む方法について学びます。

cp コマンド(簡易版)を作ってみる

例題として、UNIX の cp コマンドに相当するプログラムを作ってみましょう。 ただし、ここで実現するのは cp の基本機能である通常のファイルのコピーだけです。 (ディレクトリのコピーなどはずっと面倒です。)プログラム名は mycp としておきます。

基本的なアイデアは簡単で、以前に作った mycat を改造すればすぐにでき上がります。 mycat では、ファイルから1バイト読み出しては putchar 関数でそれを標準出力に出していましたが、 putchar 関数の代りにファイルに1バイト書き込む関数 fputc を使えば、 読み出した内容がそのまま出力ファイルに書かれるので、コピーができることになります。

mycat が非効率なプログラムだったので、こうして作った mycp も低速になってしまいますが、あくまで例題として考えて下さい。

キーポイントとなるのは、書き込み用にファイルをオープンする方法と、fputc 関数の使い方くらいのものです。

次のようなプログラムを mycp.c というファイルに記述します:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
  FILE *inputfile;         // 入力ストリーム
  FILE *outputfile;         // 出力ストリーム
  int c;                   // 読み込んだ1バイトを入れておく

  if (argc != 3) {          // コマンドライン引数が2個でない場合
    exit(1);               // 異常終了
  }

  inputfile = fopen(argv[1], "r");    // ファイルを読み出し用にオープン(開く)
  if (inputfile == NULL) {            // オープンに失敗した場合
    exit(1);                          // 異常終了
  }

  outputfile = fopen(argv[2], "w");    // ファイルを書き込み用にオープン(開く)
  if (outputfile == NULL) {            // オープンに失敗した場合
    exit(1);                          // 異常終了
  }

  while (1) {    // 無限ループ
    c = fgetc(inputfile);     // ファイルから1バイト読み込んで c に入れる
    if (c == EOF) {           // もしファイルの終端に達していたら
      break;                  // while ループから抜け出す
    }
    fputc(c, outputfile);               // c を出力ストリームに出力
  }

  fclose(outputfile);         // 出力ストリームをクローズ(閉じる)
  fclose(inputfile);          // 入力ストリームをクローズ(閉じる)
  return 0;
}

これをコンパイル・実行すると次のようになります:

% cc -o mycp mycp.c
% ./mycp file1 file2    ← file1 を file2 にコピー。
% cmp file1 file2       ← cmp コマンドで2つのファイルの違いを探す。
%                       ← 何も表示されないので、2つのファイルは同一内容。
                           確かにコピーできた。

これを試してみるときは、前もってエディタなどで file1 というファイルを作成しておく必要があります。内容は何でも構いません。なお、cmp コマンドは2つのファイルの内容を比較して異なった箇所を探して表示してくれるもので、その名前は "compare" (比較する)に由来します。

以下、このプログラムのキーポイントを解説して行きます。

書き込みモードでファイルをオープン

書き込み用にファイルをオープンするには、やはり fopen 関数を用いますが、 ファイルオープンモードとして "r" ではなく "w" (書き込みモード)を指定します。(もちろん、"w" は "write" に由来します。)

outputfile = fopen(ファイル名, "w");  // ファイルを書き込み用にオープン

ファイルが無事にオープンできたとき、 fopen はやはり FILE 構造体へのポインタ(ストリーム)を返しますので、それ以降、 そのポインタを通じてファイルへの出力を行います。

書き込みモードが指定されたとき、fopen は、第1引数に与えられたファイル名を持つファイルがまだ存在しなければ、それを作成します。作成直後、ファイルの内容は空です。気をつけなければならないのは、指定されたファイル名のファイルがすでに存在する場合です。 その場合、そのファイルの内容はいったん完全に消され、空になってしまいます。 ファイルの内容が消されては困る場合、 このモードでオープンしないように注意しなければなりません。

ファイルオープンモードのまとめ

ここまでで、"r" と "w" という2つのファイルオープンモードについて学びましたが、他のファイルオープンモードについても少し触れておきましょう。

"w" モードでは、すでに存在するファイルを開くとその内容が空になるのでしたが、 そうではなく、現在の内容はそのままにしておいて、 その末尾に新たな内容を書き足したい場合もよくあるでしょう。 例えば、ソフトウェアの動作記録をファイルに残していくような場合、 以前の記録をそのままにして、新たな記録をファイル末尾に書き足せばいいはずです。 そのようなときは、"a" モード(追加モード、"a" は append に由来)を使用します。

また、すでに存在するファイルの内容を更新するような場合、現在の内容の読み出しと、更新後の内容の書き込みの両方を行わねばならないので、ファイルを読み書き両用にオープンする必要があります。そのような場合は、"r+", "w+", "a+" の3つのモードを用います。ただし、この3つのモードの使い方は少し複雑なので、この章では詳しく解説しません。

以上の6つのモードについて、表にまとめておきます:

ファイルオープンモード
モード働き
"r"読み出し(のみの)モードでファイルをオープンする。ファイルが存在しないときはオープンできない。
"w"書き込み(のみの)モードでファイルをオープンする。ファイルがなければ作成し、存在していれば最初にその内容を空にする。
"a"追加モードでファイルをオープンする。ファイルがなければ作成し、存在していれば、現在の内容はそのままで、ファイル末尾に追加書き込みが行われる。
"r+"読み書き両用のモードでファイルをオープンする。ファイルが存在しないときはオープンできない。
"w+"読み書き両用のモードでファイルをオープンする。ファイルがなければ作成し、存在していれば最初にその内容を空にする。
"a+"読み書き両用のモードでファイルをオープンする。ファイルがなければ作成し、存在していれば、現在の内容はそのままで、ファイル末尾に追加書き込みが行われる。

ファイルへの1バイト書き込み

ファイルへ1バイト書き込むには、fputc 関数を用います:

    fputc(c, outputfile);               // c を出力ストリームに出力

第1引数は int 型の数値です。第2引数には出力ストリームを与えます。 fputc は第1引数 c の最下位8ビットを出力ストリームに書き込みます。 putchar 関数との違いは、putchar が標準出力に書き込みをするのに対して、 fputc は第2引数で指定された出力ストリームに書き込みをする、というだけの違いです。 従って、使い方に関する注意点も putchar と同様です。

数値の累計を求めるプログラム

以前にとりあげた total2 というプログラムは、ファイルの各行に1つずつ記された数値を すべて合計して、合計値を標準出力に出すプログラムでしたが、今度は、数値を読み出すたびに、 そこまでに現れた数値の合計をファイルに出力するプログラム total3 を作成してみます。 コマンドラインの第1引数に入力ファイル、第2引数に出力ファイルを指定するものとします。 すなわち、コンパイル・実行したときに以下のように働くプログラムです:

cc2001(83)% cc -o total3 total3.c
cc2001(84)% cat data    ← 入力ファイルの例
3.0
2.0
4.0
1.0
…(続く)…
cc2001(85)% ./total3 data output  ← total3 を実行
cc2001(86)% cat output  ← 出力を確かめてみる
Total = 3.000000    ← 1行目だけの合計
Total = 5.000000    ← 2行目までの合計
Total = 9.000000    ← 3行目までの合計
Total = 10.000000   ← 4行目までの合計
…(続く)…

ソースプログラム total3.c は次のようなものです:

#include <stdio.h>
#include <stdlib.h> 
#define LINESIZE 256               // 1行の長さの上限
#define BUFFERSIZE (LINESIZE + 1)  // バッファのサイズ

char linebuffer[BUFFERSIZE];      // 1行分の文字列を入れるためのバッファ

int main(int argc, char **argv) {
  FILE *inputfile;         // 入力ストリームを入れる変数
  FILE *outputfile;        // 出力入力ストリームを入れる変数
  double total = 0.0;      // 合計を入れるための変数

  if (argc != 3) {          // コマンドライン引数が2個でない場合
    exit(1);               // 異常終了
  }

  inputfile = fopen(argv[1], "r");    // ファイルを読み出し用にオープン(開く)
  if (inputfile == NULL) {            // オープンに失敗した場合
    exit(1);                          // 異常終了
  }

  outputfile = fopen(argv[2], "w");    // ファイルを書き込み用にオープン(開く)
  if (outputfile == NULL) {            // オープンに失敗した場合
    exit(1);                          // 異常終了
  }

  while (1) {    // 無限ループ
    double x;    // 読み出した実数値を入れる変数 
    char *s;     // fgets の返した値を入れる変数

    s = fgets(linebuffer, BUFFERSIZE, inputfile);     // ファイルから1行読み出して linebuffer に入れる
    if (s == NULL) {          // もしファイルの終端に達していたら
      break;                  // while ループから抜け出す
    }
    sscanf(linebuffer, "%lf", &x);  // 文字列からdouble型の数値を読み取る
    total = total + x;        // 読んだ数値を total に足し込む
    fprintf(outputfile, "Total = %f\n", total);  // 合計を印刷
  }

  fclose(outputfile);         // 出力ストリームをクローズ(閉じる)
  fclose(inputfile);          // 入力ストリームをクローズ(閉じる)
  return 0;
}

処理手順はほとんど total2.c と同様です。キーポイントは合計値を出力する際に、printf 関数の代りに fprintf 関数を使うことです。

ファイルへのフォーマットつき出力(fprintf)

fprintf 関数は次のような形で用いられます:

fprintf(出力ストリーム, フォーマット文字列, ...)

fprintf の第1引数は FILE 構造体へのポインタで、出力ストリームを指定します。 この出力ストリームに対して、第2引数のフォーマット文字列に従った出力が行われます。 printf 関数と fprintf 関数の違いは、printf 関数が標準出力に出力を行うのに対して、 fprintf 関数は第1引数で指定された出力ストリームに書き込みをする、 というだけのことです。すなわち、fprintf の第2引数以降の引数は、 printf の引数と同じ働きをします。

バッファリングについて

fputc や fprintf を使ってファイルへの出力ができることを学びましたが、 実は、これらの関数を実行しても、ただちに出力が行われるとは限りません。 出力というのはかなり手間のかかる処理なので、 少しずつ何度もデータを出力するとプログラムが低速になってしまいます。 そこで通常、出力データはある程度の量になるまでバッファにためておいてから まとめて出力するようになっています。 このことを「バッファリング」と言います。

出力データをためておくためのバッファは、プログラマが自分で用意する必要はなく、 ストリーム出力のライブラリ関数を使うと自動的に確保されます(そのようにライブラリ関数が作られています)。 FILE 構造体の重要な働きの1つは、このバッファの管理のためのデータを保持することです。

出力データがすっかりたまるまで実際の出力がされないとなると、 出力が遅れて不都合な場合もあり得ます。 そのようなときは、 fflush 関数を fflush(出力ストリーム); の形で実行すると、 それまでにたまった出力データをすべて吐き出す(実際に出力する) ことができます。 これを、「バッファをフラッシュする」といいます。 もっとも、fflush 関数が本当に必要になる場合はまれです。

fclose 関数によって出力ストリームを閉じると、 たまっていた出力データはすべて吐き出されます。 逆に言うと、出力ストリームを閉じるまでは、 未出力のデータが残っているかも知れないので、 不要になった出力ストリームは早目に閉じたほうが良いでしょう。

なお、セグメントエラーなどでプログラムが強制終了された場合、 バッファはフラッシュされません。そのため、 「エラーの原因を探るために出力を見たいのに、 バッファにたまったままで見られない」という問題が起きることがあります。 そのような場合はエラーが発生する直前の箇所で fflush を実行しておく、といった対策が考えられます。

実は、入力もバッファリングされています。 入力もある程度のバイト数だけまとめて読み込んだほうが効率的なことが多いからです。 そこで、fgetc でディスク上のファイルから1バイトだけ読み出すような場合でも、 まず最初にまとまった量(例えば8キロバイト)のデータが読み出されてバッファに入れられ、そこから1バイトずつ読み出されます。 たとえて言えば、井戸からまとめてたくさん水をくんできて、 それを少しずつ使うのと似ています。 もちろん、くみ置きがなくなったら、またまとめてくみに行くのです。

課題

以前に作成したプログラム cost.c と同様に、 商品の仕入れの記録の入ったファイルを読み出して、 商品ごとに仕入れ費用を出力するプログラム cost2.c を考えよう。 ただし、仕入れ費用が10000円未満の商品と10000円以上の商品とに分けて、 別々のファイルに出力したいものとする。 コマンドラインの第1引数には仕入れの記録が入った入力ファイル、 第2引数には仕入れ費用が10000円未満の商品についての出力を入れる出力ファイル名、 第3引数には仕入れ費用が10000円以上の商品についての出力を入れる出力ファイル名を それぞれ指定する。

コンパイル・実行の例:

% cc -o cost2 cost2.c
% cat data        ← 入力ファイルの例
apple 100 50
orange 20 300
lemon 50 200
banana 60 250
kiwi 30 150
…(続く)…
% ./cost2 data output1 output2        ← cost2 を実行
% cat output1     ← 仕入れ費用10000円未満の出力
apple 5000
orange 6000
kiwi 4500
…(続く)
% cat output2     ← 仕入れ費用10000円以上の出力
lemon 10000
banana 15000
…(続く)…

このようなプログラム cost2.c を作成しなさい。