整数表現と2の補数・オーバーフロー
桁あふれや符号バグの原因を内部表現から理解でき、ラップアラウンドや未定義動作の地雷を踏まなくなる。2の補数の仕組みと飽和演算・ビット演算の落とし穴を原理から解説。
- 1.2の補数は最上位ビットだけ重みを負にした表現で、加減算は符号の有無に関係なく同じ回路で計算でき、ゼロが一意になるのが利点。
- 2.桁あふれ時、符号なしは2のn乗を法とするラップアラウンドが定義されるが、符号付きのオーバーフローはC/C++では未定義動作で最適化に消される。
- 3.右シフトは符号付きで算術/論理が分かれ、負数の割り算とビット演算は丸め方向や符号拡張で結果がずれる。飽和演算は範囲端で値を張り付かせる別の選択肢。
整数はビット列をどう値に対応づけるか
CPUが扱うのは固定幅のビット列にすぎません。nビットの並びに「どんな値を割り当てるか」という解釈のルールが整数表現です。同じ 1111 1111(8ビット)が、符号なしなら 255、符号付き2の補数なら -1 になるように、ビット列と数値は1対1ではなく解釈次第で変わります。型はこの解釈を固定する札であり、扱いの違いは 変数とデータ型 の延長線上にあります。
符号なし整数は素直で、各ビットに 2^0, 2^1, ..., 2^(n-1) の重みを与えて足すだけです。nビットで 0 から 2^n − 1 までを表します。問題は負数をどう表すかで、ここで歴史的に2の補数が勝ち残りました。
2の補数の幾何的意味
2の補数は「最上位ビット(MSB)だけ重みを負にする」表現です。8ビットなら最上位の重みが +128 ではなく −128 になり、値は次のように復元されます。
符号なし: b7·128 + b6·64 + ... + b0·1
2の補数 : b7·(−128) + b6·64 + ... + b0·1
この一手間が効くのは、値を法 2^n の円環(mod 2^n)上の点として扱える点です。8ビットの値を時計の文字盤のように 0..255 で円に並べ、128 以上を負側へ読み替えると -128..127 になります。-1 は 0 の1つ手前なので 1111 1111、加算は円周上を時計回りに進む操作になります。だから -a は「補数(全ビット反転して1を足す)」で求まり、5 + (-5) が桁あふれを伴って自然に 0 へ戻ります。
符号ビット方式(MSBを単なる符号)や1の補数(反転のみ)では +0 と −0 の2つのゼロが生じ、比較や分岐に余計な処理が要ります。2の補数はゼロが一意で、しかも加減算回路を符号なしと共有できるため、ハードウェアが単純になります。これが標準になった決定的な理由です。
最大の実利は、加減算が符号の有無を区別しないことです。a + b のビット演算は、両者を符号なしと見ても符号付きと見ても同じビット列を生む。CPUのALUは1種類の加算器で両方を賄え、減算は a + (~b + 1) に帰着します。
オーバーフロー:ラップアラウンドと未定義動作
固定幅では表現範囲を超える結果が必ず起こり得ます。重要なのは、符号なしと符号付きで言語仕様上の扱いがまったく違うことです。
| 種別 | 範囲(8bit例) | 桁あふれ時の挙動 |
|---|---|---|
| 符号なし | 0 〜 255 | 2^n を法とするラップアラウンド(well-defined) |
| 符号付き(C/C++) | −128 〜 127 | 未定義動作(UB)。何が起きても規格上は許される |
| 符号付き(Java/Rust release) | −128 〜 127 | 2の補数ラップが定義(Rust debugはpanic) |
符号なしの 255 + 1 は 0 に戻り、これは規格が保証する動作です。一方、C/C++での符号付きオーバーフロー(例 INT_MAX + 1)は未定義動作で、ラップするとは限りません。コンパイラは「符号付き演算はあふれない」と仮定して最適化するため、a + 1 > a のようなチェックが常に真と判断されて丸ごと削除されることがあります。
// 危険:i + 1 がオーバーフローしない前提で最適化され、
// ループが無限化したり境界チェックが消えたりしうる
for (int i = 0; i <= n; i++) { ... } // n が INT_MAX のとき UB
// 安全:符号なしか、あふれを起こさない比較に直す
for (int i = 0; i < n; i++) { ... }
この「最適化に消える」性質が UB の怖さで、クラッシュよりも静かに条件分岐が書き換わる方が実害は大きい。境界チェックを ptr + len < ptr のように書くと、コンパイラがそれを偽に畳んでセキュリティホールになった実例があります。検出には -fsanitize=signed-integer-overflow や、あふれを返り値で知らせる __builtin_add_overflow を使います。
C系では int と unsigned を混ぜると符号付きが符号なしへ格上げされます。-1 < 0u が false になるのが典型で、-1 が 0xFFFFFFFF(=最大値)として比較されるためです。size_t の引き算で負を期待した結果が巨大な正数に化けるバグはこの変換が原因です。
シフトとビット演算の落とし穴
ビット演算は2の補数の解釈と密結合しており、符号で挙動が割れます。
- 右シフト:符号なしや論理右シフトは上位に
0を詰める。符号付きの算術右シフトは符号ビットを複製して詰める(符号拡張)。-8 >> 1が算術なら-4、論理なら巨大な正数になり、別物です。 - 負数の右シフトは0方向の切り捨てにならない:
-1 >> 1は-1のまま。算術シフトは負の無限大方向への丸めで、割り算の/(0方向丸め)とは結果が食い違います。x / 2をx >> 1で代用すると負数で1ずれます。 - 符号拡張 vs ゼロ拡張:狭い型を広い型へ広げるとき、符号付きはMSBを複製し、符号なしは0を詰めます。
charをintに代入したときの値は、そのcharが符号付きか(処理系依存)で変わります。
int8_t a = -8;
a >> 1; // 算術右シフト → -4(符号ビットを複製)
uint8_t b = 0xF8; // ビット列は a と同じ
b >> 1; // 論理右シフト → 0x7C = 124
// 左シフトでの最上位ビット押し出しは符号付きで UB になりうる
int x = 1 << 31; // INT_MAX 超え:C では未定義動作
左シフトも要注意で、シフト量が型幅以上のとき(1 << 32 を32bit型に)の挙動は未定義です。「ビット幅で割った余り」をハード任せに期待してはいけません。
飽和演算という別解
ラップアラウンドが望ましくない領域では、範囲を超えたら端の値に張り付かせる飽和演算(saturating arithmetic)を使います。音声・画像・DSPでは、音量を上げすぎたときに最大値で頭打ちにする方が、ラップして急に最小値へ反転するより遥かに安全だからです。
| 方式 | 255 + 10(uint8) | 用途 |
|---|---|---|
| ラップアラウンド | 9(mod 256) | ハッシュ・カウンタ・暗号など循環が自然な場面 |
| 飽和 | 255(上限で停止) | 信号処理・画素値・メータ表示など反転が致命的な場面 |
SIMD命令には飽和加算(x86の paddusb など)が用意され、Rustでは saturating_add / wrapping_add / checked_add / overflowing_add と意図を型レベルで選び分けられます。どの戦略を採るかは仕様判断であり、暗黙のラップに頼らず明示するのが堅牢です。これは想定外を投げる 例外処理 とは別系統の、戻り値で安全側に倒す設計思想です。
nビット2の補数の範囲は −2^(n−1) 〜 2^(n−1) − 1 で、負側が1つ多い(−128 に対応する正の +128 は無い)。このため abs(INT_MIN) や −INT_MIN はオーバーフローし、これ自体が C では UB です。「最小値の絶対値が表せない」は定番の引っかけです。
まとめ
- 整数はビット列への解釈ルール。符号なしは各ビットを正の重みで、2の補数はMSBだけ負の重みで読む。
- 2の補数は mod 2^n の円環上の点で、ゼロが一意・加減算が符号非依存になるためハードが単純化される。
- 符号なしのあふれはラップアラウンドで定義されるが、C/C++の符号付きオーバーフローは未定義動作で最適化に消える危険がある。
- 右シフトの算術/論理差、負数での丸め方向、符号拡張、最小値の絶対値は典型的な落とし穴。意図はラップ・飽和・チェック付きで明示する。
プログラミング Article
整数表現と2の補数・オーバーフローを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
整数
比較で見る軸
難易度: advanced / カテゴリ: プログラミング / タグ数: 5
導入後に効く点
桁あふれ時、符号なしは2のn乗を法とするラップアラウンドが定義されるが、符号付きのオーバーフローはC/C++では未定義動作で最適化に消される。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- プログラミング
- タグ数
- 5
判断チェックリスト
- 自社の用途が「整数 / 2の補数」に近いか確認する。
- 強みである「2の補数は最上位ビットだけ重みを負にした表現で、加減算は符号の有無に関係なく同じ回路で計算でき、ゼロが一意になるのが利点。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。