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

ファイル処理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";

	//int型、int型配列、文字列をバイナリ形式で書き込み
    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];

	//int型、int型配列、文字列をバイナリ形式で読み取り
    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();
}

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

テキスト形式の場合はどのようなデータ型でも文字列に変換してから書き込みしていましたが、バイナリ形式の場合は「そのまま」、つまりメモリ上に保存されているままの形式で書き込みます。

保存されたファイル(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関数の第一引数bufferのデータ型はconst void*型となっています。
「const」は関数によって値が変更されないという意味ですが、「void」は「空」という意味です。
戻り値のない関数に「void型」を指定するのと同じです。

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

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

第三引数countは、第二引数の値を何個書き込むかを指定します。
つまり「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";

//以下でも動作はする
//ただし正常終了した場合の戻り値はすべて「1」となる

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関数

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

引数はfwrite関数と同じで、データが書き込まれるか読み取られるかが異なります。

ファイルからの読み取り時は、書き込んだ時と同じ変数を用意し、書き込んだ順番通りにデータを読み取ります。
これにより、例えばプログラムの設定などをファイルに保存しておき、次回の起動時に設定を復元する、などの使い方ができます。

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

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

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

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

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

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

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