純規の暇人趣味ブログ

首を突っ込んで足を洗う

[C++]構造体や変数をバイナリで入出力する

      HimaJyun

普段はこんな事やろうとは思いませんし、やる必要も大してないのですが、出来ると「なるほど」と思う「変数のファイル入出力」

調べてみたらアライメントだの何だの少しややこしい所があったので、分かった事をまとめてみようと思います。

変数のバイナリ入出力

変数なんてものは言ってしまえばメモリ上のソレなので、要はこれをファイルに入れたり出したり出来れば変数に含まれている値が出し入れ出来るのです。

雑に説明すればこう言う事

#include <cstdio>

int main() {
	// これをファイルに出し入れしたい
	int n = 9999;

	// ファイルを開く
	FILE* fp;
	// fp = fopen("./filename.dat", "wb");

	// VisualStudioがうるさいのでこっち
	fopen_s(&fp, "./filename.dat", "wb");
	// 読み込むならこっち
	//fopen_s(&fp, "./filename.dat", "rb");

	// 変数の中身を書き込む
	fwrite(&n, sizeof(n), 1, fp);

	// 読み込む
	//fread(&n, sizeof(n), 1, fp);
	//printf("%d\n", n);

	// ファイルを閉じる
	fclose(fp);

	return 0;
}

Cっぽく見えてC++のコード、闇に魂を売った魔術師達に見せたら私が生贄にされてしまいそうな呪文である。

これで変数「n」の中身、9999がファイルに書き出される。(もしくは読み込まれる)

うん、何を今更……

C++っぽくする

さっきのままでは闇の魔導師達の手によって魔王様(美女)に生贄として捧げられかねないので、C++っぽくしてみた。

#include <iostream>
#include <fstream>
using namespace std;

int main() {
	// これをファイルに出し入れしたい
	int n = 9999;

	// バイナリ出力モードで開く
	fstream file("./filename.dat", ios::binary | ios::out);
	// こっちでも良し
	//file.open("./filename.dat", ios::binary | ios::out);
	// 読み込むならこう
	//file.open("./filename.dat", ios::binary | ios::in);

	// 書き込む
	file.write((char*)&n, sizeof(n));

	// 読み込む
	//file.read((char*)&n, sizeof(n));
	//cout << n << endl;

	// 閉じる
	file.close();

	return 0;
}

これで捧げられる先が魔王様(美女)から女神様(美女)に切り替わった。

fopen、fwrite、よりもfstreamの方がいくらか美しいし扱いやすい。(C(++じゃない方)はコマンドの羅列みたいになるのが汚いから嫌だ)

(char*)としているのはfstreamとかがcharしか受け付けないから……だと思う。

構造体でやってみる

とは言え、変数1つぽっち出力できたってなぁあああんんにも嬉しくないし、ありがたくもない。

やはり構造体でまとめて出し入れをしたいのである。

と言う訳で、こんな風にしてみた。

#include <iostream>
#include <fstream>
using namespace std;

struct Status {
	int hp;      // 体力
	int power;   // 攻撃力
	int defense; // 防御力
	int speed;   // 素早さ
};

int main() {
	// これをファイルに出し入れしたい
	Status n = { 0 };
	n.hp = 9999;
	n.power = 9999;
	n.defense = 9999;
	n.speed = 9999;

	fstream file;
	file.open("./filename.dat", ios::binary | ios::out);
	//file.open("./filename.dat", ios::binary | ios::in);

	// 書き込む
	file.write((char*)&n, sizeof(n));

	/*
	// 読み込む
	file.read((char*)&n, sizeof(n));
	cout << "HP:" << n.hp << endl;
	cout << "Power:" << n.power << endl;
	cout << "Defense:" << n.defense << endl;
	cout << "Speed:" << n.speed << endl;
	//*/

	// 閉じる
	file.close();

	return 0;
}

何度か値を入れ替えて試してみたが、上手く読み書き出来ているみたいだ

これで構造体として複数の変数がまとめて出し入れ出来る様になった、素晴らしい。

アライメント

しかし名前が設定出来ないのでは不便だ、うっかりチルノをHP9999にしてしまうかも知れない。

と言う訳で、それに対処すべく、先程のStatus構造体をほんんんんの少しだけ進化させた。

struct Status {
	char name[50];  // 名前
	int hp;         // 体力
	int power;      // 攻撃力
	int defense;    // 防御力
	int speed;      // 素早さ
};

名前を登録できるようになった、早速こんな値を設定して試してみる。

Status n = { 
	"レミリア・スカーレット",
	5000,
	8000,
	6000,
	9999
};

正常に読み書き出来ている、問題ない、かわいいよレミリア。

と、ここからの解説、殆ど他所のサイト(主にロベールのC++)の受け売りみたいになる……一応解説しておくけど、向こうの方が専門だと思う。(偉そうに書いてるけど、このブログのナカノヒトはC++歴がまだまだ浅い)

変数のバイト数はMSDNとかに書かれてある、え?*nix環境?知らんな。

