前回は,複数のデータに対して, メモリ空間内での相互の位置関係について学習した.
空間内のどの領域を(倉庫内のどの辺りの箱を)使うのか?
変数 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」とか付けなくてよい.
なお,負の整数は, 補数表現によって, 正の整数としてメモリに記録される. この char型の 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)を持ちながら, 表面的には異なる値(表現,−1 or 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 用)等は ビッグエンディアンを採用していた.
さらに,最近の IoT 機器(スマホ,RasPi,Arduino,等)に多く採用されている ARM アーキテクチャな CPU では, バイトオーダを切替可能なバイエンディアン(bi-endian)となっている.
具体例:整数 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 のバイトオーダは何だと判断できるか?
ちなみに,バイナリデータの存在について,これまでは気にしていませんでした. これまでは見かけ上,テキストデータだけを取り扱ってきました. しかし実は,書式付き入出力関数 printf("%d", ...) とか scanf("%lf") とかって, バイナリとテキストとの間でデータ形式を変換しているんですよ.
最後に,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/0=0 だとか 0/0=0 などと思ってはいないよな?
たとえば,1/0 = x と考えると,0*x = 1 だ. どんな数でもゼロ倍したら 0 になるハズなので, それが 1 になってしまうとは,x はふつうの数ではない.
上記の例題 int.c では, int型のデータを unsigned char型の配列とみなすことによって, バイトオーダを調べている. これとは逆のアプローチ... unsigned char 型の配列を int型のデータとみなすことによって, バイトオーダを調べるためのプログラム int-2.c を作成せよ.
ヒント:
... int main(void) { unsigned char data[4] = {0xCD, 0xAB, 0x34, 0x12}; unsigned int *p; ... printf("%08X\n", *p); ... }
実行例:
$ ./int-2 ... 1234ABCD # リトルエンディアンの場合 # または CDAB3412 # ビッグエンディアンの場合
余裕のある人は,double.c の逆バージョンも作れるだろう.
互いにバイトオーダの異なるコンピュータ間でデータを共有/通信する場合, バイトオーダの変換処理が必要となる. もし,この変換が無いと,例えば,ある PC では 0x00 01 = 1 であった数値が, 別の PC では 0x01 00 = 256 と見なされてしまうことになる. そこで,int型 16 進数のバイトオーダを変換するプログラム xbo.c を作成せよ.
実行例:
$ ./xbo 16進数 > 1234ABCD 12 34 AB CD # PCがリトルエンディアンの場合,ビッグエンディアン形式で表示 # または CD AB 34 12 # PCがビッグエンディアンの場合,リトルエンディアン形式で表示
ヒント:16進数の入力には,scanf("%X", ...).
上の説明の通り,どのデータ型でも,表現できる数値の範囲には制限がある. これをより深く理解するために...
数値の範囲制限を超えるとどうなるか? オーバフロー(overflow)を発生させるプログラム of.c を自由に作成し,実験してみよう.
例:
質問 Q1〜Q5 に回答し,電子メールで提出せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」に変更せよ.