C で遭遇する一般的な未定義/未仕様の動作は何ですか?[閉まっている]
-
01-07-2019 - |
質問
C 言語における未指定の動作の例としては、関数の引数の評価順序が挙げられます。それは左から右かもしれないし、右から左かもしれませんが、あなたにはわかりません。これは方法に影響します foo(c++, c)
または foo(++c, c)
評価される。
何も知らないプログラマを驚かせる可能性のある、他にどのような不特定の動作があるでしょうか?
解決
言語弁護士の質問です。うーん。
私の個人的なトップ3:
- 厳密なエイリアス規則に違反しています
- 厳密なエイリアス規則に違反しています
厳密なエイリアス規則に違反しています
:-)
編集 以下は、2 回間違っている小さな例です。
(32 ビット整数とリトルエンディアンを想定)
float funky_float_abs (float a)
{
unsigned int temp = *(unsigned int *)&a;
temp &= 0x7fffffff;
return *(float *)&temp;
}
このコードは、float の表現内で直接符号ビットをビットいじることによって、float の絶対値を取得しようとします。
ただし、ある型から別の型にキャストしてオブジェクトへのポインターを作成した結果は、有効な C ではありません。コンパイラは、異なる型へのポインタが同じメモリ チャンクを指していないと想定する場合があります。これは、 void* と char* を除くすべての種類のポインターに当てはまります (符号の有無は関係ありません)。
上記の場合、これを 2 回実行します。1 回は float a の 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;
}
メモコピーを使用します。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;
}
他のヒント
私が個人的に気に入っている未定義の動作は、空ではないソース ファイルが改行で終わらない場合の動作が未定義であることです。
しかし、これは真実ではないかと思いますが、警告を発する以外に、ソース ファイルが改行で終了しているかどうかに応じてソース ファイルを異なる方法で処理するコンパイラはこれまで見たことがありません。したがって、警告に驚くかもしれないという点を除けば、何も知らないプログラマが驚くようなものではありません。
したがって、真の移植性の問題については、次のようになります(これは、未指定または未定義ではなく実装に依存することがほとんどですが、それは質問の精神に当てはまると思います)。
- char は必ずしも署名されていないわけではありません。
- int は 16 ビットから任意のサイズにできます。
- float は必ずしも IEEE 形式または準拠しているとは限りません。
- 整数型は必ずしも 2 の補数であるとは限りません。また、整数算術オーバーフローは未定義の動作を引き起こします (最新のハードウェアはクラッシュしませんが、一部のコンパイラの最適化により、ハードウェアの動作であってもラップアラウンドとは異なる動作が発生します)。例えば
if (x+1 < x)
次の場合は常に false として最適化される可能性があります。x
署名された型があります:見る-fstrict-overflow
GCC のオプション)。 - "/"、 "。 #includeには「..」には意味があり、異なるコンパイラによって異なる扱いをすることができます(これは実際には異なり、間違っている場合、それはあなたの日を台無しにします)。
本当に深刻なものは、動作が部分的にのみ未定義/未指定であるため、開発したプラットフォーム上でも驚くべきものです。
POSIX スレッドと ANSI メモリ モデル。メモリへの同時アクセスは、初心者が考えるほど明確に定義されていません。volatile は初心者が考えるようなことはしません。メモリアクセスの順序は、初心者が考えるほど明確に定義されていません。アクセス できる メモリバリアを越えて特定の方向に移動することができます。メモリ キャッシュの一貫性は必要ありません。
コードのプロファイリングは思っているほど簡単ではありません。テスト ループに効果がない場合、コンパイラはその一部またはすべてを削除できます。inline には定義された効果はありません。
そして、ニルスはついでにこうも言っていたと思います。
- 厳格なエイリアシング規則に違反しています。
何かへのポインタによって何かを分割する。何らかの理由でコンパイルされません...:-)
result = x/*y;
私のお気に入りはこれです:
// what does this do?
x = x++;
いくつかのコメントに答えると、これは標準に従って未定義の動作です。これを見て、コンパイラはハード ドライブのフォーマットまでのあらゆる操作を許可されます。たとえばを参照してください このコメントはここにあります. 。重要なのは、何らかの動作が合理的に期待される可能性があることがわかるということではありません。C++ 標準とシーケンス ポイントの定義方法により、このコード行は実際には未定義の動作になります。
たとえば、 x = 1
上の行の前では、その後の有効な結果はどうなるでしょうか?そうあるべきだと誰かがコメントした
xは1ずつ増加します
したがって、その後 x == 2 が表示されるはずです。ただし、これは実際には当てはまりません。コンパイラによっては、後で x == 1 になったり、場合によっては x == 3 になったりする場合もあります。その理由を知るには、生成されたアセンブリを詳しく調べる必要がありますが、違いは根本的な問題によるものです。基本的に、これはコンパイラが 2 つの代入ステートメントを任意の順序で評価できるため、次のことを実行できるためだと思います。 x++
最初に、または x =
初め。
私が遭遇した別の問題 (これは定義されていますが、明らかに予期せぬものです)。
チャーは悪だ。
- コンパイラの認識に応じて署名または署名なし
- ない 8ビットとして義務付けられています
引数に一致するように 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 プログラマーが読むべき記事を書きました。これまでに言及されていない興味深いものをいくつか示します。
- 符号付き整数のオーバーフロー - いいえ、最大値を超えて符号付き変数をラップすることは問題ありません。
- NULL ポインターの逆参照 - はい、これは未定義であり、無視される可能性があります。リンクのパート 2 を参照してください。
ここの EE は、>>-2 が少し厄介なことに気づきました。
私はうなずいて、それは不自然なことだと言いました。
変数を使用する前に必ず初期化してください。私が C を始めたばかりのとき、それは私に多くの頭痛の種を引き起こしました。
「max」や「isupper」などの関数のマクロバージョンを使用します。マクロは引数を 2 回評価するため、max(++i, j) または isupper(*p++) を呼び出すと予期しない副作用が発生します。
上記は標準 C の場合です。C++ では、これらの問題はほとんど解消されました。max 関数はテンプレート関数になりました。
追加するのを忘れる static float foo();
ヘッダー ファイルでは、0.0f を返す場合に浮動小数点例外がスローされるだけです。