● ファイルの読み書き

ファイルを読み書きする操作は、実験データの分析や計算結果を論文に 載せるための重要な技術である。 ここでは簡単な高水準ファイル入出力と呼ばれる機構の使い方を紹介する。

■ ファイルの開け閉め

まず最初に「ファイルを開く」と呼ばれる操作を行う。これはOSに対して ファイルの種々の面倒を見てもらうことを依頼する儀式である。 その操作は fopen() 関数で行う。この関数のprototypeは次の 通りである。
FILE* fopen( char* filename, char* mode );
第1引数にはファイルの名前を文字列として渡す (プログラムを実行しているディレクトリとファイルのディレクトリが異なる場合にはパスも含めて渡す。)。 第2引数にはファイルを読み込むのか書き込むのか等の mode を 文字列として渡す。 mode の与え方は以下の通りである。
mode 用途 開始位置 動作
"r" 読み込み用 先頭 ファイルが無ければ失敗
"r+" 読み書き両用 先頭 有ればそのままにする
"w" 書き込み用 先頭 ファイルが無ければ新設
"w+" 読み書き両用 先頭 有れば空にする
"a" 追加書き込み用 終端 ファイルが無ければ新設
"a+" 読み書き両用 終端 有ればそのままにする

fopen() 関数は FILE 型構造体のアドレスを戻り値にするので 対応するポインタ変数でこれを受け取る。このポインタ変数をstreamと呼ぶ。 この値が NULL の場合は、何らかの理由でファイルを開くことに 失敗している。以後のファイル操作はすべてこの値を元に行われる。 以下にファイルを書き込み用に開く操作の例を示す。

FILE* stream = fopen("datafile.dat","w");
if( stream == NULL ){
  perror("Can't open the file");
  exit(1);
}
ファイル操作の最後の操作である「ファイルを閉じる」と呼ばれる操作を 先に紹介する。これはOSにファイルの操作の後始末を依頼する儀式である。 その操作は fclose() 関数が行う。
fclose( stream );

■ ファイルの読み書き

ファイルを読み書きする関数の使い方は、画面(端末)へ読み書きする 関数の使い方とだいたい同じある。

fprintf() 関数は標準入出力の printf() 関数のファイル版 である。第1引数に書き込み先のstreamを渡し、以降は printf() 関数 と同じで第2引数に書式制御文字列と続く。

int n; double d;
fprintf( stream, "%d %lf\n", n, d );
fscanf() 関数は標準入出力の scanf() 関数のファイル版 である。第1引数に書き込み先のstreamを渡し、 以降は scanf() 関数と同じで第2引数に書式制御文字列と続く。
while( fscanf( stream, "%d %lf", &n, &d ) != EOF ){
  // do some disposals
}
この例はファイルの1行に整数と実数が並んであるとして、 1行ごとにファイルの最後まで読み込むループである。

fputs() 関数は渡された文字列をテキスト形式で 書き込む。単純に文字列を扱うのに向いている。

char buf[256]="Computer in Physics\n";
fputs( buf, stream );
出力する文字列に改行コードがなければ fputs() 関数は改行しない。 標準入出力の puts() 関数とは異なる。

fgets() 関数はテキスト形式のファイルの1行または指定文字数 までを文字列として読み込む。単純に文字列を扱うのに向いている。 fgets() 関数は1行の最後の改行も読み込み配列変数に一緒に格納する。

char buf[256];
while( fgets( buf, 256, stream ) != NULL ){
   // do some disposals
}
fgets() 関数は fscanf() 関数の書式制御では表せない複雑な データでも atoi(), atof() 関数と組んで 数値を読む込むことができる。例えば1行が以下の形式の データファイルで、2列目の整数と3列目の実数だけを読み込みたいとする。
    T  000  -100.62  -234.18  +086.04   96/12/16 13:23:51  th10-2-1
2列目の整数は行の先頭から7文字目の所から始まり、 3列目の実数は12文字目の所から始まる (C言語では文字列は先頭の文字を0文字目として数え始める。) ので次の様にする。
char buf[256];
int n; double d;
while( fgets( buf, 256, stream ) != NULL ){
  n = atoi( buf+7 ); 
  d = atof( buf+12 ); 
}
fwrite() 関数は数値も文字列もそのままの形で指定バイト数だけ ファイルに書き込む。配列変数のデータを書き込むのに大変便利である。
double data[256];
fwrite( data, sizeof(double), 256, stream );
この例では double 型のデータを一度に256個書き込む。 同様に構造体型の配列変数も扱える。

fread() 関数は数値も文字列もそのままの形で指定バイト数だけ ファイルから読み込む。配列変数のデータを読み込むのに大変便利である。

double data[256];
fread( data, sizeof(double), 256, stream );
この例では double 型のデータを一度に256個読み込む。 同様に構造体型の配列変数も扱える。

■ データファイルの異種マシンとの共有の問題

