C言語に限らず、コンピュータでは数値の表現には「2進法」が使われます。コンピュータが扱う数の最小単位は、0 と 1 の 2 つの値だけを格納できる「ビット」だからです。0 と 1 だけを用いた数の表記方法を 2 進表記といい、2 進表記で表された数のことを「2 進数」といいます。こういう書き方をすると非常に難しく感じるかもしれませんが、何のことはない、中高生レベルの数学の概念に過ぎません。

10進数では0~9の10通りの値を表すことができますが、2進数の一桁、つまり1ビットは0~1の2通りの値を表すことしかできません。同様に、10進数の4桁は104(=10,000)通りの値を表すことができたように、2進数の4桁は 24(=16)通りの値を表すことができます。

このように考えていくと、8ビットであれば28(=256)通りの値を表すことができますし、16ビットであれば 216(=65,536)通り、32ビットであれば232(=4,294,967,296)通りの値を表すことができます。

符号無し整数型の内部表現

ここまで書くと大体見えてくると思いますが、8ビットのunsigned char型であれば0~255の256通りの値を、16ビットのunsigned short型であれば0~65,535の65,536通りの値を、32ビットのunsigned int型であれば0~4,294,967,295の4,294,967,296通りの値を表現できます(各型のビット数は実際には処理系定義です)。

符号無し整数型では、このように0から2ビット数-1までの値を表現することができます。もし、その範囲を超えるような演算、例えば最大値に1を加算するなど、を行った場合、数学的な結果を 2ビット数で割り算したときの余りが格納されることになります。

このことを、ちょっと難しい表現で、2ビット数を法とする剰余とか、モジュロ 2ビット数などと表現することがあります。16ビットのunsigned int型であれば、65,536を法とする剰余となります。この辺りをより詳しく知りたい方は、「モジュラ算術」とか「剰余系」をキーワードとして調べてみてください。

符号無し整数型は、2ビット数(最大値より1大きい数)を法とする剰余になると定義されていますから、決してオーバーフローを起こすことがありません。どんなに大きな値どうしを足し算しても、あるいは掛け算しても、それでエラーになることはなく、結果がどうなるかも保証されています(浮動小数点数から符合無し整数型に変換した場合はこの限りではありません)。

符号付き整数型の内部表現

符号無し整数型とは異なり、符号付き整数型はやや仕様が複雑です。まず、「符号付き」ですのでマイナスの値も表現することができるわけですが、マイナスの値の表現方法が1種類ではないのです。以下に、規格上採用可能なマイナスの表現方法を挙げてみます。

  • 2の補数表現
  • 1の補数表現
  • 符号ビットと絶対値

現存する大多数の処理系では「2の補数表現」を使用しています。2の補数表現を用いてマイナスの値を表すには、2ビット数から絶対値を引いた値になります。例えば、8ビットのsigned char型で-3を表す場合、28(=256)から3を引いた256 – 3 = 253を内部的に用いることになります。

しかし、これでは253という値を見ただけでは、それが+253なのか-3なのか区別ができません。そこで、プラスの値は127までということにして、128以上の値はマイナスであると決めています。128というと27であり、8ビットの整数型であれば、一番左の桁、すなわち一番大きな桁が1になると、128以上ということになります。そこで、符号付き整数型では、この一番上の桁(上位ビット)のことを符号ビットと呼んでいます。

1の補数表現についても見てみましょう。2の補数表現では、マイナスの数を表すのに、2ビット数から絶対値を引いた値を使用しました。しかし、1の補数表現では2ビット数-1 から絶対値を引いた値を使用します。もう一度先ほどの例を取り上げると、-3を表現するには28-1から3を引いた255 – 3 = 252を内部的に用いることになります。

ここまで理解できれば、符号ビットおよび絶対値がどんな表現なのかも想像が付くことでしょう。左端の桁(上位ビット)を符号ビットとし、それ以外のビットで絶対値を表現するわけです。-3であれば、符号ビットが27ですので、128 + 3 = 131を内部的に用いることになります。

