構造体メンバと空白、そしてポインタ利用の注意
構造体の各メンバのサイズは、見た目通りとは限りません
struct
{
char a;
int b;
}st;
int x = sizeof(st.a);
int y = sizeof(char);
例えば、上記の例では、xとyの値は異なります。(通常はxが正しくyが誤り)
x は 4 になりますが y は 1 になります。
char型が8bit=1byteで、int型が32bit=4byte(32bit環境の場合)なので、a と b の間に空白の3byteが生まれるためです。
32bitリトルエンディアン(little-endian)の場合
ADDRESS DATA
+0 a
+1
+2
+3
+4 bのbit7~0
+5 bのbit15~8
+6 bのbit23~16
+7 bのbit31~24
32bitビッグエンディアン(big-endian)の場合
ADDRESS DATA
+0 a
+1
+2
+3
+4 bのbit31~24
+5 bのbit23~16
+6 bのbit15~8
+7 bのbit7~0
これは、ハードウェアの都合にコンパイラがあわせているためです。
32bitのハードウェアでメモリとCPUが32bitのバスで接続されている場合、1回のアクセスで32bit(アドレス+0~+3の4byte)のデータを読み書きできます。
bを読み出す際は、アドレス+4をリードし、+4~+7のデータを読み取ります。
仮にaとbを詰めてメモリ上に配置した場合、以下の様になります。
(仮定)
ADDRESS DATA
+0 a
+1 bのbit7~0
+2 bのbit15~8
+3 bのbit23~16
+4 bのbit31~24
この配置のメモリから b を読み出すためには、
アドレス+0をリードしてからbit31~bit8をbit23~0へ8bitずらして、
更に、アドレス+4をリードしてbit7~0をbit31~bit24へずらし、
先のbit23~0へ8bitと結合しなければなりません。
これでは時間と回路を消費するばかりです。
その為、メモリに隙間を作って楽にアクセスできるようにしているのです。
また、下記の場合、printされるのはbではありません。
char *p = &st.a;
p++;
printf("%X", *p);
printf("%X", *(int*)p);
pの型がchar型(1byte)のポインタなので、インクリメントするとアドレスが+1しかされません。
pのアドレスは+1なので、空白のメモリを読む事になります。
空白部分は、未定義なので、どんな値が入っているか分かりません。
以前入っていた値がそのまま残っているときもあれば、0やffに書き換えられることも、
一度も書き込みが行われず、電源が入ったときのランダムな値がそのまま残っているかもしれません。
こんなポインタの使い方はNG
そして、bだと思って(int*)でキャストしたりしたらハングアップしてしまいます。
アドレス+1に32bitアクセスを行うように指示するため、ハードウェアには実現できない、
実現できないからこの後の動作を決められない、という理由から、CPUはやむなく動作を停止します。
実際には、CPUはこの指示を受けた次点で「実行不可能」「この先の動作を決めることができない」ということが分かるので、これを実行せず、決まった(あるいは事前に設定してある)エラー処理を実行します。
しかし、このエラー処理はOSレベルよりも更に下位のハードウェアに密着した部分の処理なので、アプリケーションはおろか、OSも停止します。
Linux等のOSには、この例の様な分かりやすい「不可能な処理」をアプリケーションが実行しようとした場合、その命令をCPUに渡す前にOSが拾い上げ、アプリケーションだけ停止させたり、代替処理に自動的に置き換えたりして自動的に問題を回避する機能が備わっています。
構造体のメンバーを使う場合は、アドレスやポインタで指定せず、 . や -> でメンバー名を明記するにしましょう。