fwrite() 関数で2byte以上の型の値を書き込んだファイルを CPUの種類の異なるマシンで fread() 関数で読み込む場合 には、byte orderと呼ばれる形式がCPUによって違うことと、構造体 型変数の場合にはpropertyの構造体内での位置(offset address)が わずかに異なることが致命的な問題となる。 前者の問題はendianを適切に変換することで対処でき、 後者の問題は構造体の型のpropertyの位置を調整するため だけの新たなpropertyを付け加えれば対処できる。 その対処法を簡単に紹介する。

Byte orderとは2byte以上の基本型 ( short , int , long , float , double など) をメモリに格納する際の、変数の内部のbyteの順番とメモリのアドレスの 順番の対応のことである。変数の小さい方(下位)のbyteから先にメモリに 格納する方式を little endian と呼び、Intel系のCPUやSparcのCPUなどで 採用されている。逆に大きい方(上位)のbyteから先に格納する方式を big endian と呼び、MacのCPUやDEC AlphaのCPUなどで採用されている。

使用しているマシンのbyte orderは次の様にして簡単に調べられる。

int n=1;
if( *(char*)&n == 1 ) printf("Little endian\n"); else printf("Big endian\n");
int 型と double 型の変数のbyte orderをひっくり返す 関数 bswap(int) と bswap(double) は次の様になる。
inline void swap( char* p1, char* p2 ){
  char t = *p1; *p1 = *p2; *p2 = t;
}
inline void bswap( int& n ){
  swap( 0+(char*)&n, 3+(char*)&n );
  swap( 1+(char*)&n, 2+(char*)&n );
}
inline void bswap( double& n ){
  swap( 0+(char*)&n, 7+(char*)&n );
  swap( 1+(char*)&n, 6+(char*)&n );
  swap( 2+(char*)&n, 5+(char*)&n );
  swap( 3+(char*)&n, 4+(char*)&n );
}
他の型用の関数も引数の型を変えて swap() を適切に変えるだけ (C++では引数の型が異なる同名の関数を定義できる。) でできあがる (この目的にはC++のtemplateの機構を利用すると便利なのだが、 実行効率が著しく下がるのでこの目的には不向きである。) 。

次に構造体のpropertyの構造体内の位置(offset address)の 調整の仕方である。調整の前に自作した構造体の各propertyの offset addressを次のマクロを用いて調べる。

#define StructOffset(Object,Property) (&(((Object*)0)->Property))

struct Test
{
  char c;  int n;  double d;
};

printf("%d %d %d\n", StructOffset(Test,c), StructOffset(Test,n), StructOffset(Test,d) );
これを異種マシンで動かしてpropertyのoffset addressに変化がないなら 問題は無い。Intel系CPUではこの例の結果は 0 4 8 となり、 char のpropertyと int のpropertyの間に2byteの隙間があることが わかる。別のCPUでは 0 8 16 となるかもしれない。この場合には Intel系CPUでの構造体の定義を
struct Test
{
  char c; char dummy1[7];
  int n;  char dummy2[4];
  double d;
};
としてoffset addressと全体のサイズを他のCPUに合わせる調整をしてデータ を作り直すか、もしくは次の様にして double 型を int 型の 2要素の配列で扱う。さらに、endianにも注意する。
int n[2];
double d;
n[0] = *(int*)&d;    n[1] = *((int*)&d+1);     // write side
d    = *(double*)n;                            // read  side
結論として、異種マシンで共有するデータに構造体を用いるべきでない。

■ 標準入出力の利用

OSのredirectionの機能を用いると標準入力(streamはstdin)から ファイルを読み込むことができ、標準出力(streamはstdout)から ファイルを書き込むことができる。 つまりファイルを開く操作をせずに標準入出力関数の printf() や scanf() 関数でファイルを簡単に読み書きできるのである。

プログラムの実行時に、読み込むファイルと書き込むファイルを 次の様にして指定する。

   program < in.dat > out.dat
読み込みだけ、あるいは、書き込みだけの場合は、それぞれ 次の様にすれば良い。
   program < in.dat
   program > out.dat
これをredirectionと呼ぶ。ここでひとつ大事な注意がある。 redirectionで既存のファイルに出力する場合には、始めに そのファイルの内容が空にされてから書き込みが始まる。 従って次の様なことをしてはいけない。
   program < in.dat > in.dat
こうすると最初に in.dat の内容が空にされるので、 < in.dat としても何も読み込めない。

また既存ファイルへ追加書き込むをする場合には次の様にする。

   program < in.dat >> out.dat
なお、MS-Windowsにおいてバイナリデータをredirection で扱うには、標準入出力を次の様にしてバイナリモードにしなくては ならないことに注意しよう。
_setmode( _fileno(stdin),  _O_BINARY );
_setmode( _fileno(stdout), _O_BINARY );


  • 次へ
  • 目次
    Copyright(C) by Naoki Watanabe. Oct 21st, 1995.
    渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp