C言語編 第42章 バイナリファイルの読み書き

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

テキストファイルとの違い

この章では、バイナリファイルを扱います。

前章までに扱ってきたテキストファイルは、文字だけで構成されたファイルでした。 これに対してバイナリファイルは、数値の羅列として表現されています。

バイナリファイルは汎用的なデータを扱えると言えます。 バイナリファイル内のデータが、文字だけで構成されていたとしても構いませんが、 文字だけで構成されたデータを、テキストファイルとして扱う場合と、バイナリファイルとして扱う場合とでは、異なる点が1つあります。 それは、改行文字の扱いです。
テキストファイルとして読み書きを行う場合、改行文字を本当に "改行" の意味で扱いますが、 バイナリファイルの場合は単なる数値でしかありません。 そもそも、テキストを扱うためのファイル形式ではないので、「行を変える」という感覚はありません。
改行文字に関する話は、後で改めて取り上げます

ただし、環境によっては、そもそもテキストファイルとバイナリファイルという区別の無いものもあります。

fwrite関数による書き込み

では、バイナリファイルの読み書きを行っていきましょう。 まずは書き込みを試します。 次項では、作成されたバイナリファイルを読み込むテストを行います。

バイナリファイルをオープンする際には、fopen関数(⇒リファレンス)の第2引数には、 "rb" や "wb" のように "b" を含むモードを指定します。 この "b" は、バイナリ(Binary) の b です。

書き込み自体は、fwrite関数(⇒リファレンス)で行います。 幾つも書き込み関数の種類があったテキストファイルと違って、バイナリファイルを書き込む標準関数は、これしかありません。 fwrite関数は、次のように宣言されています。

size_t fwrite(const void* ptr, size_t size, size_t n, FILE* stream);

第1引数に、書き込みたいデータのアドレスを指定します。 これは配列の要素のアドレスであっても構いません。
第2引数は、書き込むデータ1つ分のサイズを指定します。 例えば、int型のデータ1つならば、sizeof(int) のように指定できます。
第3引数は、書き込むデータの個数を指定します。 第1引数に指定したアドレスが配列の場合に、ここに要素数を指定できます。 単独のデータであれば 1 を指定します。
第4引数は、書き込み先ストリームの指定です。
戻り値は、書き込まれたデータの個数が返されます。 何らかのエラーが起きれば、第3引数の n に指定した値よりも小さい値が返されます。

それでは実際に試してみます。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	FILE* fp;
	int num = 900;
	double d = 7.85;
	char str[] = "xyzxyz";


	fp = fopen( "test.bin", "wb" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}


	fwrite( &num, sizeof(num), 1, fp );
	fwrite( &d, sizeof(d), 1, fp );
	fwrite( str, sizeof(char), sizeof(str), fp );


	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

実行結果(標準出力)


実行結果(test.bin のテキスト表現)

????ffffff@xyzxyz

3度の fwrite関数の呼び出しによって、int型の値を1つ、double型の値を1つ、6文字の文字列を1つ書き込んでいます。
実行結果の先頭部分は、この環境の文字コードでは表現できない文字です。 バイナリデータは文字列ではないので、このように判読できない状態になってしまいます。 それでも、"xyzxyz" のように、文字列として書き込んだ部分は読めます。

なお、出力したファイルの拡張子 ".bin" は、バイナリ(Binary) を表す一般的な拡張子です。 よく使われますが、どんな意味合いのデータなのかはよく分からない不明瞭な拡張子ではあります。

バイナリファイルの中身を調べたり、編集したりするには、バイナリエディタと呼ばれる、テキストエディタとは異なるエディタを使います。 フリーで高機能なものが幾つも存在するので、手元に置いておくと良いでしょう。 この後の項で、実際にバイナリエディタを使って、バイナリファイルの中身を見てみます。

fread関数による読み込み

次に、バイナリファイルの読み込みを行ってみましょう。 先ほどのサンプルプログラムで作成されたファイルを読み込みます。

バイナリファイルの読み込みには、fread関数(⇒リファレンス)を使います。 書き込み同様、バイナリファイルの場合の読み込み関数は、これしかありません。

