教科書9章ファイル処理
プログラムの外側との入出力(データの交換)をどのように扱うか?
C言語では入出力を,一列に並んだデータの流れ(stream)とみなせる場合,FILEと呼ぶ共通の形式で扱う標準関数を用意している。
[要点]
ハードディスクドライブ(HDD)やUSBメモリー、あるいは磁気テープのような補助記憶装置にデータを格納するとき、ファイルと呼ばれるデータ構造が使われます。
ファイル(file)は、ファイル名で識別される一列にならんだデータで、データ数は可変です。
ファイルからデータを読み取る場合、ファイルの先頭データから順番に一個づつしか読み込めません。書き込むときもファイルの末尾に1個づつしか書き込めません。この特性から、ファイルの読み書きは一列に並んだデータの流れストリーム(stream)となります。
計算機の主記憶はアドレスを指定して任意の場所のデータを読み書きできますが、
ファイルの読み書きはストリームを介しての読み書きに制限されます。
C言語では ストリームで入出力を行う対象を全てファイルとして扱います。
例えば、標準入力や標準出力もファイルとして扱われます。下図の様に、キーボード入力や端末画面への出力もストリームを介して読み書きするのでファイルの一種として扱います。
補助記憶装置は数ギガバイトのUSBメモりー、数テラ(1012 or 240 )バイト程度の容量を持つハードディスク(HDD)など様々なものがあります。電源無しでデータを維持できるのが普通で、フロッピーディスク、MOディスク、DVD-Rなど記憶媒体を装置から取り出して持ち運べる物も存在します。
補助記憶装置はその物理的構造からデータの並び順に従ってなら高速にデータを参照できます。
>>補助記憶装置は連続した場所のデータ参照は高速に行える。
しかし、記憶場所がとびとびの場合はデータの頭出しに時間がかかり、データ参照は主記憶装置に比べて桁違いに低速になります。HDDは磁気ディスク(円盤)に同心円状にデータを記憶し、磁気ヘッドで読み書きを行う。データを連続して読み出す速度は数十Mバイト/秒も可能です。
ところが、磁気ヘッドの移動は 十ms程度は必要で す。主記憶用のRAMの参照が十ns程度で何所の番地でもアクセスできることを考えるとこの差は100万倍近い大きなものです。
>>HDDのデータの読み取り時間は頭出しに時間がかかり、主記憶に比べるると6桁も遅い。
データを順番に一列に並べたファイルは補助記憶装置に適した形式です。短いファイルなら頭出しが1回で済み、以後は連続したデータを読み書きするだけで済みます。
補助記憶装置の応答速度が一様でなく、場合によっては極端に遅くなるため、データの読み書きのたびにHDDを読み書きするのは効率がとても悪くなります。主記憶のRAMに比べて100万倍遅くなることもあります。
データを送る側と受る側の速度が違う問題は補助記憶装置だけでなくキー入力や画面への出力、通信といった場面で一般に発生し、ストリームを実現するする上で解決が必要な問題です。
読み書き速度が違いすぎる問題を解決するためによく使われるのがバッファ(buffer)です。バッファは送られてくるデータを一時的に溜める場所で、受取側は自分のペースでバッファからデータを取り出せばいい事になります。HDDへの書き込みの場合、プログラムは主記憶に置かれたバッファにデータを書き込み、バッファのデータがある程度溜まったところで、 HDDへまとまった連続データとして書き込 みます。HDDからの読み出しも連続データとしてバッファに読み込んでおいて、プログラムからはバッファのデータを 必要な部分だけ利用することにすれば処理は高速に行えます。
ファイルはファイル名(file name)を使って区別します。ファイルの管理は計算機のオペレーティングシステム(OS)の重要な役目です。
ファイルの数が増えると、ファイル名の衝突が増えてきます。そこでUNIXやWindowsなど多くのOSでディレクトリ階層が用意されています。ディレクトリ(directory)はファイルや、他のディレクトリを入れる入れ物でディレクトリ名を持ちます。
Windowsではディレクトリと言わずにフォルダFolderと呼んでいます。
ファイルを識別するときの名前はドライブとディレクトリを含めたフルパス名で行うことで重複の可能性を減らします。例えば上の図で、ファイルCalc.exeはC:\WINNT\System32\Calc.exe で識別され、同じ名前のCalc.exeが別のディレクトリに置かれていても、ディレクトリ名の違いで別のファイルとして識別できます。
[目次]
ファイルとの入出力の方法を紹介しますが、序論ではテキストファイルのみを説明します。
教科書のポインタや構造体を飛ばしたのでわかりにくい点もあるかと思いますが、まずは一通り読んでみてください。
テキストファイル(text file)はをテキストを格納するファイルで、中身は文字コードが順番に並んだものです。
改行コードなども文字として含んだ文字列をテキスト(text)と呼びます。テキストは最もシンプルな形の文書データで文字飾りやフォント指定を含まないことを強調 してplain textと言うこともあります。
※バイナリーファイルは機械語プログラムや画像データなど、文字コードではない値を格納したファイルです。文字コード表に無い値を含んでいることもあるので、テキストとして読めません。
ファイルの読み書きはstdio.h内に宣言されたデータ構造 FILE を使って行われます。FILEはバッファ等のデータがまとめられたデータ構造で、通常の利用ではその構造を知る必要はありません。
stdio.h内にはファイルを処理する為の標準関数も宣言されています。これらの関数はFILEのアドレスを受け取って処理を実行する形に書かれてい ます。
FILEのアドレスを記憶するデータ型をファイルポインタと呼びます。このデータ型はFILE*と記述します。
ファイルポインタ型の変数a,bの定義は次のように行います。
FILE *a, *b;
このようなファイルポインタを使う主な関数をここで紹介します。
序論演習Iではポインタの説明をしていないの、ここで簡単ですが少し説明しておきます。
C言語にはメモリーアドレスを記憶するデータ型:ポインタがあります。
ファイルポインタpは次のように定義します。
FILE *p;
ここで、変数pは、FILEの置かれたメモリ・アドレスを記憶する変数です。
ファイルを扱う関数はファイルポインタの値を引数で受け取ります。関数は受け取ったアドレスからファイルの処理に必要なデータにアクセスできます。
※「1.計算機とプログラム」で紹介したように、計算機の記憶装置はアドレス順に記憶素子を並べたものです。記憶装置にデータを記憶しますが、その記憶場所はアドレスで区別します。ポインタ型は、このアドレスを記憶する変数として用意された仕組みです(教科書7章)。
※int *a;のように整数データのアドレスを記憶する変数aを定義すると、
a[0]=10;あるいは
*a=10;のように書いて、そのアドレスへの整数値10の代入を命令できます。
FILEのデータ領域を確保し初期化する関数
● FILE *fopen(const cahr *fname, const char *mode);
ファイルを処理するFILE領域の確保と初期化を行い、そのファイルポインタを戻す。旨く行かない場合はNULLを戻す。
※読もうとしたファイルが無いなどで,開けない場合もある。fopenがNULLを返した場合には,エラー処理に分岐するプログラムが望ましい。
第1引数fnameはファイル名、第2引数modeはファイルの利用形態を示す文字列です。modeに指定できる文字列の例を以下に示します。
例:
FILE *f; if( (f = fopen("ABC.txt","r") ) == NULL) { /*ファイル「」を開けないときにここに来る*/ printf("ファイルは開くことができません"); }
※標準入出力もFileとして扱われるので、プログラムの起動時にファイルとして開かれます。そのファイルポインタが stdin、stdout の名前で使えます。
※ バイナリファイルについては教科書など適当な参考書を見てください。
ファイルを読み書きする場合、ファイルを開いて、読み書きして、閉じるの処理を全て行わないと正しく動作しない。複数の関数を組み合わせて使うことになるのが煩わしいが、このような関数の使い方も多いので慣れてほしい。
#include<stdio.h> int main(void) { char s[256]; FILE *output; output=fopen("a.txt","a");/*追記モードでファイルを開く*/ if(output==NULL)return 0;/* a.txtのファイルが開けない場合は終了 */ while(1) { printf(">");fflush(stdout); if( scanf("%255s",s) != 1 )break; fprintf(output,"%s\n",s);/*ファイルのバッファへ書き出し*/ } fclose(output);/*バッファをフラッシュしファイルを閉じる。*/ return 0; }
● int fgetc(FILE *stream);
● int getc(FILE *stream);
getcharと同様な関数。streamから1バイト読み出しint型の値に拡張して戻す。
ファイルの終端やエラーの発生ではEOF(End
Of File)を戻す。
※読み取るのは文字コードの値です。文字が’1’だからといって数値1が戻されるわけではない。
例:
FILE *f;
int c;
if( (f=fopen("ABC.txt","r")) ==NULL)
{
printf("ファイルは開くことができません");
}
c=fgetc(f);
putchar(c);
● int fputc(int c,FILE *stream);
● int putc(int c,FILE *stream);
putcharと同様な関数。streamに一文字書き出し、書き出した値をint型の値に変換して戻す。
エラーの発生ではEOF(End
Of File)を戻す
例:
FILE *f;
int c;
if( (f=fopen("ABC.txt","w")) ==NULL)
{
printf("ファイルは開くことができません");
}
c=getchar();
fputc(c,f);
printf や sacnfと同様な関数。テキストファイルにデータを読み書きするのに便利
● int fprintf(FILE *stream, const char* format,...);
例:
FILE *f;
if( (f=fopen("ABC.txt","w")) ==NULL)
{
printf("ファイルは開くことができません");
}
fprintf(f,"Hello World\n");
● int fscanf(FILE *stream, const char* format,...);
例:
FILE *f; int n; if( (f=fopen("ABC.txt","r")) ==NULL) { printf("ファイルは開くことができません"); } fscanf(f,"%d",&n)
※fscanf はscanf 同様に読み込む値の書式と、データを格納するためのメモリー領域との整合性に注意する必要がある。
※文字列を対象にした良く似た関数として
int sprintf(char *s, const char* format,...);文字配列sに書き出す
int sscanf(const char *s, const char* format,...);文字列sから読み込む
がある。
文字列の読み書きはfscanfやfprintfで書式指定子%sを使ってが行えるが、ここでは文字列専用の関数を紹介する。
● char* fgets(char*s, int n, FILE *stream);
streamから改行か終端までの文字列をポインタsで示した番地以降に読み込 んで文字列終端の'\0'を追加する。末尾の改行コードも読み込んだ文字列に含んだり含まなかったりするので注意(処理系依存)
この際に、2番目の引数nで示した文字数を上限とし最大でもn-1文字までしか読み込まない。n-1なのは文字列の終端\0が1文字分あるため。正常に処理が行われればsをそのまま戻すが、エラーの発生ではNULLを戻す。
※標準入力から1行の文字列を読み取る類似の関数にgetsがある。getsは1行の文字列を読み込んだ場合に最後の改行文字を省く点と読み込むバイト数が制限できない点で、fgets関数と動作が異なる。
※関数getsはバッファーオーバーランの危険性が大なので使用は避けること。
● int fputs(char*s, FILE *stream);
streamに文字列sのデータを書きだす。書き出した文字数を戻すが、エラーの発生ではEOFを戻す
※標準出力に文字列を書き出す、同様の関数にputsがある。
出力ストリームのバッファからデータを全て送り出し、バッファを空にする処理
● int fflush(FILE *stream);
正常に処理できれば0を戻す。異常が生じた場合はEOFを戻す。streamがNULLの場合は全出力ストリームをフラッシュする。streamが入力ストリームの場合は動作は不定。
標準出力をフラッシュする場合は fflush(stdout); のように命令する。
int n; printf("年齢を入力してください>"); fflush(stdout); scanf("%d",&n);
FILEのデータ領域を解放する関数fclose
● int fclose(FILE *stream);
streamが出力の場合はバッファのデータをフラッシュする。 入力の場合はまだ読まれていないデータを捨てる。
streamが指すFILE領域を解放しファイルを閉じる。うまくいけば0を戻し、異常があればEOFを戻す。
例:
FILE *f; if( (f=fopen("ABC.txt","w")) ==NULL) { printf("ファイルは開くことができません"); } fprintf(f,"Hello World\n"); if(fclose(f)==EOF) { printf("ファイルを閉じる際にエラーが発生しました\n"); }
実は、プログラムが開始された時点で既に開かれているテキストファイルがある。これらへのファイルポインタ stdin, stdout, stderr はstdio.h内で宣言され、プログラムの初期化時点で開かれ、終了時点で閉じられる。従って、プログラム中でfclose(stdin)などは行ってはいけない。
※VisualStadioでのコンパイル結果を見てみると,stdin,stdout,stderrなどは変数やリテラルではなく,前処理でFILE*型の値を戻す関数呼び出しに置き換えられていた。
標準入力:デフォルトでは端末からの入力ストリーム。
端末キーボードからの入力は確認のために端末上に表示されて行くが、この状態ではまだ標準入力には渡されない。エンター・キーが押された時に、初めて端末に表示されている文字列が標準入力に渡される。
ファイルからの読込関数でファイルポインタにstdinを用いると、これまで紹介した標準入力の読込関数getcharやscanfと同じ働きをする。
getc(stdin) と getchar( ) は同じ働きをする
fscanf(stdin,-----)と scanf(-----)は同じ働きをする
標準入力から行末までの1行を読込に使う関数 gets があります。しかし、ここまで紹介しませんでした。
scanf はスペース文字などを区切りとして認識するため1行単位での読み込みには使えません。getsは必要な関数です。しかし、読み込むバイト数の指定が無いため文字列を入れるために用意したメモリーの範囲を超えてメモリーに書き込むバッファオーバーランの危険性があります。
ファイルから1行読込む関数fgetsがあります。この関数は読む文字数を制限できるので、getsの代わりにfgetsにstdinを渡して使うのが安全です。
セキュリティが必要なプログラムではgetsを使ってはいけない。
標準入力stdinを指定してfgetsを用い、読み込む文字数を制限すること。
#include<stdio.h> int main(void) { char txt[1000]; if(gets(txt)!=NULL)/*改行か終端までを読み込む、バッファオーバーランの可能性あり*/ printf("%s",txt); if( fgets(txt,1000,stdin)!=NULL)/*制限範囲内で改行か終端までを読み込む*/ printf("%s",txt); if( scanf("%999s",txt)==1)/*999文字以内で区切り文字までを読み込む*/ printf("%s",txt); return 0; }
※fgetsはgetsと異なり行末の改行コードまで読み込むので注意
標準出力:デフォルトでは端末画面へのテキスト出力ストリーム。
※ 出力先がコマンドプロンプトの場合はバッファに書き込まれたものは直ちにコマンドプロンプトに送り出されて画面に表示される。しかし、HDD上のファイルの場合、データ量がバッファサイズよりも小さければ元のプログラムが終了し、出力内容が全て確定した後にまとめて書き出される。
会話的な処理ではこまめに書き出すことが必要なのでfflush(stdout);をプログラムに書いてフラッシュする必要がある。
putcやfprintfでファイルポインタにstdoutを指定すると、これまで使ってきたputcharやprintfと同じ働きをする
putc(c,stdout) とputchar(c)は同じ働きをする
fprintf(stdout,-----) とprintf(-----)は同じ働きをする
標準エラー出力:実行時エラーなどのメッセージをstdoutとは区別して書き出せる様に別のストリームとして用意されている。stdoutをリダイレクションで他のファイルに書き出している場合でもstderrは画面に出力 するなどの使い分けが可能。
HDD上のテキストファイルを行番号付きで画面出力するプログラム
例題
/*テキストファイルを読んで行番号付きで表示するプログラム*/ #include<stdio.h> char *fileName="a.c";/*テキストファイル名*/ int main(int argc,char *args[]){ FILE *input; int num,c; if(1<argc)fileName=args[1];/*コマンドラインにファイル名が指定されている場合*/ /*------------ファイルを開く------------*/ input=fopen(fileName,"r");/*名前がfileNameで読み込み専用ファイル*/ if(input==NULL){ fprintf(stderr,"ファイル%sを開けません",fileName); return -1;/*戻り値-1でプログラムを終了する*/ } /*------------ファイルを1字づつ読む------------*/ num=1; printf("%4d: ",num);/*1行目の行番号を書く*/ while(1){ c=fgetc(input); if(c==EOF)break;/*ファイルの終端でwhileループを抜ける*/ putchar(c); if(c=='\n'){/*改行の後に行番号を書く*/ num++; printf("%4d: ",num); } } /*------------ファイルを閉じる------------*/ fclose(input); return 0; }
[実行結果]
1: /*テキストファイルを読んで行番号付きで表示するプログラム*/ 2: #include<stdio.h> 3: char *fileName="p12.c";/*テキストファイル名*/ 4: int main(int argc,char *args[]){ 5: FILE *input; 6: int num,c; 7: if(1<argc)fileName=args[1];/*コマンドラインにファイル名が指定されている場合*/ 8: /*------------ファイルを開く------------*/ 9: input=fopen(fileName,"r");/*名前がfileNameで読み込み専用ファイル*/ 10: if(input==NULL){ 11: fprintf(stderr,"ファイル%sを開けません",fileName); 12: return -1;/*戻り値-1でプログラムを終了する*/ 13: } 14: /*------------ファイルを1字づつ読む------------*/ 15: num=1; 16: printf("%4d: ",num);/*1行目の行番号を書く*/ 17: while(1){ 18: c=fgetc(input); 19: if(c==EOF)break;/*ファイルの終端でwhileループを抜ける*/ 20: putchar(c); 21: if(c=='\n'){/*改行の後に行番号を書く*/ 22: num++; 23: printf("%4d: ",num); 24: } 25: } 26: /*------------ファイルを閉じる------------*/ 27: fclose(input); 28: return 0; 29: } 30:
★こちらの不手際がありましたので,今年度の課題からは省きます。
(2017/7/14課題内容は本来の形に修正済み。試しに挑戦してみても構いません。送信はできますが,この課題の評価は行いません。)
テキストファイル「p12a.txt」と「p12b.txt」から複数の整数値を読み込んで平均値(実数)を計算するプログラムを完成させてください。整数値は10進数で表記され、数値の区切りはスペースや改行となっています
p12a.txtの初めの部分は次のようなものです
81 45 88 51 88 87 92 88 92 90 88 91 93 78 79 90 89 93 92 88 ......
注意:
実行するレポートツールReport.jarと課題ファイルp12.cとp12a.txt
とp12b.txt をを同じ場所に置いてレポートツールを実行することが課題送信では必要です。
ヒント1:
テキストファイルからの数値の読み込みはfscanfを用いるといいでしょう。データの終端はfscanfがEOFを戻すことで検出できます。
ヒント2:
データ数は不定なので「9. プログラムの作り方」の9-2-2節の方法を参考にしてください