🎓 レベル:基礎 | 重要度:A(必須)
📎 前提:なし(この章の出発点) | 関連:CPUと命令実行・メモリ階層とキャッシュ
要点(BLUF)
- コンピュータが扱えるのは 0と1(ビット)だけ。整数も小数も文字も、すべて「ビット列をどう解釈するか」という**約束(エンコーディング)**で成り立つ。
- 整数は 2の補数で表すと、足し算回路ひとつで引き算もできる。これが「オーバーフローで巻き戻る」現象の正体。
- 小数は IEEE754で「符号・指数・仮数」に分けて表す。2進で割り切れない
0.1は近似値になり、0.1+0.2 != 0.3になる。
概念 ── ビットは「意味のない0/1」、解釈が意味を与える
1バイト(8ビット)の 11101001 という並びは、それ自体には意味がありません。これを「符号なし整数」と読めば233、「符号付き整数」と読めば-23、「ある文字コードの1バイト」と読めば別の文字。同じビット列でも、解釈する型が違えば別の値になります。
要するに、データ表現とは「ビット列 ↔ 人間が扱う値」の変換ルールです。CPUやプログラミング言語の「型」は、このルールを指定する札に過ぎません。
仕組み① ── 整数と2の補数
正の整数は素直に2進数です。問題は負の数で、現代のCPUはほぼ例外なく 2の補数(two’s complement) を使います。8ビットなら「-x を 256 - x のビット列で表す」と決めます。
要するに2の補数は、「最上位ビットの重みだけをマイナスにした」位取りです。8ビットなら最上位の重みが -128、残りは +64, +32, …, +1。だから 10000000 は -128、11111111 は -128+64+…+1 = -1 になります。
なぜこの面倒な表現か。引き算を足し算で実装できるからです。a - b を a + (-b) として、負数も同じ加算回路に流せる。符号の場合分け回路が要らず、ハードが単純になります。これが「設計の理由」です。
検証してみます([[#対応ラボ]] の 01-01_data_representation.py)。
for x in (-1, -128, 127):
bits = x & 0xFF # 下位8ビットだけ取り出す=2の補数表現
print(f"{x:4d} -> 0b{bits:08b} = 0x{bits:02X}")
ov = (127 + 1) & 0xFF
signed = ov - 256 if ov >= 128 else ov
print("127+1 を8ビットで解釈 ->", signed)
実行結果(実機):
-1 -> 0b11111111 = 0xFF
-128 -> 0b10000000 = 0x80
127 -> 0b01111111 = 0x7F
127+1 を8ビットで解釈 -> -128
127 + 1 が -128 に「巻き戻った」のがオーバーフローです。8ビットの世界は -128〜127 の輪(時計の文字盤)になっていて、上限を超えると下限に回り込みます。C言語の signed char で起きるバグはこれです。
仕組み② ── 浮動小数点(IEEE754)
小数は IEEE754 という規格で、値 = 符号 × 仮数 × 2^指数 の形に分解して格納します。単精度(float32)なら 符号1ビット・指数8ビット・仮数23ビットです。
flowchart LR
S["符号 s (1bit)"] --> E["指数 e (8bit)"] --> M["仮数 m (23bit)"]
classDef box fill:#eef,stroke:#557
class S,E,M box
ここで重要なのは、2進の小数で割り切れる数は限られること。0.5 = 2^-1、0.25 = 2^-2 は表せますが、0.1 は 1/10 で、2進では循環小数になり割り切れません。だから格納時に最も近い値へ丸められます。
import struct
b = struct.pack(">f", 0.1)
print("0.1 の float32 ビット列:", b.hex())
print("float32 に丸めた 0.1 =", repr(struct.unpack(">f", b)[0]))
print("0.1+0.2 == 0.3 ?", (0.1 + 0.2) == 0.3, " 実際:", 0.1 + 0.2)
実行結果(実機):
0.1 の float32 ビット列: 3dcccccd
float32 に丸めた 0.1 = 0.10000000149011612
0.1+0.2 == 0.3 ? False 実際: 0.30000000000000004
0.1 + 0.2 が 0.3 ぴったりにならないのは、Pythonのバグではなく2進浮動小数の根本的な性質です。だから金額計算には浮動小数を使わず、整数(最小単位を「円」でなく「銭」にする等)や十進小数型を使います。
仕組み③ ── 文字コード
文字も番号(コードポイント)に対応づけ、それをバイト列に符号化します。今の標準は Unicode + UTF-8。ASCII範囲(英数記号)は1バイトのまま、日本語などは複数バイトで表します。
s = "亜A"
print("UTF-8 バイト列:", s.encode("utf-8").hex(), "バイト数:", len(s.encode("utf-8")))
print("'A':", ord("A"), " '亜':", ord("亜"))
実行結果(実機):
UTF-8 バイト列: e4ba9c41 バイト数: 4
'A': 65 '亜': 20124
亜 が3バイト(e4 ba 9c)、A が1バイト(41)。「文字数」と「バイト数」は別物で、これを混同すると文字化けやバッファ溢れの原因になります。
仕組みの直観 ── なぜこの設計か
- 2の補数:負数を別扱いせず、加算回路を使い回せる(ハード最小化)。「ゼロが1つしかない」ことも利点(符号付き絶対値方式は +0 と -0 が生じる)。
- IEEE754:固定小数点では「巨大な数」と「極小の数」を同時に扱えない。指数部で桁を動かす(=浮動)ことで、広いダイナミックレンジを限られたビットで表す。代償が丸め誤差。
- UTF-8:ASCIIと後方互換を保ちつつ、世界中の文字を可変長で表す。英語圏のデータは1バイトのまま軽い。
⚠️ よくある誤解・落とし穴
- 「
intは無限の整数」→ 固定幅(多くは32/64ビット)。境界を超えれば巻き戻る。Pythonのintは例外的に多倍長。 - 「浮動小数の誤差は精度を上げれば消える」→ 2進で割り切れない数は何ビットあっても割り切れない。等値比較(
==)を避け、許容誤差で比較する。 - 「1文字 = 1バイト」→ UTF-8では誤り。
len(文字列)とlen(バイト列)は一致しない。 - 「16進は別の数」→ 16進は2進の表記の省略形(4ビット=1桁)。値は同じ。
対応ラボ
cs-foundations-study/labs/01-01_data_representation.py(実行して上記の出力を確認済み)。
- 確認できること:2の補数とオーバーフロー、
0.1の丸め、UTF-8のバイト長
関連
- 次に CPUと命令実行 で、このビット列(命令とデータ)をCPUがどう処理するかを見る
- メモリ上の並び順(バイトオーダ)は 入出力とバス・割り込み でも顔を出す
- ハッシュや基数の話は 基本データ構造 に繋がる