ファイル入出力

これまでのプログラムは、基本的に入力は標準入力(キーボード)、出力は標準出力(画面)でした。しかしこれでは、同じプログラムを何度も実行する時に過去の結果を残したい場合や、過去の結果や大量の入力が必要なプログラム等を作成する場合に不都合です。このようなプログラムは、「ファイルからデータを読んだり、ファイルにデータを書き込んだり」という操作が必要になってきます。ここでは、そのようなファイルに関する操作の説明をしていきます。

■ 6-1 ファイル操作の手順

ファイルの操作は以下のような手順で行う必要があります。

A) ファイルを開く(オープンする)
B) ファイルの操作を行う
C) ファイルを閉じる(クローズする)

この節ではファイルのオープンとクローズの方法について説明します。


ファイルを開く

ファイルの操作(読み込み、書き込みなど)を行う前に必ずファイルを開く(オープン)ことが必要になります。ファイルを開く時には、どのファイルを、どのようなモードで開くのかを指定しなければなりません。例えば、「test.datというファイルを読み込み専用モードでオープンする」には以下のようにfopen関数を用います。第1引数がファイル名、第2引数がモードです。

使用例
fopen("test.dat", "r");

ところで、一つのプログラムでオープンするファイルは一つとは限りません。一つのプログラムで複数のファイルを同時に開くことも少なくありません。そのようなばあいに、どのようにオープンしたファイルを識別するのでしょうか?

C言語ではファイルの識別のためにファイルポインタと呼ばれる情報を用います。

ファイルポインタ 
      FILE *fp;    <--- ファイルポインタの名前(この場合はfp)は任意に決定可能
実際にはFILE型(構造体FILE)のポインタ型変数です。FILE構造体はstdio.hというヘッダファイルに予め定義されています。

オープンしたファイルをそれ以降識別するために、fopen関数はファイルポインタを返します。

FILE *fopen(char *filename, char *mode)
第1引数はオープンするファイルの名前が格納されている文字列へのポインタ。第2引数はオープンモードを示す文字列へのポインタです。オープンモードには下表のような種類があり、その指定に応じてfopen関数の振る舞いが変化します。
例えば、オープンモードが"r"であるときに第1引数で指定されたファイルが存在しない場合、fopen関数はNULL(ゼロを示すポインタ)を返しますが、オープンモードが"w"であるときは、新しくファイルを作成して、そのファイルポインタを返します。
使用例
FILE *fp;
fp = fopen("test.dat", "r"); <--- このようにしてfopen関数が返したファイルポインタを記憶する


ファイルを閉じる

ファイル操作が終了し、それ以降ファイル操作を行わないのであればファイルをクローズする必要があります。ファイルのクローズにはfclose関数を用います。

int fclose(FILE *fp);
     
fpで指されたファイルをクローズします。返り値は、正常にクローズできた時は0、エラーが発生した時はEOF(ヘッダファイルstdio.hで定義されている定数)です。なお、プログラムが終了する場合にはすべてのファイルが自動的にクローズされます
使用例
FILE *fp;
fp = fopen("test.dat", "r"); 
.....  <--- 何らかのファイル操作
fclose(fp);


■ 6-2 ファイル入出力関数

ファイルのオープンが正常にできれば、いよいよファイルに対する操作です。ファイルに対する操作には、ファイルに対してデータを書き込む、ファイルからデータを読み込む、の2種類がありますが、これらは専用の関数を用いて行います。以下が、一般的なファイル入出力関数です。

関数名 機能
fputc(int c, FILE *fp) fpで指されたファイルへ変数cの内容を出力
fputs(char *s, FILE *fp) fpで指されたファイルへsで指された文字列の内容を出力
fgetc(FILE *fp) fpで指されたファイルから一文字読み込む。
正常に読み込めた場合はその文字を返し、エラーの場合はEOFを返す。
fgets(char *s, int size, FILE *fp) fpで指されたファイルからsize-1バイトの文字をsで指された文字列へ読み込む。
正常に読み込めた場合はsを返し、エラーの場合はNULLを返す。
fprintf(FILE *fp, 書式format, 引数list) fpで指されたファイルへ出力するprintf関数
fscanf(FILE *fp, 書式format, 引数list) fpで指されたファイルから入力するscanf関数
使用例
     1  #include <stdio.h>
     2
     3  main(){
     4      FILE *fin;                  /* ファイルポインタ */
     5      FILE *fout;                 /* ファイルポインタ */
     6
     7      char x;
     8
     9      fin = fopen("input_file", "r");
    10      if(fin == NULL){
    11          printf("input_fileがオープンできませんでした。\n");
    12          exit(1);
    13      }
    14
    15      fout = fopen("output_file", "w");
    16      if(fin == NULL){
    17          printf("output_fileがオープンできませんでした。\n");
    18          exit(1);
    19      }
    20
    21      x = fgetc(fin);
    22      while(x != EOF){
    23          fputc(x, fout);
    24          x = fgetc(fin);
    25      }
    26  }

