コンテンツへスキップ
Topos Uranos

要約

プログラムの整数は数学の整数ではない。それはビットの有限の並びであり、nn ビットにはちょうど 2n2^n 個の異なる値が収まる。このレッスンでは、この表現とその帰結を学ぶ。整数を二進法と十六進法で書き、それを書式指定 {:b} と {:x} で確かめる方法を教え、C++ の整数型と、sizeof で測るその大きさ、std::numeric_limits で調べるその値の範囲を紹介する。負の数が 2 の補数でどう表されるか、値の範囲がなぜ対称でないかを説明し、符号付き整数と符号なし整数を区別して、演算が範囲の外に出たときに何が起こるかを調べる。符号なしでは規格が結果を定めており、それは 2n2^n を法とする算術である。符号付きでは規格は結果を定めず、プログラムはあらゆる保証の外に置かれる。最後に、各データの型を選ぶための基準を示す。

学習目標

  1. 整数を十進法、二進法、十六進法の間で変換し、その変換を書式指定 {:b} と {:x} で確かめる。
  2. nn ビットの整数の値の範囲を符号付きと符号なしについて計算し、std::numeric_limits と sizeof で確かめる。
  3. 2 の補数を説明し、小さな負の数の 8 ビットでの表現を書く。
  4. 未定義動作である符号付きのオーバーフローと、2n2^n を法とする算術である符号なしのオーバーフローを区別し、後者の結果を予測する。
  5. データの予想される値の範囲に応じて適切な整数型を選び、幅が重要なときには <cstdint> の固定幅の型を使う。

位取り記数法

レッスン 変数とメモリ では、変数を名前・型・値をもつ区画として紹介した。しかし、メモリが保持するのは十進の数字ではなくビットであり、各ビットは 0 か 1 の値をとり、8 ビットずつバイトにまとめられている(規格は少なくとも 8 ビットを要求し、現在のすべてのシステムはちょうど 8 ビットを使う)。したがって、整数の変数がどんな値をとりうるかを知るには、二つの数字だけで数をどう書くかを知る必要があり、それを行うのが基数 2 の位取り記数法である。

基数 bb の位取り記数法では、各桁の値はそれが占める位置によって決まる。最後の桁には b0=1b^0 = 1 が掛かり、その前の桁には b1b^1 が、さらにその前の桁には b2b^2 が掛かり、以下同様である。十進法では、20262026 は 2⋅103+0⋅102+2⋅101+6⋅1002 \cdot 10^3 + 0 \cdot 10^2 + 2 \cdot 10^1 + 6 \cdot 10^0 を意味する。二進法では数字は 0 と 1 であり、重みは 2 のべき乗である:1,2,4,8,16,32,64,128,…1, 2, 4, 8, 16, 32, 64, 128, \ldots。ここから、二進法から十進法へ移す規則が導かれる。1 がある位置の重みを足せばよい。

例二進法から十進法へ

図は 8 ビットのパターン 00101101 を読む。各区画の上にはその重みが書かれている。1 が左から右へ一つずつ点灯し、値はその重みを積み上げていく。

証明

  1. 八つの区画はすべて消えている。値は 0 である。重みは、右から左へ 1,2,4,…,1281, 2, 4, \ldots, 128 である。

  2. 最初の 1 は重み 32 の位置にある。積み上げた値は 32 である。

  3. 続く 1 は重み 8 の位置にある:32+8=4032 + 8 = 40。

  4. その次は重み 4 の位置にある:40+4=4440 + 4 = 44。

  5. 最後の 1 は重み 1 の位置にある:44+1=4544 + 1 = 45。パターン 00101101 は数 45 を表す。

  6. ビットを四つずつまとめると、十六進法の数字が得られる。0010 は 2 であり、1101 は 13 で、十六進法では d と書く。したがって、45 は十六進法で 2d と書かれる。

逆の道、つまり十進法から二進法へは、繰り返しの割り算でたどる。数を 2 で割って余り(0 か 1)を書き留め、商について同じことを 0 になるまで繰り返す。余りを最後のものから最初のものへ読むと、それが二進法の数字になる。45 の場合:45=2⋅22+145 = 2 \cdot 22 + 1、22=2⋅11+022 = 2 \cdot 11 + 0、11=2⋅5+111 = 2 \cdot 5 + 1、5=2⋅2+15 = 2 \cdot 2 + 1、2=2⋅1+02 = 2 \cdot 1 + 0、1=2⋅0+11 = 2 \cdot 0 + 1。余りを逆に読むと 101101 が得られ、左に 0 を二つ補えば図のパターンになる。その理由は、各余りが割られる数の一の位の数字であり、各商がその数を一桁右へずらしたものだからである。同じ方法はどの基数にも使える。基数 16 では 16 で割り、10 から 15 の余りは文字 a から f で書く。

基数 16、すなわち十六進法は便利さのために使われる。16=2416 = 2^4 なので、十六進法の各桁はちょうど 4 ビットに対応し、1 バイトは二桁で書ける。32 ビットの数は二進法では三十二桁を占めて読みにくいが、十六進法ではわずか八桁で、しかも計算なしに一桁ずつ二進法へ変換できる。そのため、プログラマはメモリを十六進法で読む。位取り記数法の数学的な基礎は、コース「代数と幾何 I」の回「位取り記数法」で学ぶ。

C++ では、これらの基数で直接書いた整数が使える。0b で始まるリテラルは二進法、0x で始まるものは十六進法である。どのリテラルの中にも、桁区切りとしてアポストロフィを入れることができ、コンパイラはそれを無視し、読み手はそれをありがたく思う:0b0010'1101、1'000'000。出力では、書式指定 {:b} は整数を二進法で書き、{:x} は十六進法で小文字を使って書き、{:X} は大文字を使って書く。記号 # は接頭辞を前に付け、0 を前に置いた幅は左を 0 で埋める。

例一つの数、三つの書き方
C++ のプログラム
#include <print>​int main(){    const int decimal{45};    const int binary{0b0010'1101};    const int hexadecimal{0x2d};    std::println("{} {} {}", decimal, binary, hexadecimal);    std::println("二進数:{:b}", decimal);    std::println("八ビット:{:08b}", decimal);    std::println("十六進数:{:x} {:X} {:#x}", decimal, decimal, decimal);}
出力
45 45 45二進数:101101八ビット:00101101十六進数:2d 2D 0x2d

三つの変数は同じ値を三つの基数で書いて保持しており、出力の 1 行目がそれを示している。基数は書き方の性質であって、数の性質ではない。メモリは三つの場合とも同じビットパターンを保持している。{:b} は左の 0 を省き、{:08b} は八桁まで補う。