Cのソースコードに m = 195; とか n = 0xffffffff; とか書いたときの定数（右辺）の型って、なんであるかご存じでしょうか？ また、C90(1990年版のISO C言語規格)とC99(1999年版のそれ)ではその型が微妙に異なったりすることがあるんですが、ご存じでしょうか？ さらには、お使いのマシンがILP32であるかLP64であるかLLP64であるかによっても、微妙に型が違ってきたりするんですが、それについてはどうでしょうか? えーもちろん、普段は「Uがついてなかったらint, Uがついてたらunsigned intジャネーノ？」くらいの理解でも殆ど不自由しないわけですが、詳細な理解がないとハマるケースも稀にあります。

私はというと、上に書いたような事は、C90/99の差違を除いてはだいたい理解しているつもりだったのですが、C90/99の差異について無頓着だったがために、先日ちょっとした不意打ちをくらいました。事例(クイズ?)として紹介してみます。

クイズ

このようなコードを考えます。妙なコードですが、問題の起こる最小の例を抜き出したということでご勘弁ください。これは、C90とC99で異なる振る舞いをするコードになっています。4294967295は、2進数で書くと1111 1111 1111 1111 1111 1111 1111 1111です(1が32個並ぶ)です。 #include <stdio.h> void foo(int nume) { unsigned long long x = nume / 4294967295; printf("%llu

", x); } int main() { foo(-1); return 0; } これを、次の4つの環境でコンパイル・リンクして実行すると、 (1) ILP32, C90準拠コンパイラ (gccなど)

(2) ILP32, C99準拠コンパイラ (gcc4 -std=c99 など)

(3) LP64, C90準拠コンパイラ (gccなど)

(4) LP64, C99準拠コンパイラ (gcc4 -std=c99 など) それぞれ、どのような出力が得られるでしょうか?

えー、4294967295 の型が(1)-(4)でそれぞれ何になるかがポイントです。それによって除算がどう行われるかが、除算の結果の型も含めて決まります。変数xへの代入部分は、この例では答えに影響しません。というか、影響しないようにxの型を選んであります。

答え

(1)-(4)の出力は順に (1) 1

(2) 0

(3) 0

(4) 0 となります。次の通り: % gcc -v gcc version 4.1.1 20070105 (Red Hat 4.1.1-51) % uname -m x86_64 % gcc -m32 div.c && ./a.out 1 % gcc -m32 -std=c99 div.c && ./a.out 0 % gcc -m64 div.c && ./a.out 0 % gcc -m64 -std=c99 div.c && ./a.out 0 順を追って解説します。まず、「定数の型がどのように決まるか」から押さえましょう。

定数の型はどのように決まるのか (C90編)

C90における定数の型は、ISO C90 規格書*1の6.1.3.2に載ってます。 整数定数の型は、次の並びのうちでその値を表現できる最初の型とする。 接尾語無しの10進数: int, long int, unsigned long int

接尾語無しの8進数又は16進数: int, unsigned int, long int, unsigned long int

文字u又はUが接尾語として付く場合: unsigned int, unsigned long int

文字l又はLが接尾語として付く場合: long int, unsigned long int

文字u又はU及び文字l又はLが接尾語として付く場合: unsigned long int たとえば、ILP32環境で定数「1」や「214783647」*2はintですが、「2147483648」*3はintでもlong intでも表現できないのでunsigned long intになります。型が、符号有無も含めて異なります。また、同じ数(!?)でも10進定数にするか16進定数にするかで型が異なることがあります。LP64環境で「2147483648」は long int ですが、「0x80000000」は unsigned int になってしまいます。やはり型が、符号有無も含めて異なります。ぅゎ。

定数の型はどのように決まるのか (C99編)

C99における定数の型は、ISO C99 規格書の6.4.4.1に載ってます。C99の場合は表になっているんですが、私の方でC90にフォーマットを合わせたものを記します。ひとまず「接尾語無しの10進数」のlong intの次が、C90ではunsigned long intだったのに対しC99ではlong long intになっています。これが今回のポイントです。 整数定数の型は、次の並びのうちでその値を表現できる最初の型とする。 接尾語無しの10進数: int, long int, long long int