size_t fread(void* ptr, size_t size, size_t n, FILE* stream);

第1引数に、読み込んだデータを格納する変数のアドレスを指定します。 これは配列の要素のアドレスであっても構いません。
第2引数は、読み込みデータ1つ分のサイズを指定します。 例えば、int型のデータ1つならば、sizeof(int) のように指定できます。
第3引数は、読み込みデータの個数を指定します。 第1引数に指定したアドレスが配列の場合に、ここに要素数を指定できます。 単独のデータであれば 1 を指定します。
第4引数は、読み込み元ストリームの指定です。
戻り値は、読み込まれたデータの個数が返されます。 何らかのエラーが起きれば、第3引数の n に指定した値よりも小さい値が返されます。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	FILE* fp;
	int num;
	double d;
	char str[7];


	fp = fopen( "test.bin", "rb" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}


	fread( &num, sizeof(num), 1, fp );
	fread( &d, sizeof(d), 1, fp );
	fread( str, sizeof(char), sizeof(str), fp );


	printf( "%d\n", num );
	printf( "%f\n", d );
	printf( "%s\n", str );


	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

実行結果(標準出力)

900
7.850000
xyzxyz

3回の fread関数によって、それぞれ、int型の値を1つ、double型の値を1つ、6文字の文字列を1つ読み込んでいます。 そして、その結果を printf関数で標準出力に出力して確認しています。

今回のサンプルプログラムを見ると、「int型、double型、6文字のchar型が連続して並んでいることが前提」のように見えると思います。 これだと、test.bin の中身がどうなっているか(どんな風に並んでいるか)分かっていないと困るのではないかと思うかも知れません。
実は、それは大正解です。 バイナリファイルの読み込みは、基本的に、どんなファイルフォーマットになっているのか知っていない限り、どうにもなりません。 テキストファイルのように、とりあえず1文字ずつ読み込んでみるという手は使えません。 1Byte ずつ読み込んでみたところで、結局、それをどう取り扱っていいのか分からないのです (例えば、4Byte 分のデータを読み取っても、それが 4Byte の整数なのか、2Byte の整数が 2つ並んでいるのか、4文字の文字列なのかといったことが全く分からないということです)。

テキストファイルにしても、文字コードの種類という問題はあるので、少々話を単純化し過ぎではありますが。

int型と double型が登場していますが、より正確に言えば、int型の大きさが異なる環境で作成されたファイルであれば、このサンプルプログラムではもうダメです。 同様に、浮動小数点数の表現方法が異なる環境で作成されていたら、やはり読み込めません。
このように、バイナリファイルの読み書きは、その中身のフォーマットが正確に分かっていないと、正しく扱うことができないのです。

バイナリデータを確認する

ここでは、バイナリファイルの中身を確認してみましょう。 前の項で少し触れたように、バイナリファイルの中身は、バイナリエディタを使って確認できます。

使用するバイナリエディタは何でも構いませんが、ここでは DANDP Binary Editor(作者様のサポートページ)を使ってみます。 ダウンロードから、インストールまでの流れについては割愛します。 OS X ならば、HexEdit(公式プロジェクト)などがあります。

前の項のサンプルプログラムによって生成された test.bin を読み込ませると、次のように表示されます。

DANDP Binary Editor での表示

左端の列にアドレスが、その右側には バイナリデータを 16進数で表記した羅列が、右端にはテキストデータとして見たときにどう見えるかが表示されています。 この辺りの表示構成は、他の大半のバイナリエディタでも同様だと思います。

右端のテキスト表記の部分をみると、末尾近くに "xyzxyz" という文字列が確認できます。 アドレスにすると、0000000C〜00000011 に当たりますが、この部分の 16進表記をみると「78 79 7A 78 79 7A」となっています。 ここから、'x' という文字は 78 であり、'y' は 79、'z' は 7A だと分かります。
これは次のようなプログラムで試してみても分かります。

#include <stdio.h>

int main(void)
{
	printf( "%X\n", 'x' );
	printf( "%X\n", 'y' );
	printf( "%X\n", 'z' );

	return 0;
}

実行結果(標準出力)

78
79
7A

