C言語の整数型は処理系によってサイズが異なります。標準規格では、それぞれの整数型が少なくともどれだけの表現範囲を持っているか、そして、それぞれの整数型の間の表現範囲の大小関係だけが決められています。
整数型の中でも、int
型のサイズは 16 ビットと 32 ビットの処理系がそれなりに多く存在することもあり、入門書や解説書でも注意が促されることが多いようです。最近では long
型が 64 ビットの処理系もありますので、整数型のサイズを取り巻く状況はもう少し複雑になってきています。
そうした中、極力ソースコードの移植性を高めようということで、int
型のようにサイズがよくわからない型は使用せず、int16
型とか、int32
型のような型を定義して、「それらを使うべし」とするコーディング規約もよく見かけます。それで本当に、int
型のサイズに関する移植性の問題は解消されたのでしょうか?
残念ながら、世の中はそう甘くありません。どんな型定義を行おうとも、C言語に関わっている以上、int
型のサイズにまつわる問題は地の果てまで追いかけてきます。今回は、そうした int
型のサイズに関する解説です。
汎整数拡張
汎整数拡張(integral promotion)という用語を見聞きされたことがあるでしょうか? この用語は、同じ意味にもかかわらず、いろいろ異なる表記をされることがあります。それも標準規格の中でです。
例えば、C++規格である JIS X3014:2003 の中では、「汎整数昇格」という訳語が使われています。また、C99では「整数拡張(integer promotion)」という用語になぜか変わっています。すべて同じ意味ですが、ここでは「汎整数拡張」という表記に統一したいと思います。
汎整数拡張というのは一体何でしょうか? ごく簡単にいうと、何らかの演算を行うときには、オペランドの値が int
型で表現できる場合は int
型に、unsigned int
型で表現できる場合は unsigned int
型に、暗黙的に型変換が行われることです。「オペランドの値が~」と書きましたが、これは何も、実行時に実際にどんな値なのかを調べるわけではなく、あくまでもオペランドの型だけを頼りに、静的に判断されます。
例えば、次のような処理系を考えてみましょう。
型 | 表現範囲 |
---|---|
char | -128~+127 |
short | -32768~+32767 |
int | -2147483648~+2147483647 |
long | -2147483648~+2147483647 |
近年では最も典型的な処理系ですが、この場合、汎整数拡張によって、char
型と short
型は int
型に型変換されることになります。また、unsigned char
型と unsigned short
型も、その表現範囲全体が int
型でも表現できますから、int
型に型変換されます。
つまり、元が符号無し整数型であっても、知らないうちに勝手に int
型に変換される可能性があるわけです。これは次のような状況で、勘違いを生み出す原因になります。
if (c - 0x20 <= 0x5e)
{
...
}
上記のコードは、ASCII 文字が 0x20~0x7e の範囲に収まっているかどうかを判定しようとしているようです。素直に書くと、
{
...
}
ですが、比較を 2 回行う必要があるので、少しでも最適化しようとしたのでしょう。しかし、上のコードは期待通りには動いてくれません。というのも、上のコードでは、
において、c
が unsigned char
型なので、0x20 を引いても負にはならず、0xf0 になることを期待しているわけですが、実際には、減算の前に汎整数拡張*1が発生しますので、int
型として演算を行うことになります。結果として、c - 0x20
は -0x10 になりますので、期待は完全に裏切られます。
他の例を挙げてみましょう。
if (sizeof(+a) == sizeof(a))
{
...
}
ちょっとわざとらしい例ですが、上のコードでは、if
の条件式は真になるでしょうか?それとも偽になるでしょうか?
結果は偽になります。単項の+演算子というのは、何も行わない演算子だと理解されていることが多いと思います。しかし、この演算子のオペランドも汎整数拡張が行われます。それに対して、sizeof
演算子のオペランドは汎整数拡張が行われません。結果として、左辺は int
型であり、右辺は char
型になりますので、今回仮定している処理系では両辺は等しくなりません。
このように、例えどんなに型定義などを使って int
型のサイズを隠蔽したとしても、何らかの演算を行うと、int
型のサイズの影響が露骨に現れることになります。
整数定数
整数定数(いわゆるリテラル)にも int
型のサイズが関係してきます。整数定数の型は、(ちょっと複雑ですが)次の手順で決定されます。
int
型の表現範囲であればint
型int
型の表現範囲になく、unsigned int
型の表現範囲にある 8 進または 16 進数の場合はunsigned int
型long
型の表現範囲であればlong
型- そうでなければ、
unsigned long
型
ただし、C99の場合には、long long
型が存在しますので、long
型の表現範囲にない 10 進定数は long long
型になります(以下省略)。
このように、整数定数の型は、その値によって決まります。すなわち、int
型のサイズによって、整数定数がどんな型になるかが変わるわけです。特に、0xffff のような整数定数は、unsigned int
型になったり int
型になったりしますので要注意です。
ここで、整数定数の型に関するありがちな勘違いの具体例を挙げてみます。なお、今度は、先ほどとは違って int
型が 16 ビットで、その表現範囲が -32758~+32767 の処理系について考えてみることにします。
#define COUNT_MAX 32767
#define COUNT_MIN -32768
上のコードのおかしな点に気付くでしょうか?
おかしな点は COUNT_MIN
マクロの定義にあります。int
型の表現範囲は -32768~+32767 なので、int
型に定義されている count_t
の最小値を表す COUNT_MIN
マクロが(-32768)なのは当然のような気がします。
しかし、値はともかく、問題は COUNT_MIN
の型にあります。32768 というのは、int
型(表現範囲は -32768~+32767 と仮定)の表現範囲に収まりません。また、8 進定数でも 16 進定数でもありませんから、32768 の型は long
型です。long
型のオペランドに単項の -
演算子を付けても、やはり long
型です。
つまり、上のコードのような COUNT_MIN
マクロの定義では、本来 int
型の定数式に展開されるべきであるにも関わらず、long
型になってしまうわけです。では、どのように定義すれば int
型になるのでしょうか?
それは、次のようにします。
今度は、-32767 から 1 を引いています。32767 は int
型の表現範囲に収まっていますから int
型です。そして、それに単項の -
演算子を付けてもやはり int
型です。また、int
型である 1 との減算を行っても、結果はやはり int
型になります。
いかがでしょうか? int
型のサイズを隠蔽するために別の型を定義するコーディング規約は多いのですが、それをやってしまうと、一見しただけでは式や定数の振る舞いが分からなくなってしまいます。なぜなら、定義された型の本当の型が分からないからです。
今回は主に int
型に焦点を当てましたが、整数型のサイズに関しては、まだ他にもいろいろな注意点があります。それらについては、また別の機会にお話したいと思います。