コンピュータ上で負の数はどうやって表せばよいだろうか。 0と1だけを並べて負の数も含めた整数を表すにはいくつかの方式がある。 主な方式をあげると、「符号+絶対値方式」、「げたばき表現」、 「2の補数表現」の3つがある。
日常我々は符号と絶対値で表す。例: -3 は、符号の「-」と絶対値「3」か ら成る。これと同じ考え方を2進法でも使うことができる。
符号を表すのに1ビットあればよい。 つまり、符号があれば、それを1で表す。なければ、0で表す。
例えば、32ビット使って、正負の数を表したいとする。32ビットの内の 先頭の1ビットで符号を表す。残り31ビットで絶対値を表す。
-5 をそのようにして表すとする。 符号があるので、最初に 1 と書く。 絶対値の 5(10進) は、2進では101と書ける。 これだと3ケタしかないので、0を頭に補って31ビットにする。
つまり、31ビットで5を表すと、 0000000000000000000000000000101 先頭に、符号の1をつけると、 10000000000000000000000000000101
となる。これが、32ビット使った場合の「符号+絶対値」方式での-5の表現である。
注意: 全体で何ビット使うか決めておくか、頭の0を省略しないと決めておくことにしないと、この方式は使えない。 何故なら、頭の0を省略すると、110 は、先頭の1が符号で、残りの10が絶対値なのか、先頭の0(正の数であることを表す)が省略されていて、 絶対値が110なのか、区別がつかない。 (先頭の0が正の数を表すと決めた場合、それを省略すると、符号の情報が残らない。)
(例終り)
この方式の面倒な点は、+0 と -0 ができることである。 上記のように32ビットで表すとして、
00000000000000000000000000000000 が +0 10000000000000000000000000000000 が -0
となってしまう。 しかし、もちろん、-0 も +0 も数学的には 0 と同じなので、0を表す表現が2つあることになって何かとややこしい。
また、演算が案外楽でないという問題もある。特に加減算はあとで説明する2の補数表現を用いたときのほうが楽である。
メリットがあるとすれば、人間にとってはわかりやすいということがあげられる。 人間も普段符号と絶対値を並べて数を表しているからである。
8ビットの「符号+絶対値方式」で、負の整数 -71 を表しなさい。 紙の上で計算してみること。
げたばき表現とは、適当に決めた正の数(げた)を数値に加えることによって、 負の数を0以上の数に変換して表現するものである。
例として8ビット使うとする。正の数(と0)だけなら、0〜255まで表せる。 負数を含めるとして、例えば -128〜+127 を表すとしよう。 この場合、128だけげたをはかせることになる。
例えば、-3の表現は -3 + 128 = 125 になる。 この125を2進で表す。125 は2進で 01111101。 従って、げたを128とした時の8ビットのげたばき表現では、 10進の -3 は01111101と表される。 (当然、-129以下の数は表せない。というのは、-129 にげたの128を加えても -1 になり、0以上にならないから。)
逆に、げたばき表現から普通の10進表現に変換したければ、まず10進表現に直し、 げたを引けばよい。
例えば、上のようなげたばき表現を用いた時の 01010001 という表現を10進表現に戻してみよう。 01010001 を普通の2進数だと思って10進数に変換すると 81 である(64 + 16 + 1)。この81からげたの128を引くと -47 である。 これが求める10進表現である。
げたの大きさは、もちろん、変えることができる。 たとえば、げたを129にすれば、8ビットで表せる数の範囲は-129〜+126になる。
何ビットを用いて表すか、ということと、げたをいくつにするか、 ということを決めないとげたばき表現は決まらない。 (何人かの人の間で数を伝えるとして、 おたがいの間でビット数とげたを決めておかないと、 話が正しく伝わらないことになる。)
実際には(整数を表す時は)次の2の補数表現のほうが回路設計の点で有利なので、 げたばき表現はあまり使われない。
しかし、あとで小数の表現(整数の表現法とは全く違う)を学ぶ時に、表現の中の一部にげたばき表現を使う。
負の数を含めた整数を表現するのにコンピュータ上で最も良く用いられている表現法が「2の補数表現」である。 ちなみに、英語では、2's complement と言う。 2の補数表現では、全体で何ビットを使って数値を表すかを決めておかないと表現が決まらない。 従って、厳密には「8 ビットの2の補数表現」とか「32ビットの2の補数表現」と言わなければならない。
さて、-1をどうやって表現するか考えよう。 例としてここでは8ビットで表現することにする。
-1 は、0 から 1 を引いて得られる数である。 0は8ビットで 00000000 と表されるが、ここから強引に1を引くとどうなるだろうか? 仮にこの数の一番左に1がついていて 100000000 になっていると思って、 この1を借りてくることにすれば、強引に 1を 引くことができる。
仮につけた1 ↓ 100000000 - 1 ----------- 11111111
こうして求められた 11111111 を-1の表現と決めることにする。
11111111 に 1を足してみる。
11111111
+ 1
----------
100000000
8ビットの回路では、この9ビットの内、下の8ビットが残る。 つまり、00000000だけが残る。これは確かに0なので、-1の表現に 1 を 加えると 0 になったことになり、つじつまが合う。 (さっき無理に借りた 1 をここで返す(捨てる?)わけだ。)
-1 が 11111111 で表せたので、-2はこれより1小さい数と考え、 11111111から1を引けばよい。つまり、-2 は 11111110 で表せる。 同様に、-3 は 11111101 で表せる。 同じことをくり返して行くと、-128は10000000で表せることがわかるが、 これ以上小さい数は(8ビットでは)扱わないことにする。
表にしてみると以下のようになる:
10進 2の補数表現 ------------------------ 127 → 01111111 ……… 3 → 00000011 2 → 00000010 1 → 00000001 0 → 00000000 -1 → 11111111 -2 → 11111110 -3 → 11111101 ……… -128 → 10000000
これを見ると、負の数を2の補数表現にした場合だけ1番上のビットが1になることがわかる。 つまり、1番上のケタが1なら負の数だとわかる。 そこで、1番上のケタを符号ビットと言う。
符号+絶対値方式と異なり、符号ビットを除いた残りは絶対値を表さない。
ついでながら、1番上のケタを MSB (Most Significant Bit) という。 (Significant = 重要) 例えば、01111111 の MSB は0。 1番下のケタは、LSB (Least Significant Bit) という。 例えば、01111111 の LSB は1。
さて、ここまで来れば、-128 より小さい数を扱わない理由がわかったはずである。 -129を扱おうとすると、10000000 の一つ下の数だから 01111111 と表すべきだということになるが、符号ビットが0になってしまい、負の数を表したつもりが正の数に見えてしまう。
同じ理屈から、8ビットでは正の数は127までしか表せない。 127を8ビットの2の補数表現で表すと
01111111
になるが、128を表そうと思ってこれに無理やり 1 を足すと
10000000
になってしまう。 最上位(符号ビット)が1になってしまったからこれでは負の数の表現になってしまう。 -128の表現と重なってしまう!
この現象を確認したければ、例えば、ここにある char_test.c というプログラムをコンパイルして実行してみるとよい。
そういうわけで、 一般に2^(n-1)-1 より大きな数は n ビットの2の補数表現では表せない。
n ビットの2の補数表現では、-2^(n-1) から 2^(n-1)-1 までの 2^n 通りを表すことができる。 8ビットなら、上のように -128から127までの256通り。(-2^(8-1) = -2^7 = -128。) 16ビットなら、-32768 から 32767 までの 65536通り。
この-32768という数を覚える必要はない。 必要になれば、-2^15 を計算すればよい。
2進のデータのケタ数を拡張する処理はよく必要になるが、 2の補数表現を用いているときは拡張の際に注意が必要になる。
例: (1) 3 を2の補数表現で表すと、 8ビットでは 00000011 16ビットでは 0000000000000011 この場合、ケタ数を16ビットに増やしても頭に0が増えるだけだが… (2) -3を2の補数表現で表すと、 8ビットでは 11111101 16ビットでは 1111111111111101 16ビットにすると頭に1を補うことになる。正の数と負の数で事情が 違うことがわかる。
この時、上の(1)のように、 頭に0の列をつけ加えることでケタ数を拡張するのを「ゼロ拡張」と言う。 2の補数表現を使っている時に、 データの数値としての意味を変えずにケタ数を拡張するには、 上の(1),(2)のように正負に応じて場合を分けて、 負の数に対しては頭に0ではなく、1の列を補わないといけない。 このように符号を考慮してケタを拡張することを「符号拡張」と言う。 つまり、符号拡張では、MSB (最上位ビット)が0のデータに対してはゼロ拡張を行ない、 MSB が1のデータに対しては、頭に1ばかりからなる列を補って拡張する。
要するに、符号ビットをコピーしてケタを増やせばよい。 11111101 ↑ 一番上のケタ(符号ビット)を この部分にコピーする。 ↓↓↓↓ 1111111111111101
-x (x > 0)の (Nビットの)2の補数表現を簡単に求める方法として、 以下の(a), (b)がある。
(a) 2^N (N は全体のビット数)から x を(普通の引き算で)引く。 これでうまく行く理由: 要するに 0 から x を「無理に」引けばよいのだが、 上でやったように、仮に頭に1がついていたと考えると、 1のうしろに0が N 個ついたものになる。これは、本来 2^N を表す。
例: -3 を8ビットの2の補数表現で表してみる。 2^8 = 256 から 3 を引くと、253 になるが、これを(前に説明した 10進から2進への変換のやり方で)8ビットで表せば 11111101 にな る。 もちろん、10進で256から3を引いて253を求める代わりに、最初か ら2進で考えて、100000000 (2進)から 11(2進)を引いてもよい。
でもこのやり方はちょっとしんどい。普通、次の(b)のやり方のほうがよい。
(b) x がすでにNビットの2進表現になっていたら、 (ただし、もちろん、 -(2^(N-1)-1) ≦ x ≦ 2^(N-1)-1 ) まず全ビット反転する。(0を1に、1を0に変える。) それから1を加える。これででき上がり。 例えば、-3 の表現を考える。この場合 x は3。 3は 2進で 11。8ビットの場合を考える。8ビットだと00000011。 (注意. 必ずこのように必要に応じてケタを拡張してNビットにすること!) 全ビット反転すると、11111100。これに1を加えると11111101。これが答。
図式的にまとめると、
00000011 ↓ 全ビット反転 11111100 ↓ 1を加える 11111101
何故これでうまく行くか? (a)によると、-3の表現は、2^8 - 3 。
2^8 - 3 = 100000000(2進) - 3 = 11111111(2進) + 1 - 3 = 11111111(2進) - 3 + 1 = 11111111(2進) - 00000011(2進) + 1 = (00000011の全ビットを反転させたもの) + 1 = 11111100(2進) + 1 = 11111101(2進)
話をわかりやすくするために、上では x が正の場合を考えたが、 (b) の方法は、x が(2の補数表現で表された)負の数の場合でもうまく符号を反転させることができる。 実際、x を 11111011(= 10進の -5)として上の手続きを行なうと、
11111011 ↓ 全ビット反転 00000100 ↓ 1を加える 00000101
となって、確かに結果は00000101(= 10進の5)になる。
10進の負の整数を2の補数表現に変換するには、 上の (b) を利用するのが一番簡単である。
例題: 10進の負の整数 -62 を8ビットの2の補数表現に変換せよ。
解答: まず、62を2進法で表すと111110となるが、これを8ビットにゼロ 拡張すると 00111110 となる。この全ビットを反転すると 11000001 となり、それに 1 を加えると 11000010 となる。 よって答は 11000010。
これも上の (b) を利用すればよい。
例題: 8ビットの2の補数表現 10001011 を通常の10進数に変換せよ。
解答: 10001011 は符号ビットが立っているから負の数であることがわ かる。そこで、まず上の(b)を用いて符号を反転させてやる。 10001011 の全ビットを反転させると 01110100。これに 1 を加 えると 01110101 である。これを普通のやり方で2進から10進へ 変換すると 117 になる。符号を反転させた結果が 117 なのだか ら、元の数は -117 であったことがわかる。 よって答は -117。