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

ファイル処理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();
}

このサンプルコードでは、変数の値を直接ファイルに書き込んで閉じ、それをすぐに読み込んで表示しています。
fopen関数(fopen_s関数)ファイルのオープンモードが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*とポインタ型にした場合は、データ型に依存しないポインタ、という意味になります。
つまりfwrite関数の第一引数は、int型でもchar型配列でも受け取れる、という意味です。
(ただしポインタを渡します)

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

第三引数countは、第二引数の値を何個書き込むかを指定します。
つまり「size × count = bufferのサイズ」となります。
もちろん必要ならば、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(
 void *buffer,
 size_t size,
 size_t count,
 FILE *stream
);
ファイルストリームstreamから、大きさsizeのデータ型bufferをcount個読み取る。
戻り値は読み取られた数。

引数はfwrite関数と同じで、第一引数のデータがファイルに書き込まれるか、読み取ったデータが格納されるかが異なります。

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

バイトオーダー

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

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

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

リトルエンディアンと呼ばれる並び方は、「00 01」というデータの並びがある場合、末尾バイトからデータを配置します。
ビッグエンディアンと呼ばれる並び方は、先頭バイトからデータを配置します。

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

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