つまり、文字データであっても、内部的には、何らかの数値として表現されていることが分かります。 文字と数値との対応関係は、文字コードという考え方で取り決められています。
文字コードには様々な種類があるため、異なる文字コードを使っていると、同じ 'x' という文字でも、数値化したときの値は異なる可能性があります。 Webサイトや、メールなどで、文字化けが起こる要因はここにあります。

C言語としては、基本的に ASCIIコードと呼ばれる文字コードがベースになっています。 多くのバイナリエディタでも、テキスト形式で表記されている部分では ASCIIコードが使われます。
ASCIIコードは、7bit で 1文字を表現する形式になっています。 7bit ということは、最大で 128種類の文字しか表現できない訳ですから、日本語の表示など到底不可能です。 実際、ASCIIコードは、半角英数字と、少しの記号類、幾つかの制御文字が含まれているだけであり、日本語の表現に関わるものは何も含まれていません。 ASCIIコード表は、至る所に掲載されている(⇒Wikipedia)ので、ざっと眺めておくと良いでしょう(暗記する必要はありません)。

それでは日本語はどうやって表現しているのかという話は、書き始めると長くなるので、ここでは触れません。 第46章で改めて取り上げます。

さて、先ほどの "xyzxyz" という文字列の直後に、テキスト表現だと '.' 、16進数だと 00 という文字があります。 これは、test.bin を書き出す際に、文字列の終端にある '\0' も書き出したため存在するものです。 つまり、'\0' という文字の正体は 00 という数値です。 テキスト表現が '.' となっているのは、印刷文字として表現できない場合に代替的に '.' を使うということに(このバイナリエディタが)しているからです。


次に、アドレス 00000000〜00000003 の 4Byte分を見てみましょう。 ここには、900 という int型の整数を書き出したのでした。 これが、16進数表現では 84030000 となっています。

まず、900 という 10進数が、16進数で幾つになるか調べてみます(手作業での変換については第19章で確認しました)。 すると、0x384 であることが分かります。 これがどうして、84030000 という表示になってしまうのかは、しっかり理解しておく必要があります。

この理解のためには、エンディアン(あるいはバイトオーダー)という考え方を知る必要があります。 これは、2Byte以上あるデータをメモリ上に配置するとき、各Byte をどのように並べるのかというルールのことです。 現在、圧倒的多数を占めている方式は、リトルエンディアンビッグエンディアンという2つの方式です。

今回、0x384 という数値を取り扱っていますが、これを 1Byte単位で分解すると、0x03 と 0x84 に分かれます。 実際には、int型(4Byte)の数値としてファイルへ書き出した訳ですから、頭に更に 0x00 が 2つあるはずです。 つまり、「0x00 0x00 0x03 0x84」です。
これに対して、バイナリエディタ上に表示されているのは、「0x84 0x03 0x00 0x00」という並びなので、どうやら並びが変わっているだけのようですね。

このように、「0x00 0x00 0x03 0x84」という順番のデータを、逆の順番「0x84 0x03 0x00 0x00」のように並べる方式は、リトルエンディアン方式です
一方、ビッグエンディアン方式の場合は、「0x00 0x00 0x03 0x84」は、そのままの順番「0x00 0x00 0x03 0x84」で並びます
つまり今回、バイナリエディタで確認したデータは、リトルエンディアン方式で並んでいるということです。

実は、Windows や、現在の OS X が動くコンピュータは、Intel系の CPU を使用しており、Intel が採用している方式がリトルエンディアンであることから、このような結果になります。 そのため、他の環境ではビッグエンディアン方式を使っており、今まで長々と話してきた内容は全然当てはまらず、 バイナリエディタ上でも、そのままの順番で並んで表示される可能性もあります。

古い OS X (Mac OS) は、Intel の CPU ではなく、PowerPC 上で動作していました。 これは、ビッグエンディアンなので、現在の OS X とは事情が異なります。

