こんにちは、ナナです。
ファイルには大きく分けて「テキストファイル」と「バイナリファイル」の2つがあります。
ファイル | 説明 |
---|---|
テキストファイル | 文字コードの情報で構成されたファイル |
バイナリファイル | 文字コード以外の情報も含んだファイル |
本記事では、「バイナリファイル」への書き込みと読み込みを行うための方法を紹介しましょう。
本記事では次の疑問点を解消する内容となっています。
「テキストファイル」から文字を読み書きする方法は、次の記事を読むとよいでしょう。
》参考:ファイルへの書き込み【fputc/fputs/fprintfの使い方】
》参考:ファイルから読み込み【fgetc/fgets/fscanfの使い方】
バイナリファイルの特徴とデータを見る方法
「バイナリファイル」とは、いったいどのような特徴を持つファイルなのでしょう。
まずは、バイナリファイルの特徴や、データをどのように見るのかを解説しましょう。
「バイナリファイル」と「テキストファイル」の違い
皆さんのパソコンの中には、様々なファイルが管理されています。それらのファイルは「テキストファイル」と「バイナリファイル」のどちらかに区分けされます。
テキストファイルは「メモ帳」といったアプリケーションで開けば、何が書かれているのかわかります。
しかし、バイナリファイルは、数値情報で構成されているため、それらの情報が何を示しているのかを知らないと解読できません。
気づいていないかもしれませんが、Excelの「xlsxファイル」といったファイルもバイナリファイルなんです。メモ帳で開くと文字化けが発生しますよ。
バイナリファイルを見る方法:バイナリエディタの紹介
「テキストファイル」はメモ帳アプリを代表とする「テキストエディタ」と呼ばれるアプリケーションで開いたり編集することができます。
テキストエディタは、世の中にたくさんあります。有料の「秀丸エディタ」や無料の「サクラエディタ」など、検索すればたくさん見つかります。
では、「バイナリファイル」はいったいどのように編集するのでしょうか。
その方法とは
「バイナリエディタ」
を使って編集をするのです。
今回は「Stirling(スターリング)」というバイナリエディタを使ってみましょう。フリーソフトなので、Vectorからダウンロード『Stirlingのダウンロードページ』するとよいでしょう。
「バイナリエディタ」にもいろいろな種類があります。基本的な使い方はさほど変わらないため、お気に入りのエディタを見つけておくとよいでしょう!
バイナリエディタ:Stirlingの使い方
ダウンロードしたファイルを解凍すると「Stirling.exe」が含まれています。このファイルを実行すると 「Stirling」が起動します。
起動したらExcelファイルでもPDFファイルでもよいので、適当なファイルを放り込んでみましょう。
① | データの位置を示す。16進数で表現される。 |
② | データの数値が16進数で表示される。 |
③ | データを文字コードで表示した場合の文字を示す。 |
データをクリックして、キーボードから値を書き換えることもできます。
Excelファイルなどのバイナリファイルを意図せず変更すると、ファイルが壊れてExcelから読み込めなくなるため注意しましょう!
バイナリファイルから読み書き:標準ライブラリ関数の種類
「バイナリファイル」に対して情報を読み書きすることは、標準ライブラリ関数を使えば簡単にできます。どんなものがあるのかを紹介しましょう!
標準ライブラリ関数の紹介
「バイナリファイル」へは、次の関数を使って読み書きします。
#include <stdio.h>
size_t fwrite(void * buf, size_t size, size_t num, FILE * fp);
size_t fread(void * buf, size_t size, size_t num, FILE * fp);
頭文字の「f」は「file」を示しています。
「fwrite関数」と「fread関数」の戻り値と引数の構成は全く同じですね。
第4引数には、ファイルハンドルへのポインタを指定するようになっていますね。
fwrite関数の仕様
「fwrite関数」を使用すると、ファイルハンドルが示すファイルに数値情報を書き込むことができます。
includeファイル | stdio.h |
関数仕様 | size_t fwrite(void * buf, size_t size, size_t num, FILE * fp); |
引数1 | ファイルへ書き込みするメモリへのポインタ |
引数2 | 書き込み要素1つ当たりのバイトサイズ |
引数3 | 書き込み要素の個数 |
引数4 | 書き込み対象ファイルへのファイルハンドル |
戻り値 | 書き込みできた要素の個数(データサイズではなく個数) |
特記事項 | ファイルには「size × num」のデータサイズが書き込まれる |
fread関数の仕様
「fread関数」を使用すると、ファイルハンドルが示すファイルから数値情報を読み込むことができます。
includeファイル | stdio.h |
関数仕様 | size_t fread(void * buf, size_t size, size_t num, FILE * fp); |
引数1 | ファイルから読み込んだデータを格納するメモリへのポインタ |
引数2 | 読み込み要素1つ当たりのバイトサイズ |
引数3 | 読み込み要素の個数 |
引数4 | 読み込み対象ファイルへのファイルハンドル |
戻り値 | 読み込みできた要素の個数(データサイズではなく個数) |
特記事項 | 引数1のメモリに「size × num」のデータサイズが書き込まれる |
「引数2」と「引数3」が何を示しているのか、わかりづらいかもしれません。
構造体1つのメモリサイズを「引数1:size」に設定し、構造体の配列個数を「引数2:num」へ設定するイメージです。後ほどプログラムで実例を示しましょう!
fread関数とfwrite関数を使ったサンプルプログラム
サンプルプログラムから「fread関数」と「fwrite関数」の使い方を把握しましょう。
fwrite関数を使ったサンプルプログラム
それでは「fwrite関数」を使って、バイナリファイルへ書き出してみましょう。16個分の配列に設定された「0x01~0x10」の数値を「num.bin」ファイルへ書き込んでみます。
#include <stdio.h>
int main(void)
{
FILE * fp = NULL;
unsigned char Array[16] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10
};
// バイナリファイルの書き込みでオープン
fopen_s(&fp, "num.bin", "wb");
// ファイルへ書き込み
fwrite(Array, sizeof(unsigned char), sizeof(Array) / sizeof(Array[0]), fp);
fclose(fp);
return 0;
}
実行すると、プロジェクトフォルダに「num.bin」ファイルが生成されます。Stirlingを使って中身を確認してみましょう。
プログラムから書き込んだ「0x01~0x10」の数が実際にファイルに反映されているのがわかりますね。
fopen_s関数のモードに「b」を付与することでバイナリファイルとしてオープンします。
ファイルの開き方の詳細は『C言語 ファイルの開き方・閉じ方【fopenとfcloseの使い方】』の記事を読みましょう。
fread関数を使ったサンプルプログラム
今度は「fread関数」を使ったプログラムを紹介します。先ほど作成した「num.bin」から数値を取り出してみましょう。
#include <stdio.h>
int main(void)
{
FILE * fp = NULL;
unsigned char Array[16] = { 0 };
int i;
// バイナリファイルの読み込みでオープン
fopen_s(&fp, "num.bin", "rb");
// ファイルからの読み込み
fread(Array, sizeof(unsigned char), sizeof(Array) / sizeof(Array[0]), fp);
// 読み込んだデータを画面に表示
for (i=0 ; i < sizeof(Array) / sizeof(Array[0]); i++)
{
printf("0x%02x ", Array[i]);
}
fclose(fp);
return 0;
}
実行すると、次の通りArray配列の中身が画面に表示されます。
0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10
バイナリファイルから読み込んだ「0x01~0x10」の情報が、配列Array[16]に読み込まれているのがわかります。
次はもう少し実践的なシーンを想定して、バイナリファイルの読み書きをしてみましょう。
バイナリファイルへの構造体データの記録と復元
皆さんは今までにゲームをされたことはありますね。クリアまでに長い時間が必要なゲームは、進行状況をセーブデータに記録して進めます。
このような進行状況を示すデータは、ゲーム中はプログラム内の変数で管理します。そして、ゲームを終了している時は、ファイルにセーブデータとして保管しておくということがあります。
それでは「バイナリファイル」を活用して、セーブデータへの記録と復元をしてみましょう。
このようなイメージを持ちながら、この先の記事を読みましょう!
構造体データをセーブデータへ記録するプログラム
キャラクターのステータスは「HP」「MP」「強さ」といった情報が管理されています。今回はこれらの情報を構造体で管理する構成としましょう。
それでは、構造体として管理されたデータを、まずはバイナリファイルへセーブしてみましょう。
#include <stdio.h>
typedef struct
{
char name[16]; // 名前
unsigned long hp; // HP
unsigned long mp; // MP
unsigned char level; // レベル
unsigned char strength; // 強さ
unsigned char speed; // 素早さ
unsigned char wisdom; // 賢さ
unsigned long experience; // 経験値
} S_STATUS;
int main(void)
{
FILE * fp = NULL;
S_STATUS character[]=
{
//名前 HP MP レベル 強さ 素早さ 賢さ 経験値
{"Brave", 180, 50, 1, 22, 16, 15, 0 }, // 勇者
{"Knight", 150, 0, 1, 25, 18, 6, 0 }, // 騎士
{"Wizard", 90, 120, 1, 13, 10, 22, 0 }, // 魔法使い
};
// セーブデータをバイナリの書き込みでオープン
fopen_s(&fp, "SaveData.bin", "wb");
// キャラクター状態をファイルに記憶(キャラクター:3人)
fwrite(character, sizeof(S_STATUS), sizeof(character) / sizeof(character[0]), fp);
fclose(fp);
return 0;
}
「勇者」「騎士」「魔法使い」の3人のキャラクターのステータスを、構造体データをそのままセーブデータファイルに書き込んでいます。
それでは生成された「SaveData.bin」ファイルを、バイナリエディタで見てみましょう。
各ステータスは32バイトで構成されており、合計96バイトのファイルになっています。
セーブされたデータの解析
バイナリファイルに記憶されたデータを、改めて考察してみましょう。
バイナリ情報は16進数で表現されているため、10進数に変換すると記憶したデータと整合が取れていることがわかるでしょう。
「HP」のデータが「B4 00 00 00」になっているのは、リトルエンディアンで動かしているためですね。
エンディアンについて知らない方は、こちらの記事『エンディアン【通信やファイルの読み書きで必須の知識!】』を見ておきましょう。
このようなファイルにおける読み書きは、代表的なエンディアンを意識するシーンですよ。
セーブデータから構造体データへ復元するプログラム
セーブデータに記憶されたキャラクターのステータスを、今度は構造体変数へ読み込んでみましょう。fread関数で構造体へ読み込みなおします。
#include <stdio.h>
typedef struct
{
char name[16]; // 名前
unsigned long hp; // HP
unsigned long mp; // MP
unsigned char level; // レベル
unsigned char strength; // 強さ
unsigned char speed; // 素早さ
unsigned char wisdom; // 賢さ
unsigned long experience; // 経験値
} S_STATUS;
int main(void)
{
FILE * fp = NULL;
S_STATUS character[3] = { 0 };
int i;
// セーブデータをバイナリの読み込みでオープン
fopen_s(&fp, "SaveData.bin", "rb");
// キャラクターのステータスをファイルから復元(キャラクター:3人)
fread(character, sizeof(S_STATUS), sizeof(character) / sizeof(character[0]), fp);
// 構造体変数に復元したデータを画面に表示
for (i=0; i < sizeof(character) / sizeof(character[0]); i++)
{
printf("%s, HP:%d, MP:%d, LEVEL:%d, 強さ:%d, 素早さ:%d, 賢さ:%d, 経験値:%d\n",
character[i].name, character[i].hp, character[i].mp, character[i].level,
character[i].strength, character[i].speed, character[i].wisdom, character[i].experience);
}
fclose(fp);
return 0;
}
実行した結果、画面に次のように復元した情報が表示されます。
Brave, HP:180, MP:50, LEVEL:1, 強さ:22, 素早さ:16, 賢さ:15, 経験値:0
Knight, HP:150, MP:0, LEVEL:1, 強さ:25, 素早さ:18, 賢さ:6, 経験値:0
Wizard, HP:90, MP:120, LEVEL:1, 強さ:13, 素早さ:10, 賢さ:22, 経験値:0
構造体というメモリの箱に対して、バイナリファイルに記憶された情報をそのまま読み込むのです。これにより記憶した情報を復元するのです。
このようにバイナリファイルを使って、アプリケーション固有の情報を記憶することができます。