第6回 整数型の内部表現

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 long 型であれば 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 ビットとか、無限精度とか)を自作する場合には、必要な知識になってくることでしょう。