[←1つ前] [→1つ後] [↑質問一覧] [↑記事一覧] [ホームページ]
C言語で書いたプログラムのバグの中には、言語仕様上の動作が処理系に依存 するようなコードが原因のものがあります。コンパイラをバージョンアップした のでコンパイルし直してみたら動かなくなった、というのはよくある話です。コ ンパイラ自体のバグだったという悲劇的なケースもないわけではありませんが、 コンパイラのバージョンに依存するようなコードを書いていたため、ということ も案外多いようです。基本的には、結果が不確実なコードは書かない、というの が原則ですが、そのためには、不定、未定義という考え方を身に付けておく必要 があります。
a = foo() + bar();これをコンパイルすれば、どちらかが先に呼ばれて、その後もう一つが呼ばれる、 というコードが生成されることは保証されます。しかし、
foo
と
bar
のどちらが先に呼ばれるかは分かりません。
JIS Cでは不定を「未規定の動作」という表現を使っていますが、俗には「不定」 という表現の方がよく使われるようです。「未規定の動作」の定義は「正しいプ ログラム構成要素及び正しいデータに対する動作であり、この規格が明示的に何 ら要求を課さない動作。」となっています。
errno
、
setjmp
等の実装がマクロか外部識別子であるか。
インターネットのニュースグループ、comp.std.cやcomp.lang.cでは、未定義の コードを実行した結果「鼻から悪魔が飛び出しても仕様に反しない」というjoke が流行したことがありました。今でもたまにこのような表現を見ることがありま す。
英語では、未定義のことをundefined、不定のことをunspecifiedと表現します。
不定と未定義の最も大きな違いは、不定のコードはプログラムとしては正しい
が、未定義のコードは間違いであり、動作する保証すらないという所にあります。
従って、未定義のコードは絶対に書くべきではありません。不定の場合は、結果
がどのようになるかは処理系に依存しますが、プログラム自体は正しいとみなさ
れることが保証されています。従って、
a = foo() + bar();
のようなコードは、どちらが先に呼ばれても構わない状況で用いる限り、安心し
て使うことができます。
JISでは未定義の動作について、次のように記述しています。「可搬性がない若 しくは不正なプログラム構成要素の使用における動作、又は不正なデータ若しく は不確定な値を持つオブジェクトの使用における動作であり、この規格が何ら要 求を課さない動作。未定義の動作に対して、その状況を無視して予測不可能な結 果を返してもよい。」
(参考 JIS X3010)
void
式の値を使っている場合。
NULL
による参照、既に終了したブロック内の自動記憶域期
間のオブジェクトに対する参照。
auto
変数の読みだし。
memmove
以外のライブラリ関数を使って重なり合うオブジェクト間の複写を行
った場合。
free
または
realloc
で開放された記憶域を参照するポインタ値を使用する場合。
int a[10]; int i = 0; while (i < 10) a[i] = i++;
i++
という式を実行すると、
i
の値が変化します。このような振る舞いのことを副作用と呼びます。ある変数に
対して副作用が生じるような式においては、他の箇所でその変数を参照すること
はできません。その結果は未定義とされています。
この式の場合、
i
は
i++
の他に、配列
a
の要素を指定するために参照されているため、その結果は未定義となるのです。
K&Rの記述を読むと、上の結果は不定であると解釈できるような記述があり ますが、ANSI Cの規格では、不定ではなく未定義の扱いとすることになっています。
(参考 comp.lang.c FAQ 3.1)
int i = 7; printf("%d¥n", i++ * i++);
printf
がどのような値を表示してもC言語の仕様としては問題ありません。極端な場合、
ここでプログラムが異常終了してしまっても問題ありません。
i++ * i++
は、一見すると、
++
が実行されて、
i++
という式の値としては7となり、
i
の値が8になる。
++
が実行されて、
i++
という式の値は8となり、
i
の値が9になる。
i
の値が実際に増加するタイミングは、
i++ * i++
という式が終了するまでのいつかである、という範囲でしか保証されていないか
らです。
(参考 comp.lang.c FAQ 3.2)
int a[10]; int i = 0; while (i < 10) a[i] = i++;
それに、たいていの未定義、あるいは不定なコードは、ほんの僅かな変更で、 定義されたコードに修正することができます。この例の場合は次のように書くと 明白です。
int a[10]; int i; for (i = 0; i < 10; i++) a[i] = i;
i
の値が7になってしまった。どうしてこんな変な値になってしまうのか。
int i = 3; i = i++;
i = i++;
の結果は未定義です。どんな値になってもC言語としては正しい結果です。
なお、
i = i++;
自体がほとんど無意味であることにも注目してください。これは他言語に慣れた
初心者がうっかり間違うことの多い例です。もしかすると、この人は
i = i + 1;
と書くつもりだったのですが、たまたま、C言語にはインクリメントの演算子があ
るということを発見したため、
i + 1
という表現の部分をうっかり
i++
に置き換えてしまったのかもしれません。
ここは、深く考えずに、
i = i + 1;
と書いておくか、あるいは
i++;
とだけ書けばよいのです。
(参考 comp.lang.c FAQ 3.3)
a ^= b ^= a ^= b
というコードを使えば、
a
と
b
の値を交換することができることを動作確認した。しかし、この書き方はよくな
いと言われた。なぜか。
a^a
が0になることを利用している所がポイントです。しかし、見てすぐ分かるよう
に、一つの式の中に副作用が複数回あり、従って、この結果は未定義となります。
次のようにカンマで区切って書くと、副作用の影響は消滅します。なぜなら、 カンマ演算子は、左から右に評価すると仕様で決められているからです。
a ^= b, b ^= a, a ^= bしかし、これでもなお、このコードは同一のオブジェクトに対して動作しない (aとaを交換するとどんな値になるか?)という欠点があるため、実際にプログラム に使うのは止めておいた方が無難でしょう。
(参考 comp.lang.c FAQ 10.3)
a = foo() + bar();
のようなコードに対して、
foo
と
bar
のどちらが先に呼ばれるか決まっていない、ということです。
しかし、優先順序に関らず、それぞれの項の評価順序は不定のままです。例え
ば、次の式は、どのような順序で関数が呼ばれるか分かりません。これで言える
のは、
f2()
を呼び出して得られた値と
f3()
を呼び出して得られた値をまず掛け算
し、その結果と
f1()
を呼び出して得られた値を加算する、ということだけです。最後に足し算が行わ
れるにもかかわらず、
f1()
が真っ先に呼び出されて、掛け算が終わるまでその値が保存されているかもしれ
ません。
i = f1() + f2() * f3();
評価順序を確定させるためには、複数の文に分けるのが簡単です。
(参考 comp.lang.c FAQ 3.4)
kcode = (getchar() << 8) + getchar();
+
」という演算子の両辺の評価順序は不定です。つまり、+の両側に書かれた
getchar()
のどちらが先に実行されるか分かりません。右から順に評価する処理系もあれば
、左から順に評価する処理系もあるでしょう。この人の使った処理系は、たまた
ま右の
getchar()
が先に呼び出されるものだったようです。
C言語の仕様では、多くの演算子の評価順序は不定です。左から順に実行される という先入観を持ちがちなので、注意が必要です。
kcode = getchar() + (getchar() << 8);
この例では、評価順序に依存しないコードに修正することは、実に簡単です。 次のように二行に分けて書けばいいのです。
kcode = getchar() << 8; kcode += getchar();
&&
や
||
を使ったコードの中には、評価順序が左から右という前提で書かれているものが
あるが、評価順序は不定ではないのか。
&&
、
||
、カンマ演算子の「
,
」、三項演算子の「
:?
」です。これらは、左を評価し、その後に右が評価されることになります。
ある時点においてそこまでの副作用が全て完了することが保証されている区切 りのことを、副作用完了点(sequence point)と呼びます。前述の演算子は、左の 式を評価した直後が副作用完了点となっています。つまり、左が処理し終わって から右が処理されることが保証されているわけです。
なお、関数の引数を区切るのに使うカンマは、カンマ演算子ではないため、評 価順序は不定です。
i = foo() & bar(); /* (1) */ i = foo() && bar(); /* (2) */
&&
という演算子は、左から右へ評価し、全てが真だった時には式全体の値を真とし
ます。評価した結果が偽だった場合、そこで評価が打ち切られ、式全体の値を偽
とします。
&&
や
||
という演算子は、他の多くの演算子とは異なり、左から右の方向に順番に評価さ
れることが仕様で決まっているのです。従って、
foo() && bar()
というコードは、
foo()
が0を戻してきた場合、そこで打ち切られ、
bar()
は呼び出されることはありません。
foo()
が0になることが分かった時点で式全体の値が0になることが分かりますから、残
りを評価しないという仕様は無駄のないことといえるでしょう。
しかし、場合によっては、結果にかかわらず、とりあえずどちらの関数も呼び 出しておきたいということがあります。この場合、明らかに全く問題のない方法 は、
i = foo(); i = bar() && i;のように別々に呼び出すことです。
プログラマーによっては、凝った方法を使う人もいます。ここでは、
foo()
も
bar()
も0または1という値を戻すことが分かっていて、かつ、どちらが先に呼ばれても
結果としては影響がなく、さらに、どのような場合でもとりあえず両方の関数を
呼び出したい、という条件の下でのみ、
foo() & bar()
という書き方が意味を持ってきます。
i = (c >= 0x81 && c <= 0x9f) || (c >= 0xe0 && c <= 0xfc) /* (1) */ i = c >= 0x81 && (c <= 0x9f || (c >= 0xe0 && c <= 0xfc)) /* (2) */
&&
や
||
が、左から右へ評価することに注目してください。評価の順序が分かるように、
if
で書いてみると、次のようになります。
(1) if (c >= 0x81) { if (c <= 0x9f) i = 1; else i = 0; } else { if (c >= 0xe0) { if (c <= 0xfc) i = 1; else i = 0; } else { i = 0; } } (2) if (c >= 0x81) { if (c < 0x9f) { i = 1; } else { if (c >= 0xe0) { if (c <= 0xfc) i = 1; else i = 0; } else { i = 0; } } } else { i = 0; }内容に殆ど違いがないことが分かります。一つだけ明白な違いは、
c <= 0x80
の場合の評価の回数です。この範囲にある場合、(1)の方法だと、まず、
c >= 0x81
の評価で偽になりますから、
||
の後の評価に移ります。そして、
c >= 0xe0
の評価も偽になり、結果としては偽になるでしょう。しかし、考えてみれば、
c >= 0x81
の結果が偽ならば、0x81よりも小さい数であることがその時点で分かっているの
ですから、わざわざ再度確認するまでもなく、
c >= 0xe0
も必ず偽になことが明らかです。(2)では、
c >= 0x81
が偽になったらその時点で全体の値を偽にするので、一度の評価で済ませている
ことになります。
(1)と(2)のどちらが良いかというのは、議論の余地があると思われます。