デジタル道具屋
デジタル道具屋
記事一覧へ戻る
更新: 読了 約 5 分

進数変換とビット演算 - 「なぜ16進数なのか」を実務目線で理解する

#development

執筆: ありがっさまりょうた

デジタル道具屋を個人で開発・運用しています。記事は実装時の検証結果と公式仕様を照合して執筆しています。

本記事の執筆方針と編集ポリシーについてはAbout ページをご覧ください。

導入:16進数を読む場面は意外に多い

デバッグ中に 0xDEADBEEF のような値を目にすることがあります。これは「DEAD BEEF」と読める語呂合わせの値で、 メモリの初期化パターンや未使用領域のマーカーとして古くから使われてきました。 16進数をそのまま読めると、こうした値の意味をすぐに判断でき、デバッグの効率が上がります。

Web 開発でも 16進数は身近です。CSS のカラーコード #FF0000 は 16進数ですし、 Unicode の文字コードも U+3042 のように 16進数で表記されます。 「読めると便利」というより、文字化けやカラー計算の調査では「読めないと原因にたどり着けない」場面が少なくありません。

1. なぜ「16進数」なのか

コンピュータは内部的に 2進数で動作しますが、人間が 2進数の並びを直接読むのは負担が大きいものです。 同じ値でも 11111111(2進数)より FF(16進数)の方が桁数が少なく、誤読も減ります。 16進数は 4ビットをちょうど 1桁で表せるため、2進数との対応が機械的に取れる点が最大の利点です。

16進数が好まれる理由
  • 4ビット = 1桁:1バイト(8ビット)が 2桁できれいに表現できる
  • 視認性:32 ビット値が 8桁、64 ビット値が 16桁。一目でビット幅がわかる
  • 歴史的経緯:CPU レジスタの幅、メモリアドレス、文字コード表など、低レイヤの世界では事実上の標準

JavaScript では Number.prototype.toString(radix) と parseInt(string, radix) で相互変換できます。 次の例は、10進数の 255 が 2進数では 8桁、16進数では 2桁になることを示しています。

(255).toString(2)    // → "11111111"
(255).toString(16)   // → "ff"
(255).toString(8)    // → "377"

parseInt('11111111', 2)  // → 255
parseInt('ff', 16)       // → 255
parseInt('0xff', 16)     // → 255("0x" 接頭辞は基数 16 のとき許容される)

8進数(octal)は、Unix のファイルパーミッション 0755 や chmod 644 で今も現役です。 3ビット = 1桁の対応で、rwx の3ビットを表現するのに都合が良かったため残っています。

2. ビット演算で書ける「権限フラグ」

権限管理をデータベースで設計する際、can_read、can_write、can_delete のように 権限ごとに列を追加する方法があります。権限の種類が少ないうちはこれで十分ですが、 フラグが 10 個、20 個と増えていく場合は、整数 1 つの各ビットにフラグを割り当てる方法の方がスキーマも処理も単純になります。

const READ    = 0b0001; //  1
const WRITE   = 0b0010; //  2
const DELETE  = 0b0100; //  4
const ADMIN   = 0b1000; //  8

// 「読み書き可能、削除不可、管理者でない」
const perm = READ | WRITE; // → 3 (0b0011)

// 権限チェック
const canDelete = (perm & DELETE) !== 0; // → false
const canWrite  = (perm & WRITE) !== 0;  // → true

// 削除権限を付与
const newPerm = perm | DELETE; // → 7 (0b0111)

// 削除権限を剥奪
const restored = newPerm & ~DELETE; // → 3

この方式の利点は次の 2 点です。

  • 保存が単純:データベースには整数 1 列を持つだけで済み、フラグを追加してもスキーマ変更が不要
  • 判定が高速:権限チェックは AND 演算 1 回で完了し、複数列を参照する必要がない

Linux のファイルパーミッション(rwx を 3 ビットで表現する)も、これと同じ仕組みで動いています。

注意:JavaScript のビット演算は 32bit まで

JavaScript のビット演算子は符号付き 32bit 整数に変換して動作します。 32 ビット目(最上位)に立ったビットは負数として扱われ、思わぬ結果になります。 例えば 1 << 31 の結果は 2147483648 ではなく -2147483648 です。 32 個を超えるフラグを扱う場合は BigInt を使うか、複数の整数に分割しましょう。

3. 文字コードを読み解く

絵文字や漢字が文字化けして「?」になったとき、原因究明には Unicode コードポイントを 16進数で読む力が要ります。

'あ'.codePointAt(0).toString(16)
// → "3042" (Unicode U+3042)

'😀'.codePointAt(0).toString(16)
// → "1f600" (Unicode U+1F600)

// UTF-8 では何バイト?
new TextEncoder().encode('あ').length    // → 3
new TextEncoder().encode('😀').length    // → 4

「絵文字を文字数カウントしたら 2 になった」「DB の VARCHAR に絵文字が入らない」といった問題は、 サロゲートペアを 1 文字として扱えていないことが原因です。 JavaScript の文字列は UTF-16 で表現されるため、U+FFFF を超えるコードポイントは 上位サロゲート(0xD800〜0xDBFF)と下位サロゲート(0xDC00〜0xDFFF)の 2 つのコードユニットの組で格納されます。

'😀'.length                       // → 2(UTF-16 コードユニット数)
[...'😀'].length                  // → 1(コードポイント数)
'😀'.charCodeAt(0).toString(16)   // → "d83d"(上位サロゲート)
'😀'.charCodeAt(1).toString(16)   // → "de00"(下位サロゲート)

文字数を数える際は length ではなく、スプレッド構文や Array.from でコードポイント単位に分割してから数えると、 絵文字を含む文字列でも期待どおりの結果になります。

4. 進数変換が日常で必要になるシーン

実務シーン例
  • カラーコード:HEX #FF6347 を RGB (255, 99, 71) に
  • 権限・ステータスフラグ:DB に保存された整数からビットを読み取る
  • ネットワーク:IPv4 サブネットマスク、IPv6 アドレス
  • セキュリティ:ハッシュ値、JWT のヘッダ・ペイロードを Base64URL でデコード
  • 組込み・低レイヤ:レジスタ値の読解、メモリダンプ

5. 実際に変換してみる

即座に進数を変換するなら、進数変換ツールが便利です。 2進・10進・16進・8進の相互変換に加え、入力した数のビットパターンも視覚的に確認できます。

関連: カラーコード変換(HEX↔RGB)、ハッシュ生成(生成結果は通常 16進数文字列)。

よくある失敗パターン

JavaScript で parseInt('08') がブラウザによって 0 を返した時代がありました。 ES5 より前の仕様では、先頭が 0 の文字列を 8進数として解釈することが許容されていたためです。 第2引数(基数)を必ず明示する parseInt('08', 10) の習慣を付けると、 旧仕様への後方互換が原因のバグを未然に防げます。

参考にした一次情報

本記事の内容は、以下の公式仕様や一次情報を参照して執筆しています。