前回は,複数のデータに対して, メモリ空間内での相互の位置関係について学習した.
空間内のどの領域を(倉庫内のどの箱を)使うのか?
変数 int a の領域はココ, 変数 char b の領域はその隣, さらにその隣は空き領域で, 次は変数 double c, という感じだった.
今回は,個別のデータに対して, それぞれのメモリ領域内での記録形式を解剖しよう.
領域内のどのどこに(連続する複数個の箱のどれに) どのように(どんな順序・方向で)入れられるのか?
変数 int a の領域(4 byte)内の 1 byte 目は何?とか, 変数 double b の領域(8 byte = 64 bit)内の 1 bit 目は何?てな感じ.
なお,「メモリ空間」と「メモリ領域」の意味の微妙な違いに注意.
すでに知っている通り, char型のデータは,文字だけでなく整数を記録するためにも利用される. この char型の情報量(データサイズ)は 1 byte = 8 bit なので, 28 = 256 種類の整数 0x00 〜 0xFF(0 ~ 255)を取り扱える. さらに,charには2通りの使い方があり, それぞれ表現できる数値の範囲が異なる:
これまでにも使って来た整数値としての char型は, 実は,signed char 型だった. つまり,整数では,デフォルトで signed になる. 非負整数として使いたい場合に「unsigned」を明示せよ.
なお,符号を気にしなければならないのは,数値として使う場合だけだ. 文字としての char型の場合, 符号を気にする必要はない. 文字の場合,「signed」とか「unsigned」とか付けなくてよい.
そして,負の整数は, 補数表現によって正の整数としてメモリに記録される. つまり,1 byte の場合,負の数 −x を正の数 256 − x で表現する. たとえば,−1 の補数は 256 − 1 = 255 = 0xFF, −2 の補数は 256 − 2 = 254 = 0xFE となる. 次のプログラムを使って,このことを確かめてみよう.
ソース char.c:(char型の記録形式の確認)
#include <stdio.h>
int main(void)
{
signed char sc = 0xFF; // char sc = 0xFF; でも同じ
unsigned char uc = 0xFF;
printf(" signed char : %4d\n", sc);
printf("unsigned char : %4d\n", uc);
return (0);
}
このプログラムを実行すると, メモリに記録されているデータは, どちらも同じ 0xFF であるにも関わらず, signed char では −1, unsigned char では 255 のように, 型によって異なる値をとることがわかるハズだ. つまり,実体は同じであっても,表現が異なる.
次に,int型のデータは 4 byte の情報量を持ち, 4つの連続したアドレスのメモリ領域に記録される. つまり,int型のデータは,メモリ領域内では, 4文字分の文字列と同様の形式で記録されていることになる.
なお,ややこしいことに,int のサイズについては, 実は 4 byte と決まっているわけではない. 古い処理系では 2 byte だったり, 新しい処理系では 8 byte だったりするかもしれない. 現在の主流が 4 byte というだけ. (つまり正式には,4 という定数ではなく, sizeof(int) で調べて使うべきだが, この授業では 4 としておく.)
また,int 型にも char 型と同様に, signed(符号つき)と unsigned(符号なし)があり, signed がデフォルト.
ただし,領域内でのバイトオーダ(byte order;1 byte 毎の記録順序) には2通りの方式があり, CPU の種類によって異なる方式が採用されている:
ここで,「上位/下位」は桁の「大/小」, 「先頭/末尾」はアドレスの「小/大」を意味する.
ちなみに,Intel 社の Core や AMD 社の Athron 等はリトルエンディアンを, IBM 社の PowerPC(Apple の旧 Mac 用)や Cell(Sony の PS3 用)等は ビッグエンディアンを採用している.
具体例:整数 2567(10進数)=00 00 0A 07(16進数)
自分が使っている PC のバイトオーダを 次のプログラムによって確かめてみよう.
ソース int.c:(int型の記録形式の確認)
#include <stdio.h> int main(void) { int data = 0x1234ABCD; // 0x12 が上位のバイト,0xCD が下位のバイト unsigned char *p; p = (unsigned char *)&data; /* 変数 data(int 型)のアドレス &data(int * 型)を 文字列(バイト列,unsigned char * 型)のアドレスとみなす */ // 4 byte のまま表示 printf("%X\n", data); // 1 byte ずつ表示 printf(" %02X", p[0]); // 先頭のバイト printf(" %02X", p[1]); printf(" %02X", p[2]); printf(" %02X", p[3]); // 末尾のバイト printf("\n"); return (0); }
ポインタとキャストが出てきた. 忘れてしまった人は, 前期の該当部分を復習すること.
また,配列のメモリマップについても理解している必要がある.
なお,メモリ内に記録されているバイナリデータ (binary data,ビット列,バイト列)を 「生(なま,raw)」の状態で取り出すには, このプログラムのように, データを unsigned char型へキャストする必要がある. もし,この時に char(つまり signed char)型を使ってしまうと, 補数表現によってデータの値が変化してしまう場合があることに注意しよう.
実行結果より,その PC のバイトオーダは何だと判断できるか?
最後に,double 型のデータは,8 byte = 64 bit の情報量をもち, 連続したメモリ領域に, IEEE 倍精度浮動小数点形式のビット列(2進数)として記録される.
IEEE 形式のビット列は次の手順によって生成される:
具体例:実数 -10.5
int.c を改造し,確認してみよう:
$ cp int.c double.c $ vim double.c
ソース double.c:(double型の記録形式の確認)
#include <stdio.h> int main(void) { double data = -10.5; unsigned char *p; int i; p = (unsigned char *)&data; printf("%f\n", data); for (i = 0; i < 8; i++) { printf(" %02X", p[i]); } printf("\n"); return (0); }
実行結果は上の具体例の通りだろうか?
さらに,特殊な実数データ 0.0,1.0/0.0,および 0.0/0.0 を data に代入した場合の実行結果についても調べてみよう.
なぜ,0.0 が特殊なデータなのか? 普通じゃねーの? いや,これは 1.F の形にならないので, IEEE 形式としては例外のデータということになる.
また,○/0.0 については,数学的に考察すればわかるハズだ. まさか,1/0=0 だとか 0/0=0 などと思ってはいないよな?
たとえば,1/0 = x と考えると,0*x = 1 だ. どんな数でもゼロ倍したら 0 になるハズなので, それが 1 になってしまうとは,x はふつうの数ではない. x の値は何だ?
0/0 についても同様に考えよう.
... int main(void) { unsigned char data[4] = {0xCD, 0xAB, 0x34, 0x12}; unsigned int *p; ... printf("%08X\n", *p); ... }
$ ./int-2 ... 1234ABCD # リトルエンディアンの場合 # または CDAB3412 # ビッグエンディアンの場合
$ ./xbo 16進数 > 1234ABCD 12 34 AB CD # PCがリトルエンディアンの場合,ビッグエンディアン形式で表示 または CD AB 34 12 # PCがビッグエンディアンの場合,リトルエンディアン形式で表示
余裕のある人は,double.c の逆バージョンも作れるだろう.
提出: