符号付き整数を徹底解説:二の補数の仕組み・オーバーフロー検出・ビット演算と言語別の扱い

符号付き整数とは — 概要

符号付き整数(signed integer)は、正の数・負の数・ゼロを表現できる整数型のことです。コンピュータ内部では有限ビットで値を表すため、符号付き整数の表現方法や範囲、演算の挙動(オーバーフロー、シフト、ビット演算など)には注意が必要です。本稿では主要な表現方式(特に二補数)を中心に、実装上の意味、プログラミング言語やハードウェアでの扱い、実用上の注意点まで詳しく解説します。

符号付き整数の主な表現方式

  • 符号・絶対値表現(Sign–Magnitude):最上位ビットを符号ビット(0=正、1=負)に割り当て、残りのビットで絶対値を表します。例:8ビットで +5 = 00000101、-5 = 10000101。+0 と -0 が別々に存在するのが特徴です。

  • 一の補数(One's complement):負数は正数のビットを反転して表します(ビットごとの否定)。例:+5 = 00000101、-5 = 11111010。やはり +0 と -0 が存在します。

  • 二の補数(Two's complement):負数は正数をビット反転して1を加えたもの(または値を2^Nから引いたもの)で表します。例:8ビットで +5 = 00000101、-5 = 11111011。二の補数では +0 と -0 が統一され、加減算回路が単純化されるため現在の主流です。

二の補数の詳細(なぜ主流か)

二の補数表現はハードウェア実装上の利点が大きく、現代のほとんどのCPUやコンパイラが採用しています。主な利点は:

  • 加算器ひとつで減算ができる(符号専用回路が不要)。
  • 正負のゼロが1種類に統一される(+0 と -0 の重複がない)。
  • 比較や加減算の回路が単純化される。

Nビットの二の補数整数の表現範囲は次の通りです:

−2^(N−1) から 2^(N−1) − 1

例:8ビットなら −128 〜 +127、16ビットなら −32768 〜 +32767。

具体例(8ビット)

  • +5 : 0000 0101
  • -5 (二の補数) : 1111 1011 (= ビット反転 1111 1010 に 1 を足す)
  • -128 : 1000 0000(最小値。絶対値が一番大きく、これを反転すると扱いづらい点がある)

オーバーフローとその検出

符号付き整数の演算では、結果が表現可能な範囲を超えると「オーバーフロー(符号付きオーバーフロー)」が発生します。二の補数での重要な検出法は次のとおりです:

  • 同じ符号の2つの数を足して符号が変わった場合、符号付きオーバーフローが起きたと判断できる(例:+120 + +10 が負になる)。
  • ハードウェアでは、x86のように OF(オーバーフローフラグ) を使って符号付きオーバーフローを示し、CF(キャリーフラグ)は符号無し演算での桁あふれ(キャリー)を示します。CF と OF は異なる意味を持ちます。
  • プログラミング言語では扱いが分かれます。例えば C言語では「符号付き整数のオーバーフローは未定義動作(undefined behavior)」であり最適化や安全性に影響します。Java は二の補数かつ演算は整数の範囲でのラップ(オーバーフローはビット落ち)として定義されています。

ビット演算・シフトに関する挙動

符号付き整数に対するビット演算やシフト操作にも注意が必要です。

  • ビット否定(~x): 二の補数で ~x = −(x + 1)。例えば x=5 の ~x = -6。
  • 左シフト(<<): 値を2倍に相当。高位ビットの破壊によりオーバーフローする可能性がある。C言語では符号付き左シフトでのオーバーフローや負数の左シフトは未定義となる場合があるため注意。
  • 右シフト: 論理右シフト(0を挿入)と算術右シフト(符号ビットを複製して挿入)がある。算術右シフトは符号付き整数の除算に近い働きをする(負数は符号を維持)。言語や実装によってどちらが使われるかが異なる(Cの符号付き右シフトは実装依存だが多くのアーキテクチャは算術右シフトを採用)。
  • 符号拡張(sign extension): 小さい幅から大きい幅へ変換する際、負数は上位ビットに 1 を埋める。これにより符号が維持される。

言語ごとの扱い(代表例)

  • C/C++:実装依存の部分が多い。典型的な実装は二の補数だが、標準では符号付きオーバーフローは未定義動作(undefined behavior)とされる。符号付き・符号無しの変換は定義がある(負の符号付きを符号無しに変換すると modulo 2^N の値になる)。

  • Java:Java は整数演算を二の補数として定義しており、オーバーフローはラップして下位ビットが残る(例:int は 32ビット、範囲は −2^31..2^31−1)。右シフトには算術右シフト(>>)と論理右シフト(>>>)が明確に区別されている。

  • Python:Python の整数(int)は任意精度(多倍長)で、内部実装(CPython)では符号と絶対値の「桁」配列で表されるため、通常のユーザコードではオーバーフローを気にする必要はありません。ただし固定幅の表現が必要な場面(バイナリプロトコル、埋め込み、C拡張など)では明示的に truncation/マスクを行う必要があります。

ハードウェア側の注意点:フラグと実装

プロセッサは演算ごとにいくつかのフラグを設定します。代表的なもの:

  • CF(Carry Flag): 符号無し演算での桁あふれを示す。
  • OF(Overflow Flag): 符号付きオーバーフローを示す(最上位ビットの桁の意味が変わるとき)。
  • ZF(Zero Flag)や SF(Sign Flag): 結果がゼロか負かを示す。これらは比較命令や条件分岐で用いられる。

CF と OF の違いを理解すると、同じビット列でも「符号付き」と「符号無し」で意味が違うことが分かります。例えば(unsigned) 255 + 1 はキャリーが立つが、(signed 8-bit)127 + 1 はオーバーフローとなる。

実用上の注意点・落とし穴

  • 整数オーバーフローの扱いは言語ごとに違う。安全性が重要なシステムではオーバーフロー検出や飽和演算の利用を検討する。
  • 符号の拡張やビット幅の変換で意図しない値になる(特に符号無しとの混在)ことがある。C/C++では符号無しに昇格されると想定外の結果になることがある。
  • ファイル/ネットワークのバイナリ化(シリアライズ/デシリアライズ)では、エンディアンや固定幅の扱いに注意。エンディアンはバイト順の問題であり、ビット表現の整数の意味とは別次元ですが、実装間の互換性に影響する。
  • デバッグ時に負数をビット単位で扱うと、直感と異なる現象(負のビットパターン)に遭遇する。ビット演算を使う場合は二の補数特性を理解しておくこと。

まとめ

符号付き整数は、有限ビットで正負を表すための基本概念です。歴史的には複数の表現が存在しましたが、現在は二の補数が主流で、そのための性質(範囲、オーバーフローの検出法、符号拡張やシフト動作、~演算の等式など)を正しく理解することが重要です。プログラミング言語やプラットフォームにより挙動(オーバーフローの扱い、シフトの意味、内部表現)は異なるため、特にシステムプログラミングや数値処理を行う際は仕様書やリファレンスを確認してください。

参考文献