一見、リトルエンディアン方式は素直でない手法のように見えるかもしれませんが、実際には 0x00000384 というデータの下位の桁ほど、 若いアドレスに配置されているのですから、そういう視点で見ると素直な並びであるともいえます。 実際、そのおかげで、long型を short型に切り詰めるようなキャストなどが簡単に実現できます(上位のアドレスにあるデータを無視するだけで良い)。
この手の処理は、ビッグエンディアン方式の方がずっと面倒なことになります。


最後に、浮動小数点数7.85 を書き出した部分ですが、これについては、浮動小数点数の表現方式を知っていないと解読できません。 しかし、これはかなり難しい部類に入るので、ここでは扱いません。

改行文字

バイナリファイルには、文字を含むことができるのですから、テキストファイルとまったく同じデータで構成されていても構いません。 しかし、この章の冒頭で触れたように、改行文字に関する扱いが異なってきます。

C言語において、改行文字といえば '\n' をイメージしますが、これはC言語の文法上のルールに過ぎず、 実際のファイル内に '\n' がそのまま書き込まれているとは限りません。
Windows環境で、テキストファイルに '\n' を含んだ文字列を出力して、出来上がったファイルをバイナリエディタで覗き見てみましょう。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	FILE* fp;
	char str[] = "xyz\nxyz";


	fp = fopen( "test.txt", "w" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	fputs( str, fp );

	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

実行結果(標準出力)


実行結果(test.txt)

xyz
xyz

このプログラムは、テキストファイルへ書き出していることに注意して下さい。 作成された test.txt を、バイナリエディタで確認すると、次のようになっています。

改行文字をバイナリエディタで確認

アドレス00000000〜00000002 と 00000005〜00000007 は、いずれも "xyz" ですから、その間にあるのが改行文字だと考えられます。 ところが、ここには 2Byte分のデータ「0D 0A」が存在しています。

Windows環境では基本的に、改行は2つの文字コードで表現されます
「0D」はキャリッジリターン(復帰)と呼ばれるコードで、「0A」はラインフィード(改行)と呼ばれています。 前者を CR、後者を LF と略し、あわせて CR+LF のように表記することもあります。

環境によっては、CR と LF のいずれか一方だけで、改行を表すこともあります。 例えば、OS X では、LF だけが使われるので、先ほどのプログラムを OS X 環境の Clang でコンパイルして実行してみると、 標準出力に現れる結果は同じに見えますが、バイナリエディタで見れば次の写真のように「0A」だけしか無いことが分かります。

改行文字をバイナリエディタで確認
エンディアンの話と同様、改行文字も環境に対する依存性があるということです。 このように複数ある改行の表現を、C言語では '\n' という1つの表現方法に統一させることで、環境ごとの違いを吸収しています。 先ほどのプログラムのように、'\n' を出力するように指示しても、 実際には CR+LF や LF といったように環境ごとの表現へ自動的に変換されます
このように自動的に変換されるのは、入力の場合でも同様です。 先ほどのプログラムを実行して出力された test.txt を読み込んで、1文字ずつ出力するプログラムを作って確かめてみましょう。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	FILE* fp;
	int c;
	int i;


	fp = fopen( "test.txt", "r" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	for( i = 0; ; ++i ){
		c = fgetc( fp );
		if( c == EOF ){
			break;
		}
		
		printf( "%d: %c\n", i, c );
	}

	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

実行結果(標準出力)

0: x
1: y
2: z
3:

4: x
5: y
6: z

3文字目のところで、改行文字が読み込まれていますが、4文字目は 'x' が読み込まれています。 改行が 2Byte のデータのままで処理されているのであれば、3文字目は CR、4文字目は LF で、5文字目に 'x' が来るはずですが、そうはなっていません。 これは、CR+LF が読み込まれるとき、自動的に '\n' に変換されているからという訳です。
OS X では、最初から改行を 1Byte で表しますが、やはり自動的に '\n' に変換されていると考えられます。


一方、バイナリファイルの場合、このような自動的な変換が起こりません。 バイナリファイルにとって、改行文字というのは、単なる 1Byte のデータに過ぎず、何も特別扱いはしない訳です。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	FILE* fp;
	char str[] = "xyz\nxyz";


	fp = fopen( "test.bin", "wb" );
	if( fp == NULL ){
		fputs( "ファイルオープンに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	fwrite( str, sizeof(char), sizeof(str), fp );

	if( fclose( fp ) == EOF ){
		fputs( "ファイルクローズに失敗しました。\n", stderr );
		exit( EXIT_FAILURE );
	}

	return 0;
}

実行結果(標準出力)


実行結果(test.bin のテキスト表現)

xyz
xyz

今度はバイナリファイルとして書き出しています。 test.bin をバイナリエディタで開くと、次のようになります。

改行文字をバイナリエディタで確認

今度は、改行文字の部分は「0A」という 1Byte だけになっています。 このように、自動的な変換は行われません。 この結果は、Windows でも OS X でも同じになります。


ところで、C言語には '\n' の他に '\r' という文字も用意されています。 前者は「改行」、後者は「復帰」の意味を持っています。
先ほどから書いているように、テキスト形式の場合には '\n' は、環境に合わせて適切な改行コードへと変換されますから、 '\n' と '\r' を両方用いる必要はありません。

一方、バイナリ形式の場合には、'\n' と '\r' を区別して取り扱う必要性が生まれることもあります。 例えば、Windows環境での改行コード(CR+LF) で表現されているテキストファイルを、改行コードを LF だけで表現する環境で読み込もうと思えば、 バイナリファイルとして読み込んで、CR+LF になっている部分を手動で LF に変換しながら読み込む必要があります。

ランダムアクセス

最後に、ランダムアクセス直接アクセス)について少し触れておきます。 これは第40章で登場した fseek関数(⇒リファレンス)を使用して、 ファイルポジションを自由に動かして、好きな位置のデータを直接的にアクセスする方法のことです。

一方、先頭から順番にしか読み書きできない方式は、シーケンシャルアクセス順次アクセス)と呼ばれます。

テキストファイルの場合と違って、バイナリファイルの場合は、第2引数の移動量のところに自由に値を指定でき、 1Byte単位でファイルポジションを移動させることができます。
ただし、バイナリファイルに対する fseek関数において、第3引数を SEEK_END にしたとき、これがどんな結果になるかは環境依存となります。 そのため、ファイルサイズを調べるためのテクニックとして、よく使われている以下の方法は、環境依存の方法です。

long GetFileSize(FILE* fp)
{
	long fpos_save, size;

	/* 現在のファイルポジションを保存 */
	fpos_save = ftell( fp );

	/* ファイルの末尾まで移動して、その位置を調べる */
	fseek( fp, 0, SEEK_END );
	size = ftell(fp);

	/* ファイルポジションを元に戻す */
	fseek( fp, fpos_save, SEEK_SET );

	return size;
}

この関数は、fseek関数でファイルの末尾まで移動し、その位置で ftell関数(⇒リファレンス)を呼び出すことによって、 ファイルの先頭からの距離を調べています。 バイナリファイルの場合は、ftell関数が返す値は必ず、ファイルの先頭からのバイト数であることが保証されているので、この値はファイルサイズと一致します。 最後に、元のファイルポジションに戻してやるところまで面倒を見ています。
環境依存しているということですが、自分の環境で動作すれば良いということならば、使っていけない訳ではありません。


練習問題

問題@ 手元にある適当なファイルを幾つか、バイナリエディタに読み込ませて、中身を確認してみて下さい。

問題A 次のような構造体で定義されたデータを、バイナリ形式でファイルへ出力するプログラムおよび、 ファイルから入力を受け取るプログラムを作成して下さい。

typedef struct NameList_tag {
	int nameLength;    /* name の文字数 (終端文字を除く) */
	char* name;        /* 名前 */
	int age;           /* 年齢 */
} NameList;

問題B リトルエンディアンとビッグエンディアンを相互に変換するためには、どのようにすれば良いか答えて下さい。


解答ページはこちら

参考リンク

Write Great Code〈Vol.1〉
 -- データの表現形式。エンディアンや文字コードについて。

更新履歴

'2015/8/29 flose関数の戻り値もチェックするようにした。

'2014/1/31 OS X 環境に対応。

'2010/5/23 新規作成。



前の章へ

次の章へ

C言語編のトップページへ

Programming Place Plus のトップページへ