コンピュータが負の整数を表現する方法
私たちは「-5」と書けば負の数だとわかりますが、コンピュータは0と1しか扱えないため、マイナス記号「-」を直接表現する手段がありません。0と1だけで「この数は負です」と表すにはどうすればよいでしょうか? その答えが2の補数という仕組みです。現在のほぼすべてのコンピュータがこの方式を採用しています。
基本的なルールは2つだけです。
・最上位ビット(MSB)で正負を判定する:一番左のビットが0なら正の数、1なら負の数。8ビットなら左端の1ビットが符号、残り7ビットが値を表します
・負の数は「ビット反転+1」で作る:例えば+5(00000101)の負版を作るには、まず全ビットを反転して11111010、そこに1を足して11111011。これが-5です
実際に-5を作ってみましょう。
・+5のビット列:00000101
・全ビットを反転(0→1、1→0):11111010
・1を足す:11111011 ← これが-5
検算として、+5と-5を足してみると 00000101 + 11111011 = 100000000。先頭の1は8ビットからはみ出す(オーバーフロー)ので捨てると00000000 = 0。+5 + (-5) = 0 が正しく成立しています。
8ビットの2の補数で表せる範囲は-128〜+127です。
・正の側:00000000(0)〜 01111111(127)
・負の側:10000000(-128)〜 11111111(-1)
・合計:256個(2の8乗)の値を表現できます
上のスライダーで正と負の数を切り替えて、MSB(赤くハイライトされるビット)の変化とビット列全体がどう変わるか確認してみてください。
2の補数の求め方は「ビット反転して1を足す」の2ステップだけです。-5 を8ビットで求める手順を丁寧に追ってみます。
Step 1: 絶対値を2進数にする
5を2進数に変換します → 00000101
(8ビットなので先頭を0で埋めて8桁にします)
Step 2: 全ビットを反転する
0を1に、1を0に、すべてのビットをひっくり返します。
0 0 0 0 0 1 0 1 → 1 1 1 1 1 0 1 0
この時点の値を「1の補数」と呼びます。ここで止めると+0と-0の問題が出るため、もう1ステップ必要です。
Step 3: 1を足す
11111010 + 1 = 11111011
これが-5の2の補数表現です。
逆変換(2の補数 → 元の値)も同じ手順です。11111011 のビットを反転して 00000100、1を足すと 00000101 = 5。つまり「ビット反転+1」を2回やると元に戻ります。負の数が与えられたとき、同じ操作で絶対値がわかるのは非常に便利です。
もう1つ例を見てみましょう。-1 を8ビットで表すと?
・+1 = 00000001 → 反転 → 11111110 → +1 → 11111111
-1は全ビットが1になります。これは「最大値+1 = 0(オーバーフロー)」と考えると直感的です。上のツールで負の数を入力すると、この3ステップがアニメーションで確認できます。
「なぜビットを反転して1を足すと負の数になるのか?」。手順として覚えることはできても、なぜそうなるのかを理解しておくと、丸暗記より格段に応用が利きます。
鍵になるのは「時計の仕組み」です。4ビットで表せる値は 0〜15 の16通り。これをアナログ時計のように円に並べると、15の次は0(オーバーフロー)になります。この性質を使うと、「引き算」を「足し算」で表現できます。
・「5を引く」 = 「16 - 5 = 11 を足す」:どちらも時計を同じ量だけ動かすから
・5 + 11 = 16 → 4ビットではみ出した1を捨てると 0000 = 0
・つまり 11(= 1011)は、4ビットの世界での -5 として使える
この「16 - 5 = 11」を計算するとき、実は「全ビット反転 + 1」と同じ結果になります。なぜなら全ビット反転は「15から引く(15 - 5 = 10)」と等しく、そこに1を足すと「16 - 5 = 11」になるからです。
・15 = 1111(4ビット全部1)
・1111 - 0101(5)= 1010(10) → これが「全ビット反転」
・1010 + 1 = 1011(11) → これが「+1」
こうして「反転+1」は、nビットの最大数+1(繰り上がり = 0)から元の数を引いた結果を得る最短の手順なのです。
負の数を表す方法は他にも「符号ビット方式(先頭ビットで正負だけ変える)」や「1の補数」がありますが、現代のコンピュータはほぼすべて2の補数を採用しています。その最大の理由は引き算を足し算として処理できることです。
具体例で見てみましょう。「5 - 3」を計算したいとき、2の補数を使うと次のように変換できます。
・5 - 3 を 5 + (-3) と書き換える
・-3の2の補数は 11111101
・00000101 + 11111101 = 00000010(= 2)
普通に足し算するだけで、引き算の答えが正しく出ます。CPUの中に「引き算専用の回路」を作る必要がないのです。足し算回路1つで加減算の両方をこなせるため、ハードウェアがシンプルになり、計算速度の向上とコスト削減に直結します。
2の補数にはもう1つ重要な利点があります。「0」の表現が1つだけであることです。符号ビット方式では +0(00000000)と -0(10000000)の2つのゼロが存在し、「0と0は等しいか?」の判定が面倒になります。1の補数でも +0(00000000)と -0(11111111)が生まれます。2の補数では 00000000 が唯一のゼロで、この問題が発生しません。
ここからは、2の補数以外に考案された「負の数の表し方」を紹介します。2の補数がなぜ最善なのかは、他の方式の問題点を知ると一層よくわかります。
まずは最もシンプルな符号ビット方式(符号と絶対値)です。先頭の1ビットを「0なら正、1なら負」という符号に使い、残りのビットは絶対値をそのまま入れます。人間がマイナス記号を書くのと同じ発想です。
例えば8ビットの場合:
・+5 = 00000101(先頭0 = 正、残り7ビットで5)
・-5 = 10000101(先頭1 = 負、残り7ビットで5)
わかりやすいですが、致命的な問題が2つあります。
・+0と-0が存在する:00000000(+0)と 10000000(-0)。同じゼロなのに2種類あり、比較のたびに特別処理が必要
・加算回路で引き算ができない:5 + (-5) を単純に足すと 00000101 + 10000101 = 10001010(-10)で間違い。符号を見て場合分けする専用回路が必要になる
この問題を解決するために、1の補数、そして2の補数が考案されました。
1の補数は、符号ビット方式の「加算で引き算ができない」問題を改善した方式です。求め方はシンプルで、すべてのビットを反転する(0→1、1→0)だけです。
1の補数なら、足し算で引き算ができるようになります。5 + (-5) を試してみましょう:
・00000101 + 11111010 = 11111111
・11111111 は -0(全ビット1)。数学的には0なのでほぼ正解ですが、ここに問題があります。
1の補数の問題点:
・+0(00000000)と-0(11111111)の2つのゼロがまだ存在する。符号ビット方式と同じ問題が残っている
・桁上がり補正(エンドアラウンドキャリー)が必要:最上位ビットから桁上がりが出た場合、その1を結果に足し戻す追加処理が要る。例えば 3 + (-1) = 00000011 + 11111110 = 100000001 → 先頭の1を末尾に足して 00000010 = 2
この2つの問題を、「ビット反転の後に+1する」というたった1ステップの追加で解決したのが2の補数です。
負の数の表し方には歴史的に3つの方式がありますが、現在使われているのは2の補数だけです。なぜ2の補数が勝ち残ったのかが、この表で一目でわかります。
| 符号ビット方式 | 1の補数 | 2の補数 | |
|---|---|---|---|
| 求め方 | 先頭ビットを1に | ビット反転 | ビット反転+1 |
| -5 (8bit) | 10000101 | 11111010 | 11111011 |
| ゼロの数 | +0と-0の2つ | +0と-0の2つ | 1つだけ |
| 範囲 (8bit) | -127〜+127 | -127〜+127 | -128〜+127 |
| 加算で引き算 | できない | 補正が必要 | そのまま可能 |
| 現在の利用 | 使われない | ほぼ使われない | 標準 |
「1の補数と2の補数の違い」は、「2の補数 = 1の補数 + 1」と覚えると整理しやすいです。1の補数で止めると+0/-0の問題が残り、+1することでゼロが1つになり、加算補正も不要になります。
8ビットの2の補数で表せる範囲は -128 〜 +127 です。「あれ、-128まで行けるのに+128はないの?」と思うかもしれません。これは0が正の側に含まれるためです。
8ビットで表せる値の総数は 2の8乗 = 256個です。これを正と負に振り分けると:
・正の側:0, 1, 2, ... 127 の 128個(0を含む)
・負の側:-1, -2, ... -128 の 128個
合計 128 + 128 = 256個。0が正の側に入っている分、正の最大値は128ではなく127になります。
ビット数ごとの範囲はこうなります:
・4ビット:-8 〜 +7
・8ビット:-128 〜 +127
・16ビット:-32,768 〜 +32,767
・32ビット:約-21億 〜 約+21億
一般公式は -2n-1 〜 +2n-1-1 です。上のツールのビット数を切り替えて確認してみてください。
計算結果がその型で表せる範囲を超えてしまうことをオーバーフローと呼びます。日常に例えると、車のメーターが999,999kmの次に000,000kmに戻ってしまうのと同じ現象です。
4ビット(-8〜+7)で具体例を見てみましょう。
・3 + 4 = 7:+7は範囲内なので正常に計算できる
・5 + 4 = 9:+9は+7を超えている。ビット列で見ると 0101 + 0100 = 1001 = -7
正の数同士を足したのに結果が負の数になってしまいました。これがオーバーフローです。
オーバーフローが起きたかどうかは、簡単なルールで判定できます。
・正 + 正 → 負になった:オーバーフロー
・負 + 負 → 正になった:オーバーフロー
・正 + 負、負 + 正 → 絶対にオーバーフローしない(正と負を足すと結果は必ず間に収まる)
プログラミングで怖いのは、多くの言語(C、Javaなど)で整数のオーバーフローがエラーにならず、黙って間違った値を返すことです。127 + 1 が -128 になっても何の警告も出ません。大きな数を扱う可能性がある場合は、より大きな型(int → long)や多倍長整数(BigInteger)を使って対策します。
| ビット列 (8bit) | 符号なし (unsigned) | 符号付き (signed) |
|---|---|---|
| 00000000 | 0 | 0 |
| 00000101 | 5 | +5 |
| 01111111 | 127 | +127 |
| 10000000 | 128 | -128 |
| 11111011 | 251 | -5 |
| 11111111 | 255 | -1 |
同じビット列でも、符号付きと符号なしで解釈が変わります。 例えば 11111011 は、 符号なしでは 251、符号付きでは -5 です。
上のツールの「符号なし解釈」欄で、同じビット列の符号なし/符号付きの値を比較できます。 プログラミングでは C/C++ のintとunsigned intの違いがこれに対応します。 型を間違えると予期しない値になるので注意が必要です。
特に注意すべきは、符号付きと符号なしの値を混在させた比較です。 たとえばC言語で-1 < 1uは 直感的にはtrueですが、-1が符号なしに変換されて4294967295になるためfalseになります。 Javaでは符号なし整数型がないため問題は起きにくいですが、ビット演算を行う際には同様の注意が必要です。