ファイルへの入出力とは,プログラム中でファイルからデータを読み取ったり,データをファイルに書き 出したりすることをいう. Cでファイルの入出力を行なうには,実際にファイルを利用する前に,ファイル をオープンし,利用が終ったらクローズしなくてはならない. ファイルをオープン (open)するとは,シス テム(OS)に対して,どのようなファイルをどのように利用するかを知らせることであり,ファイルをク
ローズ(close)するとは,ファイルの利用が終わったことをシステムに通知することである. これらの操作
には標準入出力関数と呼ばれる一連の関数を用いる. Cのプログラム中では入出力用のファイルをファイル ストリーム(file stream)とよび,一旦オープンされたファイルは入力用または出力用のバッファ(buffer) と呼ばれる領域を経由してデータがプログラムに渡されたり,プログラムからデータがOSに流れていく.
ファイルのオープンに成功すると,プログラムはOSからファイル記述子(file descripter)と呼ばれる 負でない整数を得ることができる98. 一方,ファイルをクローズするとは,実際にはその読み出し,書き込み などのためのファイル記述子を再利用可能な状態にし,書き込みを行ったときには,そのディスク領域を決 定するなどの操作が行われる.
実際にファイルのオープンやクローズをを行なうには,fopen,fclose関数を利用する99. fopen関数の 戻り値の型はFILE構造体へのポインタであり,そこにはファイル記述子の他,ファイルへのアクセスのた めのバッファなどのメンバーを持つ. FILE構造体は,標準ヘッダファイルstdio.hでその構造が定義され ている.
Example 6.19.1 実際,a.dataというファイルをオープンして,ファイルの内容をchar型変数として読 み込み,それをクローズするには以下のようにしておこなう.
FILE *fp ; int c ;
if ((fp = fopen("a.data","r")) != NULL) { while (!feof(fp)) {
c = fgetc(fp) ; putc(c,stdout) ; }
fclose(fp) ; }
ここで,fopenの第一引数は,ファイル名を表す文字列であり,第二引数は,ファイルをオープンするモー
ドを表す文字列である100. また, feof は, ファイルに対する現在の読みとり(書き込み)位置が, EOF
(ファイルの終端を表す特別な仮想的な文字)かどうかを判断する関数である.
ここで,fgetc関数の戻り値の型はchar ではなく,intであることに注意. もし戻り値の型がchar で あるならば,EOFを表す値(#define EOF (-1)と定義されていることが多い. )と実際の文字とを区別で きなくなるからである.
また,このファイル中ではFILE型へのポインタfpのための領域を確保していないが,この領域はfopen
98指定したファイルが存在しない,アクセス権がないなどの場合には,ファイルのオープンに失敗する. この場合,ファイル記述子 に対応する値として,負の値が戻ってくる仕様になっているものが多い.
99Cには「低レベル入出力」と呼ばれる関数群(UNIXの場合には,実際にはシステムコール)があり,それらはファイル記述子 を利用して,入出力を制御する.fopenやfcloseなどの標準ファイル入出力関数は,その内部で低レベル入出力システムコールを利 用している.
100この場合は,読みとり専用にオープンしている. 新規ファイルや,ファイルの内容を新しく書き込むためにはwを指定したりす る.詳しくはfopenのオンラインマニュアルを参照.
関数内で確保され,fclose関数内で開放される.
また,fopenは指定したファイルがオープンできない時にはNULLを返す. fopen以後は,ファイルへのア クセスは,このポインタを利用する.
また, ファイルに出力をする時には,fprintf 関数を利用することが多い. fprintfは, printf とほぼ 同様な利用法をする. すなわち,prinf関数で
printf(fmt, ....)
としたものを,ファイルポインタfpで示されるファイルに出力するには, fprintf(fp, fmt, ...)
とする. printf関数は標準出力stdoutへのfprintfを行っているに過ぎない.
これまでに利用してきた標準入出力も,ファイルとして定義されている. それらは,stdio.hに以下のよ うに定義されている.
• stdin: 標準入力(ファイル記述子0).
• stdout: 標準出力(ファイル記述子1).
• stderr: 標準エラー出力(ファイル記述子2).
これらは,fopenなどを利用せずに使うことができる.
ファイルへの入出力を行なう関数としては, fread, fwrite がある. fprintf がテキストのみを出力す る関数であるのに対して,fwrite はどのようなデータでも出力することができる. 一般に,ファイルから データを読みとる時には,freadを使うことが多い. どのような形式で格納されているわからないデータを 読むにはfread を使い,次のようにする.
Example 6.19.2 a.dataというファイルの内容を文字列としてとるには以下のようにしておこなう.
FILE *fp ; char buf[80] ;
if ((fp = fopen("a.data","r")) != NULL) { while (!feof(fp)) {
if (fread(buf,1,80,fp)!=0) fprintf(stdout,"%s",buf) ; bzero(buf,80) ;
}
fclose(fp) ; }
ここで,freadの第一引数は,データを格納する領域,第二引数は,読みとるデータの数,第三引数は最大
どれだけのデータを読みとるかである. この時,bufを再利用するため,出力した後にはbufの中をbzero 関数でクリアしている.
また,読み込むものが文字列とわかっている時には, 関数fgets を利用するのが望ましい101.
101詳しくは,man 3 gets参照.
Example 6.19.3 実際,a.dataというファイルの内容を文字列としてとるには以下のようにしておこなう.
FILE *fp ; char buf[80] ;
if ((fp = fopen("a.data","r")) != NULL) { while (!feof(fp)) {
if (fgets(buf,80,fp) != NULL) { fprintf(stdout,"%s",buf) ; }
fclose(fp) ; }
fread 関数ではストリーム内にあるデータを, その値が何であっても指定のバイト数だけ読み込みを行う
のに対して, fgets関数は, 指定のバイト数だけの文字列を改行文字まで読み込みを行う. すなわち,現在 のファイルポインタ (file pointer)の位置(読み込みを行っているファイル内での位置)から指定された 文字数(上の例では80文字)以内に改行文字があれば,そこで読み込みを中断する. なお, fgetsに良く 似たgets という関数もあるが, こちらは, 読み込みの最大データ量を指定できないため, 読み込み領域を 越えて読み込みが行われ,データが破壊される原因となるので使わない方がよい102. したがって,テキスト データの場合の読み込みにはfgets関数が適しているが,バイナリファイルはfread関数で読み込む必要
がある. 同様に,fprintf関数で書き出しを行うとテキストデータしか出力は出来ないが,fwrite 関数で
は, データそのものが書き出される.
Example 6.19.4 b.out というファイルに配列の内容をテキスト形式で書き出す.
FILE *fp ;
int a[10] = {0,1,2,3,4,5,6,7,8,9} ; int i ;
FILE *fp ;
int a[10] = {0,1,2,3,4,5,6,7,8,9} ; int i ;
if ((fp = fopen("b.out", "w")) != NULL) {
for(i=0;i<10;i++) fprintf(fp, "%d = %d\n", i, a[i]) ; fclose(fp) ;
} }
このプログラムの出力結果は, 0 = 0
1 = 1 2 = 2
102UNIXのいくつかのアプリケーションに見られる, “buffer overflow”によるセキュリティホールは,fgetsを利用すべきところ で,不用意にgetsを利用したことに起因するものがある.
3 = 3 4 = 4 5 = 5 6 = 6 7 = 7 8 = 8 9 = 9
となる. この出力ファイルのように,fprintf関数を用いて出力されたテキストファイルは, fscanf関数 で読み出すことが出来る.
FILE *fp ; int a[10] ; int i ;
if ((fp = fopen("b.out", "r")) != NULL) {
for(i=0;i<10;i++) fscanf(fp, "%d = %d", &i, &a[i]) ; fclose(fp) ;
for(i=0;i<10;i++) fprintf(stdout, "%d = %d\n", i, a[i]) ; }
fscanf, scanf関数を使うのは,fprintf, printf関数で定型のフォーマットで書き出したファイルを読 む場合だけである.
Example 6.19.5 b.out というファイルに配列の内容をバイナリ形式で書き出す.
FILE *fp ;
int a[10] = {0,1,2,3,4,5,6,7,8,9} ; if ((fp = fopen("b.out", "w")) != NULL) {
fwrite((int *)a, sizeof(int), sizeof(a)/sizeof(a[0]), fp) ; fclose(fp) ;
}
このプログラムの出力結果をodコマンドで, od -xとして見てみると, 0000000 0000 0000 0000 0001 0000 0002 0000 0003
0000020 0000 0004 0000 0005 0000 0006 0000 0007 0000040 0000 0008 0000 0009
となり,4バイトの整数値が順に書かれていることがわかる. このようにfwrite関数で書き出したバイナ リファイルは,fread 関数で読み出す.
FILE *fp ; int a[10] ; int i ;
if ((fp = fopen("b.out", "r")) != NULL) {
fread((int *)a, sizeof(int), sizeof(a)/sizeof(a[0]), fp) ; fclose(fp) ;
}
for(i=0;i<10;i++) printf("%d\n", a[i]);
上のint型の数値をfwrite関数で書き出した結果を, 例えばchar型で読み出すと,すなわち, FILE *fp ;
char a[10] ; int i ;
if ((fp = fopen("b.out", "r")) != NULL) {
fread((char *)a, sizeof(char), sizeof(a)/sizeof(a[0]), fp) ; fclose(fp) ;
}
for(i=0;i<10;i++) printf("%d\n", a[i]);
として読み出すと,a[0] から順に 0 0 0 0 0 0 0 1 0 0
という値が入力される. これは,int型が4バイトで, little endieanで書かれているため,a[0] からa[3]
までが intの 0を読み, a[4]から a[7]までが intの1を読んだ結果である. このように, バイナリで 出力した場合には,どのような型で,どのような順序(little endieanか big endieanか)で出力したかを管 理しなければならない.
Example 6.19.6 プログラムの第一引数に与えられたファイル名を持つファイルから,第二引数に与えら
れたファイル名を持つファイルにデータをコピーする.
#include <stdio.h>
int main(int argc, char **argv) {
FILE *in, *out ; char buf[1024] ; size_t len ; if (argc < 2) {
fprintf(stderr,"Usage: %s inputfile outputfile\n", argv[0]) ; exit(-1) ;
}
if ((in = fopen(argv[1], "r")) == NULL) {
fprintf(stderr,"Could not open file %s\n", argv[1]) ; exit(-1) ;
}
if ((out = fopen(argv[2], "w")) == NULL) {
fprintf(stderr,"Could not open file %s\n", argv[2]) ;
exit(-1) ; }
while((!feof(in))
&&(len = fread((char *)buf, sizeof(char), sizeof(buf)/sizeof(buf[0]), in))) { fwrite((char *)buf, sizeof(char), len, out) ;
}
return 0 ; }
このプログラムでは,第二引数に与えられたファイルはwでオープンしているため,既存のファイルがあっ ても,それを上書きする103.
6.19.1 演習問題
Exercise 6.19.1 標準入力から入力されたファイルの行数を印字するプログラムを書け.
Exercise 6.19.2 標準入力から入力されたファイルの行数,単語の数,文字数を印字するプログラムを書け.
Exercise 6.19.3 標準入力から入力されたファイルの中の,数字,空白,その他の文字の出現頻度を数える プログラムを書け. ただし,空白とは,空白文字,タブ,改行の3種である.
Exercise 6.19.4 ファイルもしくは標準入力から, 空白で区切られた学籍番号, ID,氏名の組を読み込み, それらをリスト形式として格納し,標準出力もしくはファイルにに以下のフォーマットで出力するプログラ ムを書け.
学籍番号: xxxxx
ID : xxxxx
氏名 : xxxxx
その際,このプログラムを起動する時に与えた引数が読み込みもしくは書き込みのファイルとなるようにし なさい. 具体的には,以下の通り. (実行コード名をprogとした).
% prog # このときには, 入出力とも標準入出力.
% prog - - # このときには, 入出力とも標準入出力.
% prog infile # このときには, 入力はファイル, 出力は標準出力.
% prog infile - # このときには, 入力はファイル, 出力は標準出力.
% prog infile outfile # このときには, 入出力ともファイル.
Exercise 6.19.5 ファイルもしくは標準入力から入力されたテキストファイルの行数, 文字数を標準出力 に出力するプログラムを書け. その際,このプログラムを起動する時に与えた引数が読み込みもしくは書き 込みのファイルとなるようにしなさい. 具体的には,以下の通り. (実行コード名をprogとした).
% prog # このときには, 標準入力.
% prog - # このときには, 標準入力.
% prog infile # このときには, 入力はファイル.
103もし,上書きをしたくない場合.すなわち,同じファイル名(パス名)をもつファイルが存在する場合に何かの警告を出したいと きには,statシステムコールなどで,そのパス名に対応するファイルの情報を得る必要がある.