C言語での不特定の動作の例は、関数への引数の評価の順序です。それは左から右または右から左かもしれません、あなたはただ知りません。これは、foo(c++, c)
またはfoo(++c, c)
の評価方法に影響します。
知らないプログラマーを驚かせる可能性のある他の不特定の動作はありますか?
言語弁護士の質問。うーん。
私の個人的なトップ3:
厳密なエイリアシングルールに違反している
:-)
編集これは2回間違っている小さな例です:
(32ビットintとリトルエンディアンを想定)
float funky_float_abs (float a)
{
unsigned int temp = *(unsigned int *)&a;
temp &= 0x7fffffff;
return *(float *)&temp;
}
そのコードは、floatの表現で符号ビットを直接ビットをいじることによって、floatの絶対値を取得しようとします。
ただし、ある型から別の型にキャストしてオブジェクトへのポインタを作成した結果は無効です。C。コンパイラは、異なる型へのポインタが同じメモリチャンクを指していないと想定する場合があります。これは、void *とchar *を除くすべての種類のポインターに当てはまります(符号は関係ありません)。
上記の場合、私はそれを2回行います。 1回はfloataのintエイリアスを取得し、もう1回は値をfloatに変換し直します。
同じことを行うための3つの有効な方法があります。
キャスト中にcharまたはvoidポインタを使用します。これらは常に何かのエイリアスであるため、安全です。
float funky_float_abs (float a)
{
float temp_float = a;
// valid, because it's a char pointer. These are special.
unsigned char * temp = (unsigned char *)&temp_float;
temp[3] &= 0x7f;
return temp_float;
}
Memcopyを使用します。 Memcpyはvoidポインターを受け取るため、エイリアシングも強制します。
float funky_float_abs (float a)
{
int i;
float result;
memcpy (&i, &a, sizeof (int));
i &= 0x7fffffff;
memcpy (&result, &i, sizeof (int));
return result;
}
3番目の有効な方法:ユニオンを使用します。これは明示的にC99以降未定義ではありません:
float funky_float_abs (float a)
{
union
{
unsigned int i;
float f;
} cast_helper;
cast_helper.f = a;
cast_helper.i &= 0x7fffffff;
return cast_helper.f;
}
私の個人的なお気に入りの未定義の動作は、空でないソースファイルが改行で終わっていない場合、動作は未定義です。
警告を出す以外に、改行が終了したかどうかに応じてソースファイルを異なる方法で処理したコンパイラはありませんが、それは本当だと思います。したがって、警告に驚かれる可能性があることを除けば、知らないプログラマーを驚かせるようなことではありません。
したがって、本物の移植性の問題(ほとんどが未指定または未定義ではなく実装に依存しますが、それは質問の精神に該当すると思います)の場合:
if (x+1 < x)
x
が型に署名している場合は、常にfalseとして最適化される可能性があります。GCCの-fstrict-overflow
オプションを参照してください)。動作が部分的に定義されていない/指定されていないため、開発したプラットフォームでも驚く可能性のある本当に深刻なもの:
POSIXスレッドとANSIメモリモデル。メモリへの同時アクセスは、初心者が考えるほど明確には定義されていません。 volatileは、初心者が考えることをしません。メモリアクセスの順序は、初心者が考えるほど明確に定義されていません。アクセスcanメモリバリアを越えて特定の方向に移動します。メモリキャッシュの一貫性は必要ありません。
コードのプロファイリングは、思ったほど簡単ではありません。テストループに影響がない場合、コンパイラはその一部またはすべてを削除できます。インラインには定義された効果はありません。
そして、私が思うに、ニルスは通過で言及しました:
何かへのポインタで何かを分割する。なんらかの理由でコンパイルされません... :-)
result = x/*y;
私のお気に入りはこれです:
// what does this do?
x = x++;
いくつかのコメントに答えるために、それは標準による未定義の動作です。これを見て、コンパイラはハードドライブのフォーマットまで何でもすることができます。たとえば、 このコメントはこちら を参照してください。重要なのは、何らかの行動が合理的に期待できる可能性があることがわかるということではありません。 C++標準とシーケンスポイントの定義方法により、このコード行は実際には未定義の動作です。
たとえば、上の行の前にx = 1
がある場合、その後の有効な結果はどうなりますか?誰かがそうあるべきだとコメントしました
xは1ずつ増加します
したがって、後でx == 2が表示されるはずです。ただし、これは実際には当てはまりません。後でx == 1になるコンパイラ、またはx == 3になるコンパイラもあります。生成されたアセンブリを詳しく調べて、その理由を確認する必要がありますが、違いは原因です。根本的な問題に。基本的に、これは、コンパイラーが2つの代入ステートメントを好きな順序で評価できるため、最初にx++
を実行するか、最初にx =
を実行できるためだと思います。
私が遭遇した別の問題(定義されていますが、間違いなく予期しないものです)。
charは悪です。
Printfフォーマット指定子を引数に一致するように修正した回数を数えることができません。 不一致は未定義の動作です。
int
(またはlong
)を%x
に渡してはなりません-unsigned int
が必要ですunsigned int
を%d
に渡してはなりません-int
が必要ですsize_t
を%u
または%d
に渡してはなりません-%zu
を使用してください%d
または%x
でポインタを出力してはなりません-%p
を使用して、void *
にキャストしてください関数プロトタイプが利用できない場合、コンパイラは、間違った数のパラメータ/間違ったパラメータタイプで関数を呼び出していることを通知する必要はありません。
私は、比較的経験の浅いプログラマーが複数文字の定数に噛まれるのを見てきました。
この:
"x"
文字列リテラルです(タイプはchar[2]
で、ほとんどのコンテキストでchar*
に減衰します)。
この:
'x'
は通常の文字定数です(歴史的な理由から、タイプはint
です)。
この:
'xy'
も完全に正当な文字定数ですが、その値(まだタイプint
)は実装定義です。これはほとんど役に立たない言語機能であり、主に混乱を引き起こすのに役立ちます。
Clang開発者は、しばらく前にいくつかの すばらしい例 を投稿しました。投稿では、すべてのCプログラマーが読む必要があります。以前に言及されていないいくつかの興味深いもの:
ここのEEは、a >>-2が少し問題があることを発見しました。
私はうなずいて、それは自然ではないと彼らに言いました。
変数を使用する前に、必ず変数を初期化してください。私がCを始めたばかりのとき、それは私に多くの頭痛の種を引き起こしました。
「max」や「isupper」などの関数のマクロバージョンを使用する。マクロは引数を2回評価するため、max(++ i、j)またはisupper(* p ++)を呼び出すと、予期しない副作用が発生します。
上記は標準C用です。C++では、これらの問題はほとんどなくなりました。 max関数はテンプレート化された関数になりました。