C言語での不特定の動作の例は、関数への引数の評価の順序です。それは左から右または右から左かもしれません、あなたは知らないだけです。foo(c++, c)
これは、評価方法や評価方法に影響しますfoo(++c, c)
。
知らないプログラマーを驚かせる可能性のある他の不特定の動作はありますか?
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)
、常に false として最適化される場合があります。x
型が署名されている場合: GCC のオプションを参照し-fstrict-overflow
てください)。動作が部分的に未定義/未指定であるため、開発したプラットフォームでさえ驚くことができる本当に深刻なもの:
POSIX スレッド化と ANSI メモリ モデル。メモリへの同時アクセスは、初心者が考えるほど明確に定義されていません。volatile は、初心者が考えることをしません。メモリ アクセスの順序は、初心者が考えるほど明確に定義されていません。アクセスは、特定の方向でメモリ バリアを越えて移動できます。メモリ キャッシュの一貫性は必要ありません。
コードのプロファイリングは、思ったほど簡単ではありません。テスト ループが効果がない場合、コンパイラはその一部または全部を削除できます。inline には定義された効果はありません。
そして、ニルスが通りすがりに言及したと思うように:
何かへのポインタで何かを分割します。なんらかの理由でコンパイルされません...:-)
result = x/*y;
私のお気に入りはこれです:
// what does this do?
x = x++;
いくつかのコメントに答えるために、それは標準による未定義の動作です。これを見て、コンパイラはハードドライブのフォーマットまで何でもできるようになります。たとえば、このコメントをここで参照してください。重要なのは、何らかの行動が合理的に期待できる可能性があることを確認できるということではありません。C ++標準とシーケンスポイントの定義方法により、このコード行は実際には未定義の動作です。
たとえば、x = 1
上記の行の前にあった場合、その後の有効な結果はどうなりますか?誰かがそうあるべきだとコメントしました
xは1ずつ増加します
したがって、後でx==2が表示されるはずです。ただし、これは実際には当てはまりません。後でx == 1になるコンパイラ、またはx == 3になるコンパイラもあります。生成されたアセンブリを詳しく調べて、なぜそうなるのかを確認する必要がありますが、違いは原因です。根本的な問題に。x++
基本的に、これは、コンパイラーが2つの代入ステートメントを好きな順序で評価できるため、最初のステートメントまたは最初のステートメントを実行できるためだと思いますx =
。
私が遭遇した別の問題(これは定義されていますが、間違いなく予期しないものです)。
charは悪です。
引数に一致するように printf 書式指定子を修正した回数は数え切れません。不一致は未定義の動作です。
int
いいえ、 (またはlong
) を to に渡してはいけません%x
- anunsigned int
が必要ですunsigned int
いいえ、 to を渡してはいけません%d
-int
が必要ですsize_t
いいえ、 to%u
または%d
- useを渡してはいけません%zu
%d
いいえ、 orを使用してポインターを出力してはなりません%x
- を使用%p
してにキャストしますvoid *
関数プロトタイプが利用できない場合、コンパイラは、間違った数のパラメーター/間違ったパラメーター型で関数を呼び出していることを通知する必要はありません。
clang 開発者は、すべての C プログラマーが読むべき投稿で、しばらく前にいくつかの優れた例を投稿しました。前に言及されていないいくつかの興味深いもの:
ここにいる EE は、a>>-2 が少し難しいことを発見しました。
私はうなずき、それは自然なことではないと彼らに伝えました。
変数を使用する前に、必ず変数を初期化してください。私がCを始めたばかりのとき、それは私に多くの頭痛の種を引き起こしました。