memologue RSSフィード

書いている人

日記というよりは備忘録、ソフトウェア技術者の不定期メモ。あるいはバッドノウハウ集。プライベートで調査した細々した諸々のスナップショット。嘘が散りばめられています。ISO/IEC 14882(C++)とPOSIX, GCC, glibc, ELFの話ばかりで、WindowsやMacの話はありません。特に記載がなければLinux/x86とILP32が前提です。時間の経過と共に古い記事は埋もれてしまいます。検索エンジンから飛んできた場合は、ページ内検索をご利用いただくかgoogleキャッシュを閲覧してみてください。技術的な記事を書きためる場所として使っています。言及してもらえると喜びます。主要な記事の一覧を書いておきます:

にんきコンテンツ: [GCC] mainを一度も呼ばないばかりか蹂躙する / Binary2.0 Conference 2006 発表資料 / C++ で SICP / ついカッとなって実行バイナリにパッチ / hogetrace - 関数コールトレーサ

昔のPOSIX関係の記事: シグナルの送受に依存しない設計にしよう / シグナルハンドラで行ってよい処理を知ろう / マルチスレッドのプログラムでのforkはやめよう / スレッドの「非同期キャンセル」を行わない設計にしよう / スレッドの「遅延キャンセル」も出来る限り避けて通ろう / マルチスレッドプログラミングの「常識」を守ろう / C++でsynchronized methodを書くのは難しい / シグナルハンドラからのforkするのは安全か? / g++ の -fthreadsafe-statics ってオプション知ってます? / 非同期シグナルとanti-pattern / localtimeやstrtokは本当にスレッドセーフにできないのか / UNIXの規格について / マルチスレッドと共有変数 - volatile?なにそれ。 / type punning と strict aliasing / アセンブラで遊ぶ時に便利なgdb設定 / 最近の記事は一覧から

2007-02-04

[][] C/C++の定数の型の話, C90/C99の差分のびみょーな話

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\n", 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についても同様)

というのもよい習慣だと思います。

C++について


C++言語(ISO/IEC 14882:2003)は、2003年版の規格では、C言語互換部分についてはC90を参照してますので、gccをC90モードで使ったときと同じ結果になります。

% cp div.c div.cpp

% g++ -m32 div.cpp && ./a.out       
div.cpp:4: 警告: this decimal constant is unsigned only in ISO C90
1
% g++ -m64 div.cpp && ./a.out
0

*1:C90のJIS版は、旧規格になりますのでWebから発注はできません。http://www.webstore.jsa.or.jp/webstore/Com/html/jp/ShoppingInfo.htm のFAX注文書で JIS X3010:1993 と X3010:1996(Amd1) を発注することになります。合計で3.5万円程です。詳しくはJSAに問い合わせを

*2:2進で 0111 1111 1111 1111 1111 1111 1111 1111 です

*3:2進で 1000 0000 0000 0000 0000 0000 0000 0000 です

通りすがり通りすがり 2008/09/29 10:44 (3) と (4) ですが、 C90 の場合、負数を含む除算のため処理系によって 0 となる場合と -1 となる場合がありますね。 C99 では商は 0 方向に切り捨てられることになったので、必ず 0 になります。

名無し名無し 2010/01/04 11:48 JIS X3014だと接尾語無しの10進数はint、long intの次は未定義だそうで

名無し名無し 2010/02/05 12:54 ISO/IEC 14882:2003でも接尾語無しの10進数はint、long intの次は未定義と書いてありました

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/yupo5656/20070204/p1