バイナリファイルの読み書き

ファイル処理5

バイナリファイルとは

今までのファイルの読み書きは、すべてテキストファイル形式のものでした。
テキストファイルはメモ帳などのテキストエディタで中身を読むことができます。

バイナリファイルは人間が読むものではなく、ソフトウェアに読み込ませるための形式のファイルです。
画像ファイルや音楽ファイルなど、人が直接読み書きする以外のファイルはすべてバイナリファイルです。
メモ帳などでバイナリ形式のファイルを開いてみてもよくわからない文字列が並んでいてまともに読むことはできません。

バイナリデータの読み書きのサンプル

バイナリファイルの書き込みにはfwrite関数、読み込みにはfread関数を使用します。

#include <stdio.h>

void Write(const char *file)
{
    FILE *fp;

    //fp = fopen(file, "wb");
    fopen_s(&fp, file, "wb");

    if (fp == NULL)
    {
        printf("%sのオープンに失敗しました。\n", file);
        return;
    }

    int integer = 10;
    int intArr[3] = { 1, 2, 3 };
    char str[4] = "ABC";

    fwrite(&integer, sizeof(int), 1, fp);
    fwrite(intArr, sizeof(int), 3, fp);
    fwrite(str, sizeof(char), 4, fp);

    fclose(fp);

    printf("%sに保存しました。\n", file);
}

void Read(const char *file)
{
    FILE *fp;

    //fp = fopen(file, "rb");
    fopen_s(&fp, file, "rb");

    if (fp == NULL)
    {
        printf("%sのオープンに失敗しました。\n", file);
        return;
    }

    int integer;
    int intArr[3];
    char str[4];

    fread(&integer, sizeof(int), 1, fp);
    fread(intArr, sizeof(int), 3, fp);
    fread(str, sizeof(char), 4, fp);

    fclose(fp);

    printf("%sの中身を表示。\n", file);

    printf("%d\n", integer);
    printf("%d、", intArr[0]);
    printf("%d、", intArr[1]);
    printf("%d\n", intArr[2]);
    printf("%s\n", str);
}

int main()
{
    const char *file = "C:\\test.txt";

    Write(file);
    Read(file);

    getchar();
}

このサンプルコードでは、変数の値を直接ファイルに書き込んで閉じ、それをすぐに読み込んで表示しています。
ファイルのオープンモードが「wb」「rb」と、バイナリモードになっていることに注目してください。

テキスト形式の場合はint型変数などは文字列に直してから書き込みしていましたが、バイナリ形式の場合は「そのまま」の形式で書き込みます。

保存されたファイル(C:\test.txt)は拡張子をテキスト形式(.txt)にしていますが、中身はバイナリ形式です。
そのため、メモ帳などのテキストエディタで開いてもまともに中身を読むことはできません。
バイナリファイルをテキストエディタで開いたところ

今回はファイルの拡張子を「.txt」で保存しましたが、これだとダブルクリックでファイルが開けてしまい、不用意に編集して保存してしまうおそれがあるので、「.dat」や「.bin」などで保存したほうが良いでしょう。
(dat=data、bin=binaryの略。特に名前のないバイナリデータに付けられる拡張子です)
空白の部分は何もデータがないように見えますが、文字として表示できないデータが書き込まれているので、テキストエディタで編集してしまうとデータが壊れます。

fwrite関数

size_t fwrite(
 const void *buffer,
 size_t size,
 size_t count,
 FILE *stream
);
ファイルストリームstreamに、大きさsizeのデータ型bufferをcount個書き込む。
戻り値は書き込まれた数。

fwrite関数の第一引数のデータ型は「const void *」型となっています。
「const」は関数によって値が変更されないという意味ですが、「void」は「空」という意味です。
戻り値のない関数に「void型」を指定するのと同じです。

「void *」とポインタ型にした場合は、データ型に依存しないポインタ、という意味になります。
つまりfwrite関数の第一引数は、int型でもchar型配列でも受け取れる、という意味です。
(ただしポインタを渡します)

第二引数は、書き込む値(第一引数)のデータ型のサイズ(バイト数)を指定します。
これはsizeof演算子にデータ型名を記述することで取得できます。

第三引数は、第二引数の値を何個書き込むかを指定します。
つまり「size × count = bufferのサイズ」となります。

int integer = 10;
int intArr[3] = { 1, 2, 3 };
char str[4] = "ABC";

//大きさsizeof(int)の値を1個書き込む
fwrite(&integer, sizeof(int), 1, fp);

//大きさsizeof(int)の値を3個書き込む
fwrite(intArr, sizeof(int), 3, fp);

//大きさsizeof(char)の値を4個書き込む
//4文字目はNULL文字
fwrite(str, sizeof(char), 4, fp);

最終的に第一引数の大きさが正しく表現できるのならば、第二、第三引数の書き方は滅茶苦茶でも(プログラムの実行上は)問題ありません。

int integer = 10;
int intArr[3] = { 1, 2, 3 };
char str[4] = "ABC";

fwrite(&integer, sizeof(int), 1, fp);
fwrite(intArr, sizeof(intArr), 1, fp);
fwrite(str, sizeof(str), 1, fp);

fwrite関数とfread関数の第一引数はデータ型に依存しないので、自分で定義した構造体であってもそのまま読み書きが可能です。

typedef struct
{
	char str[32];
	int num;
} TestStruct;

//~省略~

TestStruct ts = { 0 };
fwrite(&ts, sizeof(TestStruct), 1, fp);

fread関数

fread関数の使い方はfwrite関数とほとんど同じです。
データが書き込まれるか、読み取られるかの違いです。

size_t fread(
 const void *buffer,
 size_t size,
 size_t count,
 FILE *stream
);
ファイルストリームstreamから、大きさsizeのデータ型bufferをcount個読み取る。
戻り値は読み取られた数。

ファイルからの読み取り時は、書き込んだ時と同じ変数(配列)を用意し、書き込んだ順番通りにデータを読み取ります。

54~58行目で、ファイルから読み取ったデータが正しく復元できていることを確認しています。
今回は一度に書き込みと読み取りを行っていますが、例えばプログラムの設定などをファイルに保存しておき、次回の起動時に設定を復元する、などの使い方ができます。

OSによるバイナリファイルの互換性

あまり初心者向けの話ではないのでさらっと説明します。

例えばshort型変数は、内部的には2バイトのデータで表現されます。
その2バイトがどのような並び方で表現されるかはOS(というかCPU)によって異なります。
このデータの並びをバイトオーダーと言います。

short型変数に「1」を代入した場合、データは以下のように配置されます。
バイトオーダー
(本当はもう一つありますが滅多にないので無視します)

リトルエンディアンと呼ばれる並び方は、2バイトのうち先頭バイトからデータを詰めていきます。
ビッグエンディアンと呼ばれる並び方は、末尾バイトからデータを詰めていきます。

Windowsや今のMacOSではリトルエンディアンでデータが保存されます。
古いMacOS(PowerPC)ではビッグエンディアンでデータが保存されます。

つまり、バイトオーダーが異なるシステムで作られたバイナリファイルをfreadで読み込む場合、このことを考慮しないと正常なデータを得られません。