要約
プログラムの整数は数学の整数ではない。それはビットの有限の並びであり、 ビットにはちょうど 個の異なる値が収まる。このレッスンでは、この表現とその帰結を学ぶ。整数を二進法と十六進法で書き、それを書式指定 {:b} と {:x} で確かめる方法を教え、C++ の整数型と、sizeof で測るその大きさ、std::numeric_limits で調べるその値の範囲を紹介する。負の数が 2 の補数でどう表されるか、値の範囲がなぜ対称でないかを説明し、符号付き整数と符号なし整数を区別して、演算が範囲の外に出たときに何が起こるかを調べる。符号なしでは規格が結果を定めており、それは を法とする算術である。符号付きでは規格は結果を定めず、プログラムはあらゆる保証の外に置かれる。最後に、各データの型を選ぶための基準を示す。
学習目標
- 整数を十進法、二進法、十六進法の間で変換し、その変換を書式指定
{:b}と{:x}で確かめる。 - ビットの整数の値の範囲を符号付きと符号なしについて計算し、
std::numeric_limitsとsizeofで確かめる。 - 2 の補数を説明し、小さな負の数の 8 ビットでの表現を書く。
- 未定義動作である符号付きのオーバーフローと、 を法とする算術である符号なしのオーバーフローを区別し、後者の結果を予測する。
- データの予想される値の範囲に応じて適切な整数型を選び、幅が重要なときには
<cstdint>の固定幅の型を使う。
位取り記数法
レッスン 変数とメモリ では、変数を名前・型・値をもつ区画として紹介した。しかし、メモリが保持するのは十進の数字ではなくビットであり、各ビットは 0 か 1 の値をとり、8 ビットずつバイトにまとめられている(規格は少なくとも 8 ビットを要求し、現在のすべてのシステムはちょうど 8 ビットを使う)。したがって、整数の変数がどんな値をとりうるかを知るには、二つの数字だけで数をどう書くかを知る必要があり、それを行うのが基数 2 の位取り記数法である。
基数 の位取り記数法では、各桁の値はそれが占める位置によって決まる。最後の桁には が掛かり、その前の桁には が、さらにその前の桁には が掛かり、以下同様である。十進法では、 は を意味する。二進法では数字は 0 と 1 であり、重みは 2 のべき乗である:。ここから、二進法から十進法へ移す規則が導かれる。1 がある位置の重みを足せばよい。
図は 8 ビットのパターン 00101101 を読む。各区画の上にはその重みが書かれている。1 が左から右へ一つずつ点灯し、値はその重みを積み上げていく。
証明
八つの区画はすべて消えている。値は 0 である。重みは、右から左へ である。
最初の 1 は重み 32 の位置にある。積み上げた値は 32 である。
続く 1 は重み 8 の位置にある:。
その次は重み 4 の位置にある:。
最後の 1 は重み 1 の位置にある:。パターン
00101101は数 45 を表す。ビットを四つずつまとめると、十六進法の数字が得られる。
0010は 2 であり、1101は 13 で、十六進法ではdと書く。したがって、45 は十六進法で2dと書かれる。
逆の道、つまり十進法から二進法へは、繰り返しの割り算でたどる。数を 2 で割って余り(0 か 1)を書き留め、商について同じことを 0 になるまで繰り返す。余りを最後のものから最初のものへ読むと、それが二進法の数字になる。45 の場合:、、、、、。余りを逆に読むと 101101 が得られ、左に 0 を二つ補えば図のパターンになる。その理由は、各余りが割られる数の一の位の数字であり、各商がその数を一桁右へずらしたものだからである。同じ方法はどの基数にも使える。基数 16 では 16 で割り、10 から 15 の余りは文字 a から f で書く。
基数 16、すなわち十六進法は便利さのために使われる。 なので、十六進法の各桁はちょうど 4 ビットに対応し、1 バイトは二桁で書ける。32 ビットの数は二進法では三十二桁を占めて読みにくいが、十六進法ではわずか八桁で、しかも計算なしに一桁ずつ二進法へ変換できる。そのため、プログラマはメモリを十六進法で読む。位取り記数法の数学的な基礎は、コース「代数と幾何 I」の回「位取り記数法」で学ぶ。
C++ では、これらの基数で直接書いた整数が使える。0b で始まるリテラルは二進法、0x で始まるものは十六進法である。どのリテラルの中にも、桁区切りとしてアポストロフィを入れることができ、コンパイラはそれを無視し、読み手はそれをありがたく思う:0b0010'1101、1'000'000。出力では、書式指定 {:b} は整数を二進法で書き、{:x} は十六進法で小文字を使って書き、{:X} は大文字を使って書く。記号 # は接頭辞を前に付け、0 を前に置いた幅は左を 0 で埋める。
#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} は八桁まで補う。
コンテンツを読み込んでいます…