このプログラムはinput_fileという名前のファイルの内容を一文字ずつoutput_fileという名前のファイルにコピーするプログラムです。このサンプルを実行する前にあらかじめinput_fileという名前のファイルをxemacsなどを用いて作成しておいてください(内容は任意です)。

9行目でまずinput_fileという名前のファイルを読み込みモードでオープンしています。fopen関数はファイルをオープンできなかった場合はNULLを返しますので、その返り値がNULLであればエラーメッセージを表示してプログラムを終了します(10行目〜13行目)。このように、malloc関数と同様にファイル関連の処理を行う際にもエラーチェックを必ず行うようにして下さい。

21行目でファイルポインタfinで指されたファイルから1文字読み込んでいます。fgetc関数は読み込みが出来なかった時(エラーが発生したり、ファイルの最後の文字まで読み込んでしまって文字が残っていない等の理由が考えられます)はEOFを返します。従って、fgetc関数がEOFを返すまで、ファイルポインタfinで指されるファイルから1文字読み込んでは、ファイルポインタfoutで指されるファイルに1文字書き込むという作業を続けることでファイルのコピーを行っています。

使用例
     1  #include <stdio.h>
     2
     3  main(){
     4      FILE *fin;                  /* ファイルポインタ */
     5      FILE *fout;                 /* ファイルポインタ */
     6
     7      char string[11];
     8      char *res;                  /* fgets関数の返り値を格納する */
     9
    10      fin = fopen("input_file", "r");
    11      if(fin == NULL){
    12          printf("input_fileがオープンできませんでした。\n");
    13          exit(1);
    14      }
    15
    16      fout = fopen("output_file", "w");
    17      if(fin == NULL){
    18          printf("output_fileがオープンできませんでした。\n");
    19          exit(1);
    20      }
    21
    22      res = fgets(string, 11, fin);
    23      while(res != NULL){
    24          fputs(string, fout);
    25          res = fgets(string, 11, fin);
    26      }
    27  }

こちらのプログラムは、先ほどと全く同じ動作をするプログラムですが、1文字ずつではなく10バイト(10文字)ずつコピーしている点が異なります。

23行目からのループでファイルのコピーを行っていますが、fgets関数は読み込みが出来なかった時はNULLを返します。そのため、fgets関数がNULLを返すまで、このループを繰り返してファイルをコピーするわけです。なお、それぞれの関数の詳細についてはmanコマンドを利用して調べてください。


ファイルポジション

ファイルポインタには、現在、ファイルのどこが読み込み、書き込みの対象になっているかという情報も含まれています。このような情報をファイルポジションと呼びます。ファイルポジションはfgets関数やfputs関数を実行することにより変化します。例えば、fgets関数を用いてファイルから10バイト分のデータを読み込んだ場合、ファイルポジションはその分先に進みます。次に、fgets関数を実行するとその位置から指定されたバイト数を読み込むわけです。

ファイルポジションが保存されるおかげで、「1000バイトのファイルを10バイトずつに分けて読み込む」といったことが可能になるわけです。


演習問題6-1

下記のようにキーボードから任意の個数の文字列を読み込み、それをファイルdatafileに書き込むプログラムhozonを作成せよ。ただし、「end」と入力された時点で文字列の入力が終了するものとする。

さらに、ファイルdatafileの中身を行番号つきで表示するプログラムhyoujiを作成せよ。

演習問題6-1の解答


レポート課題6-1

任意のファイルを開き、その内容を行番号と共に画面に出力するプログラムgyouを作成せよ。但し、開くファイルの名前はプログラムの引数として与える。ファイルが存在しない時などは、エラー表示せよ。


■ 6-3 特別なファイルポインタ

ここまでで、主なファイル入出力関数について説明してきましたが、実はこれらの関数は2-2で説明した関数と全く同じものです。2-2では、これらの関数は標準入力と標準出力、そして、標準エラー出力に対する入出力を行う関数として使用しましたが、本来はこれらの関数はファイルに対する入出力を行うための関数です。

