16 進数・2 進数・10 進数:プログラマーが実際に使う数の読み方
色は #FF5733。ファイルモードは chmod 755。権限チェックは flags & 0x04。定数は 0b1010、あるいは 0xFF、あるいは 255。どれも別々の方言に見えて、実は一つの数が服を着替えただけだ——そして、これらすべてを摩擦でなくするのに要る習熟は見た目より小さい。バイトには 16 進、ビットには 2 進、人には 10 進。
同じ数に、何度も違う服装で出会う。色は #FF5733。ファイルモードは chmod 755。権限チェックは flags & 0x04 と読める。定数は 0b1010、あるいは 0xFF、あるいは 255。どの記法もそれぞれ小さな方言に見え、それらの間を行き来しなければならないとき——0xFF は本当に 255 か? chmod 755 は思っている意味か?——変換ツールに手を伸ばすか、指を折って数える。これらすべてを摩擦でなくする習熟は見た目より小さく、そして一つの考えから始まる。
その考えはこうだ。16 進・2 進・10 進は三種類の数ではない——同じ数を書く三つの方法だ。255、0xFF、0b11111111 は一つの値の三つの記法であり、それはちょうど「十五」「15」「XV」が一つの値の三つの記法であるのと同じだ。数は基数を持つのではない。書かれた数が持つのだ。だから本当の技能は変換ではなく(それはツールが一打鍵でやる)、なぜ切り替えるかを知ることだ。その数の構造が見える記法を選ぶ。数が実はバイトの山であるとき(色、ハッシュ、アドレス)は 16 進。一つひとつのビットが意味を持つとき(フラグ、マスク、ハードウェア)は 2 進。人がただ個数を読むだけなら 10 進。
この記事は、基数とは何かを手短に説明し——同一性ではなく、まとめ方だ——それから、なぜ 16 進の 1 桁がちょうど 4 ビットなのか、それが 16 進を 2 進へのコンパクトな窓にする理由を見る。次に、実際に出会う場所——色、chmod、ビットマスク。それから、ビットが見えて初めて意味の通るビット演算と、見えないときに咬みつく落とし穴——符号付きの数、バイト順——を歩く。
唯一の核心:基数はまとめ方であって、数そのものではない
どの数も、何かの個数だ。基数とは、新しい桁を始める前にそれらをまとめて入れる「バケツ」の大きさにすぎない。10 進(base 10)では各桁が 10 のべき乗だ。255 = 2×100 + 5×10 + 5×1。16 進(base 16)では各桁が 16 のべき乗で、数字は 0–9、続いて十から十五を A–F で表す。0xFF = 15×16 + 15×1 = 255。2 進(base 2)では各桁が 2 のべき乗だ。0b11111111 = 128+64+32+16+8+4+2+1 = 255。毎回同じ 255 である。値はそのままで、基数は綴り方を決めるだけだ——だから「16 進を 10 進に変換する」は数を変えず、見方だけを変える。
16 進:バイトへのコンパクトな窓
プログラマーが 16 進に絶えず手を伸ばす理由は、一つのきれいな偶然に帰着する。16 は 2⁴ なので、16 進の 1 桁はちょうど 4 ビット——ニブル——であり、16 進の 2 桁はちょうど 1 バイト(8 ビット、0〜255、0x00〜0xFF)だ。その揃いこそ、その魅力のすべてである。10 進で書かれたバイト(77)はそのビットについて何も語らないが、同じバイトを 16 進で書けば(0x4D)二つのニブルにきれいに割れ、各ニブルはそのまま読み取れる 4 ビットに対応する。0x2F で自分でやってみるとよい。二つのニブル 2 と F に分け、1 バイト全体の 0010 1111 に展開し、立っている桁を足す——32 + 8 + 4 + 2 + 1 = 47。だから 16 進は実のところ人のための 2 進だ——バイトとビットの構造すべてを、4 分の 1 の桁数で。ハッシュ、色、メモリアドレス、バイトダンプが 16 進で書かれるのはこのためだ。それらはバイトの山であり、16 進はバイトを見せる。
2 進:一つひとつのビットが意味を持つとき
ときに欲しいのはコンパクトな眺めではなく、ビットそのものだ。一つひとつが何かを意味するからだ。それが 2 進、base 2 で、どの桁も一つのビット——オンかオフ、セットされているかクリアされているか——である。数が量ではまったくなく、互いに独立したスイッチの集まりであるとき、2 進へ降りる。権限の集合、フラグを詰めた整数、ビット 3 が何かを有効にしビット 6 が「エラー」を表すハードウェアレジスタ。そこでは 10 進の 13 は何も有用なことを告げないが、0b1101 はビット 0、2、3 が立っていると言う——それこそがその数が運んでいた本当の情報だ。
実際に出会う場所
三つの日常の例が、これらすべてを具体的に示す。
- 色は 16 進の 3 バイト。
#FF5733はFF 57 33——赤0xFF(255)、緑0x57(87)、青0x33(51)、それぞれ 0〜255 のバイトだ。#RRGGBBAAは不透明度のために 4 バイト目を足す。色を 3 バイトと見れば、#FFFFFF(全バイト最大 → 白)と#000000(全ゼロ → 黒)はもう魔法でなくなる。 - Unix の権限は 8 進——第三の基数。
chmod 755は 10 進でも 16 進でもない。8 進(base 8)で、各桁がちょうど 3 ビット、r w xだ。7は111= 読み+書き+実行、5は101= 読み+実行、書き込み不可。だから755は所有者/グループ/その他のrwx r-x r-x。8 進がここで生き残るのは、まさに「1 桁 3 ビット」が三つの権限ビットに揃うからだ。(現代のコードは0o755と書く。) - フラグは、ビット演算で調べるビット。一つの整数に詰めた選択肢の集合で、各選択肢は 2 のべき乗、だからちょうど 1 ビットを占める。
READ = 1 (0b001)、WRITE = 2 (0b010)、EXEC = 4 (0b100)。OR で組み合わせ(READ | WRITE→0b011)、AND で一つを調べ(flags & WRITEはそのビットが立っていれば非ゼロ)、反転との AND で一つを消す。flags & 0x04の0x04は、16 進の服を着たビット 2 にすぎない。
ビット演算:ビットが見えれば一目瞭然
これらはビットごとに働き、被演算子を 2 進か 16 進で書いた瞬間に、難解から自明へ変わる。
- AND(
&)——両方が 1 の位だけ 1。ビットを調べる・マスクするのに使う。x & 0xFFは下位バイトを残し、残りをゼロにする。 - OR(
|)——どちらかが 1 なら 1。ビットを立てるのに使う。x | 0x04はビット 2 をオンにする。 - XOR(
^)——ビットが異なるところが 1。ビットを反転するのと、いくつかの単純なチェックサムにも現れる。 - NOT(
~)——固定幅の中のすべてのビットを反転する。だから結果は型が何ビット幅かで変わる。 - シフト(
<<、>>)——ビットを左右へ滑らせる。符号なしでオーバーフローがなければ、x << 1は ×2、x << 8は値を丸ごと 1 バイト上げる。非負数ならx >> 4は下位 4 ビットを落とす。(符号ありの値では右シフトが符号拡張することがあり、正確な挙動は言語による——なのでビット操作は符号なしで行うのが安全だ。)シフトはフィールドを詰め・取り出す方法だ——(r << 16) | (g << 8) | bは三つの別々のバイトから色を組み立てる。
0b1100 & 0b1010 を試せば答えは見るからに 0b1000 だが、10 進で 12 & 10 を試せば謎かけになる。ビット演算の前に記法を切り替える理由は、まさにそれだ。
咬みつく落とし穴
この整理された見方が崩れる場面が二つある。どちらも知る価値がある。
- 符号あり対符号なし、そして 2 の補数。同じビットは符号なしとも符号ありとも読める。コンピュータは負数を2 の補数で格納し、固定幅の表現では最上位ビットが符号として働く——だから 8 ビットの 2 の補数では、バイト
0xFFは符号なしで255、符号ありで-1だ。これがあの古典的な 32 ビットオーバーフローの仕組みである。符号付き 32 ビットのカウンタは0x7FFFFFFFで頭打ちになり、もう一つ増えるとそれを越えて大きな負数へ向かう——だからこそ時刻を符号付き 32 ビット整数で保持する多くの Unix システムが 2038 年問題に突き当たる。(そのオーバーフローに言語がどう反応するかはさまざまで、回り込むもの、例外を投げるもの、未定義のものがある。) - バイト順(エンディアン)。数が複数バイトにまたがるとき、バイトはビッグエンディアン(上位から)でもリトルエンディアン(下位から)でも格納できる。値
0x01020304は、ビッグエンディアンのレイアウトでは01 02 03 04、リトルエンディアンでは04 03 02 01になる——だから「逆さ」に見えるバイトダンプは、たいてい壊れているのではなく、ただのリトルエンディアンだ。
小さな便利が二つ、締めに。接頭辞 0x(16 進)、0b(2 進)、0o(8 進)は、書かれた数が基数を自ら告げるために存在する——0x10 は 16、0b10 は 2、10 は十。そして 16 進や 2 進の先頭のゼロは単なる幅だ。0x0F と 0xF は同じ値で、余分なゼロは読みやすさのために 1 バイト分に詰めているだけである。
クイックリファレンス
実際のコードやデータで見慣れない数に出会ったら、計算する前にまず位置づける。
- 接頭辞を読む。
0xは 16 進、0bは 2 進、0o(古い C では先頭の0)は 8 進、接頭辞なし → 10 進。6 桁の 16 進の前の#→ RGB の色。 - 基数を構造に合わせる。バイト(色、ハッシュ、アドレス)→ 16 進。互いに独立したビット(フラグ、マスク、レジスタ)→ 2 進。人向けの個数 → 10 進。権限 → 8 進。
- 16 進 1 桁 = 4 ビット、2 桁 = 1 バイト。この一つの事実だけで、たいていの 16 進を頭の中でビットへ、そして戻せる。
- ビット演算の前に、まず 2 進か 16 進へ切り替える。それから AND で調べる/マスク、OR で立てる、XOR で反転、シフトでフィールドを動かす。
- 値がひどくおかしいときは符号とバイト順を疑う——小さいはずの巨大な数は、たいてい符号ありを符号なしとして読んだか、逆のエンディアンのバイトだ。(崩れた URL の中の
%XXの対も 16 進のバイトで、同じ技能で読める。)
暗算を越えるものはすべて——64 ビットのマスク、珍しい基数、2 の補数の値の確認——基数変換ツールが 16 進・10 進・2 進・8 進を並べて見せ、同じ値をあらゆる記法で一度に見られる。そのすべての底にあるのは、あの唯一の核心だ。これらは同じ数が服を着替えたものであり、習熟とは、どの服装が構造を見せるかを知ることにすぎない。