web-dev-qa-db-ja.com

あなたが遭遇するCの一般的な未定義/未指定の動作は何ですか?

C言語での不特定の動作の例は、関数への引数の評価の順序です。それは左から右または右から左かもしれません、あなたはただ知りません。これは、foo(c++, c)またはfoo(++c, c)の評価方法に影響します。

知らないプログラマーを驚かせる可能性のある他の不特定の動作はありますか?

63
Benoit

言語弁護士の質問。うーん。

私の個人的なトップ3:

  1. 厳密なエイリアシングルールに違反している
  2. 厳密なエイリアシングルールに違反している
  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;
}
72

私の個人的なお気に入りの未定義の動作は、空でないソースファイルが改行で終わっていない場合、動作は未定義です。

警告を出す以外に、改行が終了したかどうかに応じてソースファイルを異なる方法で処理したコンパイラはありませんが、それは本当だと思います。したがって、警告に驚かれる可能性があることを除けば、知らないプログラマーを驚かせるようなことではありません。

したがって、本物の移植性の問題(ほとんどが未指定または未定義ではなく実装に依存しますが、それは質問の精神に該当すると思います)の場合:

  • charは必ずしも(un)signedではありません。
  • intは、16ビットから任意のサイズにすることができます。
  • フロートは、必ずしもIEEE形式または準拠である必要はありません。
  • 整数型は必ずしも2の補数ではなく、整数算術オーバーフローは未定義の動作を引き起こします(最新のハードウェアはクラッシュしませんが、一部のコンパイラ最適化では、ハードウェアが行うことであっても、ラップアラウンドとは異なる動作になります。たとえば、if (x+1 < x)xが型に署名している場合は、常にfalseとして最適化される可能性があります。GCCの-fstrict-overflowオプションを参照してください)。
  • 「/」、「。」 #include内の「..」には定義された意味がなく、コンパイラーによって異なる方法で処理される可能性があります(これは実際には異なり、うまくいかないと1日が台無しになります)。

動作が部分的に定義されていない/指定されていないため、開発したプラットフォームでも驚く可能性のある本当に深刻なもの:

  • POSIXスレッドとANSIメモリモデル。メモリへの同時アクセスは、初心者が考えるほど明確には定義されていません。 volatileは、初心者が考えることをしません。メモリアクセスの順序は、初心者が考えるほど明確に定義されていません。アクセスcanメモリバリアを越えて特定の方向に移動します。メモリキャッシュの一貫性は必要ありません。

  • コードのプロファイリングは、思ったほど簡単ではありません。テストループに影響がない場合、コンパイラはその一部またはすべてを削除できます。インラインには定義された効果はありません。

そして、私が思うに、ニルスは通過で言及しました:

  • 厳密なエイリアシングルールへの違反。
30
Steve Jessop

何かへのポインタで何かを分割する。なんらかの理由でコンパイルされません... :-)

result = x/*y;
22
Adam Pierce

私のお気に入りはこれです:

// what does this do?
x = x++;

いくつかのコメントに答えるために、それは標準による未定義の動作です。これを見て、コンパイラはハードドライブのフォーマットまで何でもすることができます。たとえば、 このコメントはこちら を参照してください。重要なのは、何らかの行動が合理的に期待できる可能性があることがわかるということではありません。 C++標準とシーケンスポイントの定義方法により、このコード行は実際には未定義の動作です。

たとえば、上の行の前にx = 1がある場合、その後の有効な結果はどうなりますか?誰かがそうあるべきだとコメントしました

xは1ずつ増加します

したがって、後でx == 2が表示されるはずです。ただし、これは実際には当てはまりません。後でx == 1になるコンパイラ、またはx == 3になるコンパイラもあります。生成されたアセンブリを詳しく調べて、その理由を確認する必要がありますが、違いは原因です。根本的な問題に。基本的に、これは、コンパイラーが2つの代入ステートメントを好きな順序で評価できるため、最初にx++を実行するか、最初にx =を実行できるためだと思います。

19

私が遭遇した別の問題(定義されていますが、間違いなく予期しないものです)。

charは悪です。

  • コンパイラの感触に応じて符号付きまたは符号なし
  • not 8ビットとして必須
10
itj

Printfフォーマット指定子を引数に一致するように修正した回数を数えることができません。 不一致は未定義の動作です

  • いいえ、int(またはlong)を%xに渡してはなりません-unsigned intが必要です
  • いいえ、unsigned int%dに渡してはなりません-intが必要です
  • いいえ、size_t%uまたは%dに渡してはなりません-%zuを使用してください
  • いいえ、%dまたは%xでポインタを出力してはなりません-%pを使用して、void *にキャストしてください
8
Jens

関数プロトタイプが利用できない場合、コンパイラは、間違った数のパラメータ/間違ったパラメータタイプで関数を呼び出していることを通知する必要はありません。

7
mbac32768

私は、比較的経験の浅いプログラマーが複数文字の定数に噛まれるのを見てきました。

この:

"x"

文字列リテラルです(タイプはchar[2]で、ほとんどのコンテキストでchar*に減衰します)。

この:

'x'

は通常の文字定数です(歴史的な理由から、タイプはintです)。

この:

'xy'

も完全に正当な文字定数ですが、その値(まだタイプint)は実装定義です。これはほとんど役に立たない言語機能であり、主に混乱を引き起こすのに役立ちます。

6
Keith Thompson

Clang開発者は、しばらく前にいくつかの すばらしい例 を投稿しました。投稿では、すべてのCプログラマーが読む必要があります。以前に言及されていないいくつかの興味深いもの:

  • 符号付き整数オーバーフロー-いいえ、符号付き変数を最大値を超えてラップすることはできません。
  • NULLポインタの逆参照-はい、これは未定義であり、無視される可能性があります。リンクのパート2を参照してください。
4
Per Johansson

ここのEEは、a >>-2が少し問題があることを発見しました。

私はうなずいて、それは自然ではないと彼らに言いました。

2
Tim Williscroft

変数を使用する前に、必ず変数を初期化してください。私がCを始めたばかりのとき、それは私に多くの頭痛の種を引き起こしました。

1
William Keller

「max」や「isupper」などの関数のマクロバージョンを使用する。マクロは引数を2回評価するため、max(++ i、j)またはisupper(* p ++)を呼び出すと、予期しない副作用が発生します。

上記は標準C用です。C++では、これらの問題はほとんどなくなりました。 max関数はテンプレート化された関数になりました。

0
Mike Thompson