これらの関数は、ファイルポインタで示されたファイルに対して操作を行うわけですが、標準入力、標準出力、標準エラー出力を示す下記のような特別なファイルポインタがあらかじめ用意されており、それらを使うことにより、本来はファイルに対する操作の関数であるfgets関数などを標準入力に対して利用できるようになります。

stdin 標準入力を示すファイルポインタ
stdout 標準出力を示すファイルポインタ
stderr 標準エラー出力を示すファイルポインタ

これらのファイルポインタはあらかじめstdio.hヘッダファイルで宣言されていますから、通常のファイルポインタと異なり宣言する必要はありません。いきなり使用することが可能です。


■ 6-4 ブロックリードライト

これまでのファイル入出力では、データを1文字、1行などの単位で入出力を行ってきました。また、基本的に入出力を行うデータは文字か文字列でした。C言語では、これとは別に「ファイルの特定の位置から100バイト読め」や「特定のアドレスから100バイト分のデータをファイルに書き出せ」といった入出力が可能です。このような入出力のことをブロック・リードライトと呼びます。ブロックリードライトを行う関数は以下のfread関数とfwrite関数です。。

size_t fread(void *p, size_t size, size_t n, FILE *fp);     
fpで指されたファイルから「n個のsizeバイトのデータ」をポインタpで指された領域に読み込みます。つまり、サイズが幾つのデータを、何個、どこに読み込むか、を指定するわけです。
関数の返り値は読み込んだデータの個数です。ファイルの最後に達していたり、読み取りエラーのときは0を返します。なお、size_t型とはint型と同じであると考えてください。
size_t fwrite(void *p, size_t size, size_t n, FILE *fp);
ポインタpで指された場所にある「n個のsizeバイトのデータ」をfpで指されたファイルに書き込みます。つまり、どこのデータを書き込むか、そのデータのサイズはいくつか、何個のデータを書き込むかということを指定するわけです。関数の返り値は書き込んだデータの個数です。書き込みエラーのときは0を返します。
使用例
     1  #include <stdio.h>
     2  
     3  typedef struct sample{
     4    int a;
     5    float b;
     6    char c[11];
     7  }SAMPLE;
     8  
     9  main(){
    10    FILE *fp;                     /* ファイルポインタ */
    11    int x=10;                     /* ファイルに書き込んでみるint型変数 */
    12    float y=15.0;                 /* ファイルに書き込んでみるfloat型変数 */
    13    SAMPLE z={20, 40.5, "XYZ"};   /* ファイルに書き込んでみるSAMPLE型変数 */
    14    size_t res;                   /* fwrite関数の結果 */
    15  
    16    
    17    /* ファイルを書き込みモードでオープン */
    18    fp = fopen("test.dat", "w");
    19    if(fp == NULL){
    20      printf("ファイルを開くことができませんでした\n");
    21      exit(1);
    22    }
    23    
    24    /* xの中身をファイルに書き込む */
    25    res = fwrite(&x, sizeof(int), 1, fp); /* アドレス&xからint型のデータを
    26                                             一つfpで指されたファイル
    27                                             に書き込む */
    28    if(res == 0){
    29      printf("ファイルに書き込むことができませんでした\n");
    30      exit(1);
    31    }
    32  
    33    /* yの中身をファイルに書き込む */
    34    res = fwrite(&y, sizeof(float), 1, fp); /* アドレス&xからfloat型のデータを
    35                                             一つfpで指されたファイル
    36                                             に書き込む */
    37    if(res == 0){
    38      printf("ファイルに書き込むことができませんでした\n");
    39      exit(1);
    40    }
    41  
    42    /* zの中身をファイルに書き込む */
    43    res = fwrite(&z, sizeof(SAMPLE), 1, fp); /* アドレス&xからSAMPLE型の
    44                                                データを一つfpで指された
    45                                                ファイルに書き込む */
    46    if(res == 0){
    47      printf("ファイルに書き込むことができませんでした\n");
    48      exit(1);
    49    }
    50  }

このプログラムは、test.datファイルを作成して、その中にint型の変数x、float型の変数y、構造体型の変数zをそのままの形で書き込むプログラムです(正常終了した場合は何もメッセージが表示されません)。「そのままの形」というのは、文字列に変換してから書き込むわけではない、つまり、作成されたtest.datファイルをxemacsなどで開いたとしても意味のあるデータが表示されないということです(下図参照)。