オーバーフロー

符号付き整数型の内部表現の基本についてはすでに書きましたが、これで符号付き整数型のすべてを知ったと思うと大間違いです。符号付き整数型は、符号無し整数型とは異なり、いろいろと厄介な問題を抱えています。その一つがオーバーフローです。

演算の結果が符号付き整数型の表現範囲を超える場合には、オーバーフローが発生します。オーバーフローが発生した場合の動作は未定義です。すなわち、そのときの動作がコンパイラの取扱説明書に明記されている場合を除き、結果はまったく保証されません。また、取扱説明書に動作が明記されていたとしても、少なくとも移植性はまったくありません。

オーバーフローの結果は未定義ですが、よくある振る舞いとしては、

  • 何事もなかったかのように、符号無し整数型の場合と同等のビットパターンを生成する。
  • 何らかのシグナルが発生する。signal関数で登録したハンドラが呼び出されるかもしれないし、OSなどがプログラムを異常終了させるかもしれない。

といったところです。

また、オーバーフローとは少し異なりますが、別の型を符号付き整数型に型変換した結果が、変換後の型で表現できない場合、処理系定義の値になるか、処理系定義のシグナルが発生することになります。未定義の動作よりはましですが、コンパイラの取扱説明書を丹念に読まなければ正確な動作を把握することはできませんし、移植性に関してはまったくないわけです。

変な値 ― マイナス・ゼロとトラップ表現

符号付き整数型に絡む最後の話題は「変な値」です。符号付き整数型は、ある意味少々強引な方法でマイナスの値を表現していますので、そのしわ寄せとして「変な値」を生じてしまいます。

例えば2の補数表現を用いる8ビット符号付き整数型の場合、符号ビットだけが1で他のビットがすべて0になるパターンでは、それに対応するプラスの値を表現することができません。つまり、上記のパターンは-128を表すのですが、+128というのは表現範囲を超えているために、8ビットの符号付き整数型では表現できません。

このような事情から、符号ビットだけが1で他のビットが0のビットパターンを「トラップ表現」として扱うことがあります。トラップ表現というのは、値を表現しない内部表現のことです。ビット演算の結果などで、トラップ表現が生成される場合や、トラップ表現に対して何らかの演算を行った場合の動作は未定義になります。

2 の補数表現を用いている処理系にトラップ表現があるかどうかは、<limits.h>ヘッダの中のINT_MINなど(~_MIN マクロ)を見れば分かります。int型が32ビットの場合、INT_MINの定義が

-2147483647

となっていれば、その処理系にはトラップ表現があります。それに対して、

(-2147483647-1)

となっていればトラップ表現はありません。ちなみに、

-2147483648

となっていれば、それは処理系のバグです(前回の記事参照)。

1の補数表現や符号ビットおよび絶対値の場合には、さらにややこしい問題があります。すなわち、0を表す内部表現が2種類あるのです。符号ビットが0の場合の0は通常の0、符号ビットが1の場合の0は-0と呼ばれます。ただし、-0がサポートされているかどうかは処理系定義です。そして、-0がサポートされない場合、-0を表すビットパターンはトラップ表現になります。また、-0がサポートされる場合、-0を生成するようなビット演算を行ったときに、そのまま-0となるか通常の0になるかも処理系定義です。

ここまで、非常にややこしい話をしてきましたが、普通は2の補数表現を用いる処理系しかありませんので、それ以外のことは参考程度に留めておいてもよいでしょう。また、トラップ表現のある処理系というのもほとんど遭遇する機会はないと思います。

2の補数表現を使うと、加減算や比較演算を行う場合は便利なのですが、乗除算を行うときは一転して不便になります。これについても、基本型を使う分には処理系まかせでよいでしょう。しかし、非常に大きい値を扱う整数型(256ビットとか、無限精度とか)を自作する場合には、必要な知識になってくることでしょう。