接尾語無しの8進数又は16進数: int, unsigned int, long int, unsigned long int, long long int, unsigned long long int

文字u又はUが接尾語として付く場合: unsigned int, unsigned long int, unsigned long long int

文字l又はLが接尾語として付く場合の10進数: long int, long long int

文字l又はLが接尾語として付く場合の8進数又は16進数: long int, unsigned long int, long long int, unsigned long long int

文字u又はU及び文字l又はLが接尾語として付く場合: unsigned long int, unsigned long long int

文字ll又はLLが接尾語として付く場合の10進数: long long int

文字ll又はLLが接尾語として付く場合の8進数又は16進数: long long int, unsigned long long int

文字u又はU及び文字ll又はLLが接尾語として付く場合: unsigned long long int ILP32+C99では定数「1」や「214783647」はintですが、「2147483648」は long long int になります。またC90同様、同じ数(?)でも10進定数にするか16進定数にするかで型が異なることがあります。ILP32+C99で「2147483648」は long long int ですが、「0x80000000」は unsigned int です。やはり型が、符号有無も含めて異なります。

解説

これを踏まえ、(1)-(4)で4294967295の型がどうなるかを起点として除算の動きを見ます。

(1)では、4294967295の型はunsigned long int になります。ですから、除算は -1 [int] / 4294967295 [unsigned long int] ==(usual arithmetic conversion)==> -1 [unsigned long int] / 4294967295 [unsigned long int] ==> 4294967295 [unsigned long int] / 4294967295 [unsigned long int] ==> 1 のように行われ（適当な記法でスミマセン）、結果は1になります。0にはなりません！。結果の型は unsigned long int です。

(2)では、4294967295の型はlong long intになります。ですから、除算は -1 [int] / 4294967295 [long long int] ==(usual arithmetic conversion)==> -1 [long long int] / 4294967295 [long long int] ==> 0 のように行われ、結果は0になります。結果の型は long long int です。64bitの符号「付き」の型に揃えられてから演算されるので、(1)のように -1 が 4294967295UL に読み替えられないのがポイントでしょう。

(3)と(4)はlong型が64bitである関係で、同じ演算になります。C90/99で違いは出ません。4294967295の型はlong intになります。除算は -1 [int] / 4294967295 [long int] ==(usual arithmetic conversion)==> -1 [long int] / 4294967295 [long int] ==> 0 のように行われ、結果は0になります。結果の型は long int です。-1が-1のまま演算される点は、(2)と同じですね。

(余談) usual arithmetic conversion

usual arithmetic conversion (通常の算術型変換) については、Cの規格を参照してください。このpdfにも少し載ってます。long long型(64bit型)のことを忘れてもいいなら、いやあまりよくないとおもいますが、K&Rの第二版にも一応載ってます。

GCCの警告

実はGCCは、C90とC99で型が異なるような定数を使用すると、C90モードでコンパイルした際に次のような警告を出してくれます。この警告が出たら、無視せずに原因をよく考える方がよさそうです。 warning: this decimal constant is unsigned only in ISO C90 しかしながら、データモデルによって型が変化するケース、10進で書くか8/16進で書くかによって型が変化するケースについては、プログラマが気を付けるしかなさそうです。

まとめ

C言語のソースコード中に記載した定数は、結構意外な型として扱われていることがあり、その詳細を知らないと（たまに）問題が起こる。特に、 10進定数を8/16進定数に書き直すと（あるいはその逆）、型が変化してしまうことがあるので注意 同じことですが、ある数を10進定数として書くか、8/16進定数で書くかによって型が異なってしまうことがあるので注意

C90準拠のコンパイラとC99準拠のコンパイラで、定数の型が異なってしまうことがあるので注意

ILP32環境でコンパイルするかLP64環境でコンパイルするかで、定数の型が異なってしまうことがあるので注意 この3点に注意が必要です。また、 上に書いたGCCの警告を無視しない

定数を、符号無しだと思ってソースコード中に書いているのなら、必ず接尾語Uを書く (LとLLについても同様) というのもよい習慣だと思います。