25行目で、int型変数xの内容をファイルに書き込んでいますが、fwrite関数はint型のデータを書き込むには、(1) そのデータの先頭アドレス、(2) そのデータのサイズ、(3) データの個数が必要になります。ここで、(1)は&xで求めることが出来ますし、(3)は自明です(この場合は1)。問題になるのは(2)です。linuxではint型は4byteですから直接「4」と指定しても構いませんが、残念ながらint型のサイズは必ずしも4byteとは限りません。コンピュータやOSによって異なります。さらに、ユーザが定義した構造体の場合はどうでしょう?サイズをその都度計算するのは面倒ですよね。このような場合、5-1ででてきたsizeof演算子を用います。例えば、sizeof(int)とすればint型のサイズを返してくれますし、sizeof(SAMPLE)とすればSAMPLE型のバイト数を返してくれます。これを用いて(2)を簡単に得ることが出来ます。

なお、fwrite関数の返り値は実際に書き込んだデータの個数ですから、それが0である場合は何らかのエラーが発生しているということになります。よって、そのような場合はプログラムを終了します(28行目〜31行目)。


使用例
     1  #include <stdio.h>
     2  
     3  typedef struct sample{
     4    int a;
     5    float b;
     6    char c[11];
     7  }SAMPLE;
     8  
     9  main(){
    10    FILE *fp;                     /* ファイルポインタ */
    11    int x=10;                     /* ファイルに書き込んでみるint型変数 */
    12    float y=15.0;                 /* ファイルに書き込んでみるfloat型変数 */
    13    SAMPLE z={20, 40.5, "XYZ"};   /* ファイルに書き込んでみるSAMPLE型変数 */
    14    size_t res;                   /* fwrite関数の結果 */
    15  
    16    
    17    /* ファイルを読み込みモードでオープン */
    18    fp = fopen("test.dat", "r");
    19    if(fp == NULL){
    20      printf("ファイルを開くことができませんでした\n");
    21      exit(1);
    22    }
    23    
    24    /* ファイルからint型のデータをxに読む */
    25    res = fread(&x, sizeof(int), 1, fp);  /* アドレス&xからint型のデータを
    26                                             一つfpで指されたファイル
    27                                             に書き込む */
    28    if(res == 0){
    29      printf("ファイルから読み込むことができませんでした\n");
    30      exit(1);
    31    }
    32  
    33    /* 読み込んだデータを表示 */
    34    printf("x = %d\n", x);
    35  
    36    /* ファイルからfloat型のデータをyに読む */
    37    res = fread(&y, sizeof(float), 1, fp); /* アドレス&xからfloat型のデータを
    38                                             一つfpで指されたファイル
    39                                             に書き込む */
    40    if(res == 0){
    41      printf("ファイルから読み込むことができませんでした\n");
    42      exit(1);
    43    }
    44  
    45    /* 読み込んだデータを表示 */
    46    printf("y = %f\n", y);
    47  
    48    /* ファイルからSAMPLE型のデータをzに読む */
    49    res = fread(&z, sizeof(SAMPLE), 1, fp); /* アドレス&xからSAMPLE型の
    50                                                データを一つfpで指された
    51                                                ファイルに書き込む */
    52    if(res == 0){
    53      printf("ファイルから読み込むことができませんでした\n");
    54      exit(1);
    55    }
    56  
    57    /* 読み込んだデータを表示 */
    58    printf("z.a = %d\n", z.a);
    59    printf("z.b = %f\n", z.b);
    60    printf("z.c = %s\n", z.c);
    61   
    62  }

こちらの例は、先ほどのプログラムで作成したtest.datファイルからfread関数を用いてデータを読み込み、画面に表示するプログラムです。test.datファイルにはint型、float型、SAMPLE型のデータが書き込まれているのですが、注意すべきことは書き込まれた順番どおりデータ型の順にファイルの先頭から読み込む必要があるということです。


テキストファイルとバイナリファイル

fwrite関数などを用いて、「文字列以外のデータ」を書き込まれたファイルをバイナリファイルと呼びます。これに対して、画面に表示可能な文字列のみからなるファイルをテキストファイルと呼びます。Windowsではバイナリファイルをオープンする際には、オープンモードに"b"を付け加える必要がありますが、LinuxやUNIX系のOSでは、これらのファイルの明確な違いはありません。


レポート課題6-2

氏名と5科目の点数を標準入力から読み込み、ファイル「gakusei.dat」に保存するプログラムkakikomiを作成せよ。但し、氏名は最大20文字とし、「Q」を入力した時点で終了するものとする。また、学生のデータは構造体を用いて管理せよ。

ファイル「gakusei.dat」から、保存されたデータを読み込み、平均、最高、最低点、全員の点数を表示するプログラムyomidashiを作成せよ。