構造体データのファイルの読み書き

C 言語の入門者向けの書籍ではファイルの書き込み、読み込みの例としては、「ファイルへの文字列の読み書き」で紹介したように、 文字や数字を書き出したり、読み込んだりする例が多いと思います。

ここではバイナリデータのファイルへの読み書きの例として、構造体のデータをファイルに保存したり、ファイルから読み込む例を示します。

構造体データをバイナリーデータとしてファイルに書き出す

具体例として、「typedef による構造体のユーザー定義型の宣言方法」で作成した PERSON 型のデータを使いましょう。

person.h は次の内容です。

#ifndef PERSON_H_
#define PERSON_H_

typedef struct _PERSON {
  char name[40];
  int age;
} PERSON, *PPERSON;

#endif /* PERSON_H_ */

PERSON 型のデータのインスタンスをひとつ作り、それをファイルに保存します。テキストとして書き出すのではなく、メモリブロックをそのまま出力するところがポイントです。

ソースコードは次のようにします。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "person.h"

int main() {
  FILE *fp = NULL;
  PERSON p;

  memset(p.name, 0xff, sizeof(p.name));
  snprintf(p.name, sizeof(p.name), "Hanako Yamda");
  p.age = 20;

  if ((fp = fopen("foo.dat", "wb")) == NULL) {
    perror(NULL);
    return 1;
  }

  fwrite(&p, sizeof(PERSON), 1, fp);

  fclose(fp);
  return 0;
}

PERSON 型の変数 p をひとつ作成しています。

ここでは name という char 型のバッファを memset 関数を用いて 0xff で初期化してから、名前をセットしています。 age メンバーは数字の 20 をセットしています。

さて、このデータをファイルに保存します。

fopen関数にて、書き出すファイル名は foo.datとし、 wb モードで開きます。 wb は、書き込みモード (w) かつバイナリモード (b) の意味です。

ファイルを開いたら、fwrite 関数でデータを書き出します。

  fwrite(&p, sizeof(PERSON), 1, fp);

&p で構造体があるメモリブロックへの参照を指定して、サイズにして size(PERSON) バイト分のデータを 1 個、ファイルに書いています。

さて、これを実行すればファイルにデータが出力できているはずです。

このように、メモリブロックのどの部分からどれだけファイルに出力するか、ということを指定すればメモリの内容がそのままファイルに出力されます。

バイナリファイルをみる

上のように出力すると、バイナリーデータとしてデータを出力したことになります。バイナリデータなので、 テキストエディタで開いても、基本的にはそのまま読むことはできません。

試しに、上で作成したバイナリファイル foo.dat をテキストエディタ vi で開くと次のようになりました。

バイナリデータの内容を確認するには、バイナリエディタ等のツールを使います。Linux では xxd コマンドなどで、内容をみることができます。

cat foo.dat | xxd
00000000: 4861 6e61 6b6f 2059 616d 6461 00ff ffff  Hanako Yamda....
00000010: ffff ffff ffff ffff ffff ffff ffff ffff  ................
00000020: ffff ffff ffff ffff 1400 0000            ............

こうして HEX としてデータを見ると、確かに 0xff でバッファが初期化されて、それがそのまま保存されたことがわかります。

データの最後の 0x14 は 10 進数で 20 です。これは上の age として保存した 20 ですね。

バイナリーデータを読み込む

次は上で作成したバイナリファイル foo.dat から、構造体にデータを読み込んでみましょう。

ソースコードは次の通りです。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "person.h"

int main() {
  FILE *fp = NULL;
  PERSON p;

  if ((fp = fopen("foo.dat", "rb")) == NULL) {
    perror(NULL);
    return 1;
  }

  fread(&p, sizeof(PERSON), 1, fp);
  fclose(fp);

  printf("%s (%d)\n", p.name, p.age);

  return 0;
}

fopen関数にてファイル名 foo.datrb モードで開きます。 rb は、読み込み (w) かつバイナリモード (b) の意味です。

ファイルを開いたら、fread 関数でデータを読み込み、最後に本当に読み込めているか、printf 関数で出力しています。

これを実行すると次が表示され、確かにファイルからデータが取りこめていることが確認できます。

Hanako Yamda (20)

ここでみたように、どこのメモリにどれだけファイルから読み込むか、ということを指定すれば、 ファイルの内容がそのままメモリに取り込まれます。

なお、ファイルの先頭にデータを書き込んだり、読み込んだりしない場合は、fseek 関数でオフセットを指定します。

バイナリーデータは、テキストエディタで文字として直接見ることができないので、一見ややこしそうですが、 プログラムからは比較的素直に操作することができます。

よりポータブルなデータにするために

ここで紹介した例では、メモリのデータ表現をそのままファイルに保存しています。 このためデータファイルをバイトオーダー違いの環境に持っていったり、 データサイズが異なると正常動作しないはずです。

どこに持っていっても正常動作する、よりポータブルなデータフォーマットとするには、もう少し注意が必要であることは指摘しておきます。