MSDNによると、charは1バイトで、intは4バイト。

つまり、先ほどの構造体は1バイトのcharが50個入りになった50バイトと、4バイトのintが4つで16バイト、計算上は合わせて66バイトになるはず。

しかし、出力されたファイルを実際に見てみると……68バイトある、多い……2バイトはどこから出て来た?

一般的なCPUでは、メモリにデータを取りに行くときに、intのサイズ単位、すなわち、4バイト単位で取りに行く。

(こう言うnバイト単位系は別にCPUに限った話ではない、HDDなんかも1バイト書き換えるために1セクタ(512バイト、最近のだと4096バイト)をバッファに溜めて、そのバッファ上の1バイトを書き換えて、1セクタ書き込む、とかやってる)

そのため、普通にやろうとすると、4バイト単位で区切った時に、先の構造体で言う体力用の変数(hp)より後の部分が4の区切りを跨いでしまう。

これでは変数1つ取りに行くのに2回のメモリアクセスを伴い、とても効率が悪い。(いくらメモリが早いと言えど、CPUから比べればナメクジ同然)

そのため、自動的に適切な部分に適切な量の詰め物がぶち込まれる(先の構造体で言うとname[50]の後ろに2バイト分がある)

分かりづらいと思うけど、このハイライトされてある「CC CC」がそれ。
cpp-structure-file-001

このアライメント、VC++環境なら#pragma pack()で詰める事が出来る、こんな感じ

#pragma pack(1)
struct Status {
	char name[50];  // 名前
	int hp;         // 体力
	int power;      // 攻撃力
	int defense;    // 防御力
	int speed;      // 素早さ
};
#pragma pack()

アライメントを詰めて実行してみると、確かに66バイト、計算通りの結果になった。

が、これはすなわち、変数をズレたまま、2回アクセスが必要な状態にすると言う事だろうし、きっと何のメリットもない。

これで詰められるファイルサイズなんてせいぜい4バイト未満、だったらそのままで良い。

そんな無駄な事を考えている時間にさっさと次のコードを書いた方が良い、と、私はそう思う。(どうしても詰めたければchar配列の大きさを52にすれば良い、いわば手動アライメント)

……説明が下手で申し訳ない、感覚で理解してちょ、そう難しい物じゃないから。

ポインタに注意

これで構造体を入出力する感覚が掴めた、はい、ちゃんちゃん、とはならないのが世の常、世界は非情である。

先程の構造体では名前の部分を「大きさ50のchar!!」で決め打ちしていた。

しかし、例えばここに50文字を超える名前を入れる必要が出来た。

「50文字超える名前の奴なんか居ない!!」まったくもってその通りではあるが、例えばピカソのフルネームとか余裕で溢れる。

そういう状況に合わせて、容量を動的に確保したい事があるかも知れない。(もしくは、std::stringを使いたいとか)

と言う訳でこうする

struct Status {
	char* name;  // 名前
	int hp;      // 体力
	int power;   // 攻撃力
	int defense; // 防御力
	int speed;   // 素早さ
};

要は実行時に必要な分だけメモリを確保して、それを利用しよう、と言う思考回路である。

こんな感じで初期化する。

Status n = { 0 };
n.name = "博麗霊夢";
n.hp = 4500;
n.power = 7500;
n.defense = 8000;
n.speed = 7000;

実行してみる、めんどくさいので結果を言うと、20バイトと言うあり得ないくらいに小さいファイルが生産された。

もうこの時点でヤバい匂いがぷんぷんと香ばしいのだが、ファイルを読み込んでみる。

クラッシュした(はぁと
cpp-structure-file-002

読み取りアクセス違反、見ては行けない社会の闇を見に行ってしまったと言う事。

何故かと言うと、先ほどの「char* name」、これはcharのポインタ型であり、ここにあるのはcharそのものではなく、charがどこにあるのか、と言う情報。

20バイトになったのは、ポインタが大抵の場合4バイトで、charのポインタ(4バイト)+int(4バイト)が4つ、合わせて20バイト、と言う訳。

一応きちんと入出力する方法はあるんだけど……ちょっと自分では手に負えなかった(どうしてもやりたいならロベールのC++辺りに書いてある方法で出来ると思う)

また出来る様になった時に覚えてたら追記する(忘れる事への言い訳)

最後に

ポインタの入出力が歯に引っかかる様なもどかしい感じでしか説明出来なかったけど……変数の中身を出し入れすると言ってもなかなか一筋縄には行かない。

別に無理して「変数の中身」を出す必要もないとは思う、必要に応じてシリアライズ化するとかしたのでも十分要件は満たせる。

ちなみに、説明もなしに利用していた「バイナリモード」はWindows環境下で\nを\r\nにするためで、テキストモードで出力すると\n(0x0A)が\r\nに変換されてしまってデータが壊れる。(*nixだとバイナリモードの指定の有無を問わず常にバイナリモード)

サラッと重要な事を書いたが、バイナリはバイナリモードで扱おう。

 - プログラミング