CPUでの数値の内部表現を徹底解説:2進数、16進数って何だ?

コンピュータは現代社会のあらゆる場面で活躍していますが、その内部で数値がどのように表現され、処理されているかをご存知でしょうか?本記事では、CPUにおける数値の内部表現について、2進数や16進数などの実在する数値体系を交えながら詳しく解説します。

数え方の歴史と数値体系

私たちが日常的に使っているのは10進数です。これは、人間の指の数(10本)に由来すると言われています。0から9までの数字を使い、多桁の数を表現します。10進数は古代から使われており、算術や商取引など、日常生活のあらゆる場面で不可欠な存在です。

それに対して、CPU内部で使われているのは2進数であり、プログラミング言語では16進数が使われます。なぜ人間が使いやすい10進数ではないのでしょうか。(プログラミング言語では10進数も使えることが普通ですが、ここでは説明を省略します)

2進数、8進数、16進数

CPU内部で使われるのは2進数

CPUを構成しているのは主にトランジスタで、このトランジスタはスイッチのようにON/OFFを切り替えることができます。

ONを「1」、OFFを「0」として扱うことで、信号の明確な区別が可能になります。2進数はこの「0」と「1」の2つの状態を直接反映できるため、電子回路での実装が容易で信頼性も高く、CPUとの相性がとても良いわけです。技術的には3進数のような、電圧レベルによって値をいくつかに区切ることも可能ですが、回路も複雑になり、ノイズに弱くなってしまいます。

そのような理由から、CPU内部では2進数が使われているのです。

CPU内部では2進数を扱う論理回路が使われており、その組み合わせによって四則演算も可能になっています。

人間が把握しやすくしたのが16進数

2進数を4桁並べたものを1桁で扱えるのが16進数です。0~15が1つの桁で表現されるわけですが、当然10~15に相当する「1桁の数値」は存在しません。なので10~15についてはA~Fというアルファベットに置き換えて使います。Aが10、Bが11となっていくのでとっつきにくい印象ですが、暗記してしまえば難なく扱うことができます。

10進数2進数16進数
000
111
2102
3113
41004
51015
61106
71117
810008
910019
101010A
111011B
121100C
131101D
141110E
151111F

8進数は?他にはないの?

C言語などでは8進数表現が可能ですが、まず使われることはありません。16進数のようなわかりにくさはそのままに、10進数よりも表現の効率が悪いからです。C言語では先頭桁に0を付記すると8進数として解釈されてしまうことに注意が必要です。例えば「022」と書くと、8進数になってしまいます。あまりにも使われないため可読性を損ねることもあり、コーディング規約で禁止されているところもあるほどです。

さて、さらに表現の効率を高めるために、32進数や64進数は使われないのでしょうか?

結論からいうと、今後も使われることはないでしょう。32進数となると10~31までをアルファベットで置き換えてもVまで使うことになり、大変ややこしい上に、16進数と比べると2進数の1桁の違いでしかないためメリットが薄いからです。64進数ともなるとアルファベット全種類を使っても表現できないので、「じゃあ記号でも使うの?」となり、さらにややこしくなります。

ビットとバイト

CPU内部では2進数を使うことは既に説明したとおりです。ではよく聞く「ビット」と「バイト」は一体何なのでしょうか?

CPUが扱う最小の単位は、電圧の高低、つまり1か0か、です。この最小単位を「ビット」といいます。つまり2進数の1桁がビットです。表現を増やすには桁数を増やす必要がありますが、ビットを並べていって8桁になったとき、これを「バイト」といいます。

つまり、1バイト(byte)=8ビット(bit)です。

1バイトはなぜ8ビットなのか

「バイト」というのは、かつて1文字を指す単位でした。これは、ASCIIというコード体系により、文字と数値の相互変換ができるようになったからです。ASCIIコードは0~127までを使っていましたが、拡張されて0~255まで使われるようになり、数字だけでなく制御文字、簡単な記号も数値で表現できるものでした。この範囲の数値を扱えるのが2進数8桁、つまり8ビットだったわけです。

