C言語 構造体のメモリ状態

構造体を動的にメモリ確保する手法を以前解説しました。その時は特に気にならなかったと思いますが、実は構造体メンバの指定によっては少し不思議な状態になったりします。今回はそんな内部の状態についての解説となります。
--
いきなりここに飛んで来ちゃった人は、よろしければ下記からご覧ください。


  • メンバが同じでもサイズが異なる
例えばRPGキャラを作るとして、次のような構造体を定義したとします。

typedef struct { char Level; char Status; char Attack; char Defense; int HPNow; int HPMax; int MPNow; int MPMax; } TCharacter;
この構造体で変数を作ってサイズを確認してみます。

TCharacter npc; printf("%d\n", sizeof(npc));
結果は 20 と表示されます。char が 1バイト、int が 4バイトですから、トータル 20バイトで特に違和感はありません。では、構造体のメンバを

typedef struct { char Level; int HPNow; char Status; int HPMax; char Attack; int MPNow; char Defense; int MPMax; } TCharacter;
このように並び替えて、もう一度サイズを確認してみます。すると今度は 32 と表示されます。実は C言語では、そのコンパイルする CPU にとって、最もアクセスしやすいアドレスに変数を割り当てるという動作をするのです。char は 1バイトサイズなので、どのアドレスでも変わりはないのですが、int は 4バイトなので、4 で割り切れるようアドレスが決められるのです。npc の各メンバのアドレスを表示してみます。

printf("npc 全体の場所: %p\n", &npc); printf("char Level: %p\n", &npc.Level); printf("int HPNow: %p\n", &npc.HPNow); printf("char Status: %p\n", &npc.Status); printf("int HPMax: %p\n", &npc.HPMax); printf("char Attack: %p\n", &npc.Attack); printf("int MPNow: %p\n", &npc.MPNow); printf("char Defense:%p\n", &npc.Defense); printf("int MPMax: %p\n", &npc.MPMax);
実行結果は
アドレス
char 型メンバのデータサイズが 4バイトになっているのが分かりますでしょうか。これ、実際には char なので 1バイトしか使われず、その後方の 3バイトは未使用空間となります。この未使用空間のことを padding(パディング)と呼びます。無駄な空間は出来る限り無くす方が良いと思いますので、このアドレス補正を意識したメンバの並びにすることが肝要です。このアドレス補正のことをアライメントと呼びます。
※ 暖かい布団に包まれて引き篭もり人生が捗りそうですねw

  • ビットフィールド
ゲームなどではフラグ管理をすることが多いです。どの宝箱が開いただとか、どのイベンが発生したかなど、本当にフラグはよく使います。C言語では最小サイズの型は char となります。ひとつのフラグをひとつの char 型メンドで表現した場合は、1bit の on / off 以外の 7bit は無駄になります。そこで最小単位の bit 数をメンバに割り当てようというのがビットフィールドとなります。

typedef struct { char Level; unsigned char StPoison : 1; unsigned char StParalysis : 1; unsigned char StSleep : 1; unsigned char StConfused : 1; unsigned char Hunger : 4; char Attack; char Defense; int HPNow; int HPMax; int MPNow; int MPMax; } TCharacter;
StPoison メンバに :1 とあるのがビットフィールドです。これで指定の型のうち、何ビットを割り当てるかを指定できます。:1 とあるのは 1bit なので、値としては 0 と 1 が代入できます。:4 の場合は 4bit です。0 から 15 が代入できます。

なお、ビットフィールドのポインタアドレスの取得は禁止されています。
ビットフィールドのポインタは取得できない
そこで前後のポインタアドレスから、データサイズが本当にビット圧縮されているかを確認します。直前のメンバ Level のアドレスが F958、直後のメンバ Attack が F95A です。Level は1バイトですので、StPoison の開始アドレスは F959 と思われますが、ビットフィールドが付いた全てのメンドを合わせても、サイズが 1バイトしか用意されていない事がこれで分かりますね。
ビットフィールドの変数サイズを確認する
ビットフィールドを使用したメンバの使い方は、ごく普通のメンバ変数と同じです。TCharacter の型を持つ npc という構造体変数があったとします。

TCharacter npc; npc.StParalysis = 1; // 麻痺にする npc.Hunger = 7; // 腹減りを Lv.7 にする
ここで 4bit のサイズを持つ npc.Hunger に本来扱えないはずの 20 という数値を代入したら、どのような値になるか分かりますでしょうか。当然最大値としては 15 までしか扱えないので 20 は桁あふれを起こします。20 という数値は 2進数では

00000000 00000000 00000000 00010100
このようなビット列で表されます。ビットフィールドが 4bit指定ですので、この値のうち、下位 4bitだけが有効となります。下位 4bitは 0100 ですから、結果は 4 となります。コンパイラにも依ると思いますが、通常は桁あふれはエラーも警告も出ませんので注意が必要です。
※風呂でのんびり過ごすのも良さそうです。炭酸が効くらしいですねー