DevKitLab Logo DevKitLab
基数 / 16 進数 / 2 進数

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 進は三種類の数ではない——同じ数を書く三つの方法だ2550xFF0b11111111 は一つの値の三つの記法であり、それはちょうど「十五」「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、0x000xFF)だ。その揃いこそ、その魅力のすべてである。10 進で書かれたバイト(77)はそのビットについて何も語らないが、同じバイトを 16 進で書けば(0x4D)二つのニブルにきれいに割れ、各ニブルはそのまま読み取れる 4 ビットに対応する。0x2F で自分でやってみるとよい。二つのニブル 2F に分け、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 バイト#FF5733FF 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 だ。7111 = 読み+書き+実行、5101 = 読み+実行、書き込み不可。だから 755 は所有者/グループ/その他の rwx r-x r-x。8 進がここで生き残るのは、まさに「1 桁 3 ビット」が三つの権限ビットに揃うからだ。(現代のコードは 0o755 と書く。)
  • フラグは、ビット演算で調べるビット。一つの整数に詰めた選択肢の集合で、各選択肢は 2 のべき乗、だからちょうど 1 ビットを占める。READ = 1 (0b001)WRITE = 2 (0b010)EXEC = 4 (0b100)。OR で組み合わせ(READ | WRITE0b011)、AND で一つを調べ(flags & WRITE はそのビットが立っていれば非ゼロ)、反転との AND で一つを消す。flags & 0x040x04 は、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 進の先頭のゼロは単なるだ。0x0F0xF は同じ値で、余分なゼロは読みやすさのために 1 バイト分に詰めているだけである。

クイックリファレンス

実際のコードやデータで見慣れない数に出会ったら、計算する前にまず位置づける。

  1. 接頭辞を読む0x は 16 進、0b は 2 進、0o(古い C では先頭の 0)は 8 進、接頭辞なし → 10 進。6 桁の 16 進の前の # → RGB の色。
  2. 基数を構造に合わせる。バイト(色、ハッシュ、アドレス)→ 16 進。互いに独立したビット(フラグ、マスク、レジスタ)→ 2 進。人向けの個数 → 10 進。権限 → 8 進。
  3. 16 進 1 桁 = 4 ビット、2 桁 = 1 バイト。この一つの事実だけで、たいていの 16 進を頭の中でビットへ、そして戻せる。
  4. ビット演算の前に、まず 2 進か 16 進へ切り替える。それから AND で調べる/マスク、OR で立てる、XOR で反転、シフトでフィールドを動かす。
  5. 値がひどくおかしいときは符号とバイト順を疑う——小さいはずの巨大な数は、たいてい符号ありを符号なしとして読んだか、逆のエンディアンのバイトだ。(崩れた URL の中の %XX の対も 16 進のバイトで、同じ技能で読める。)

暗算を越えるものはすべて——64 ビットのマスク、珍しい基数、2 の補数の値の確認——基数変換ツールが 16 進・10 進・2 進・8 進を並べて見せ、同じ値をあらゆる記法で一度に見られる。そのすべての底にあるのは、あの唯一の核心だ。これらは同じ数が服を着替えたものであり、習熟とは、どの服装が構造を見せるかを知ることにすぎない