制御文字はタイプライターに由来するもので、カーソルの移動や改行、バックスペースなどがあります。

英語圏ではこれで文字のバリエーションにも問題ありませんでしたし、日本語圏でもカタカナまでであればASCIIコードをやりくりしてなんとかしていました(「半角文字」というのはこのときの名残です)。

日本で使われるC言語のコンパイラではShift-JISまでサポートしていましたが、UTF-8の普及によりエディタとの互換が取れなくなったこともあり、C言語などのレガシーな言語ではASCIIまでを標準サポートし、UTF-8はライブラリで対応するようになりました。

少し話が脱線してしまいましたが、1バイトが8ビットなのは「かつての1文字分だから」ということになります。現在でも、記憶域のサイズは「(英語圏でいう)文字数」をベースにしているため単位が「バイト」になっています。

大きい数値の扱い

技術が進歩していくと、加速度的に大きな数値を扱えるようになっていきました。

そこで用いられるのがキロ(k)、メガ(M)、ギガ(G)といった接頭辞です。通常の算術ではキロは1000、メガは100万ですが、コンピューターの世界ではこれらは中途半端な数値になるため、キロは1024(2の10乗)、メガは1048576(2の20乗)のように2の10乗ごとに区切られます。10進数だと覚えにくい上、暗算する必要もないので「2の10乗ごと」、もしくは「1024のべき乗」と覚えておくとよいでしょう。

なお、これらは単位の書き方によって区別することが推奨されており、キロバイトを「kB」と書くとキロは1000ですが「kiB」とiをつけると2進数ベースとなり、キロは1024になります。

負の数はどうするのか

16進数表記では、末尾に「h」をつけることがお約束ですが、例えばFFhとあったとき、これを10進数にするといくつになるのでしょうか。

それは「解釈による」が正解です。

C言語では「型」があり、扱えるビット数が異なります。char型なら8ビットですし、short型なら16ビット、long型なら32ビットです。FFhを解釈する場合、16ビット以上のサイズで扱うなら、10進数で255になります。

しかしchar型の場合、符号の有無で解釈が異なります。符号無しなら255、符号ありなら-1です。

一見、大きな数値に見えるFFhが、負数になったとたん-1になりました。これはどういうことでしょうか。

「1の補数」と「2の補数」

符号ありの場合、最上位ビットが1だと負数、という解釈をします。そこまでは一緒なのですが、1の補数と2の補数ではその後の変換が少し違います。

  • 1の補数:全ビットを反転したものが絶対値となる。FFhであれば反転して00hとなり、これは-0を意味する
  • 2の補数:全ビットを反転後、1を加えたものが絶対値となる。FFhであれば反転して00h、+1して1になり、負数なので-1を意味する

人間にとっては「2の補数」は何やら面倒なイメージですが、CPUの内部で負数を扱う場合、「2の補数」を使います。「1の補数」では0に正負の2通りが存在してしまい、演算が複雑になるからです。

反面、「2の補数」では「引き算をする回路が不要になる」という大きなメリットがあります。つまり、引き算を「負数の足し算」にし、足し算のための回路がそのまま使えるわけです。

例えば、1-1=0という計算をしてみましょう。これは1+(-1)=0と書き換えられます。8ビットの16進数にすると、01h+FFhとなり、これはそのまま加算すれば0になります。引き算が不要になる理由がわかると思います。もちろん、0に正負の2通りが存在することもありません。

まとめ

CPU内部で扱われる「数値」について解説しました。

数値の内部表現を理解することで、コンピュータの動作原理やプログラミングの深い部分まで理解できます。これは、効率的で信頼性の高いシステムの構築や、バグの原因追及に役立ちます。

負数の表現である「2の補数」は情報処理試験にも頻出するものなので、是非抑えておいてください。

  • B!