1

私はこのようなコードを持っています

#include  <stdio.h>

int main()
{
    int A[4] = {3, 519, 27, 49};
    (A[1]) ^=  (A[3]);
    (A[3]) ^=  (A[1]);
    (A[1]) ^=  (A[3]);

    printf("%d, %d, %d, %d\n", A[0], A[1], A[2], A[3]);

    A[1] ^= A[3] ^= A[1] ^= A[3];

    printf("%d, %d, %d, %d\n", A[0], A[1], A[2], A[3]);
}

A[1] と A[3] の値を交換したい。最初の printf では、3、49、27、519 という答えが得られました。これは正しいです。しかし、2 番目については、3、0、27、49 を取得します。「A[1] ^= A[3] ^= A[1] ^= A[3];」というステートメントだと思います。に翻訳されています:

A[1] = A[1] ^ A[3];
A[3] = A[3] ^ A[1] ^ A[3];
A[1] = A[1] ^ A[3] ^ A[1] ^ A[3];

これらの式を計算すると、A[1] は常に 519 になり、A[3] は 49 になります。gdb でデバッグすると、このステートメントで A[1] が最初に 49 -> 566 に変更され、次に A[3] が 519 から 49 に変更され、次に A[1] が 566 から 0 に変更されることがわかります。

また、次のように宣言を変更しようとしました: volatile int A[4] = {3, 519, 27, 49}; しかし、出力はまだすべて同じです。次のようにステートメントを変更します。 A[1] ^= (A[3] ^= (A[1] ^= A[3])); 答えはまだ間違っています。

しかし、gcc の代わりに g++ を使用してコードをコンパイルすると、正しい答えを得ることができます: 3, 49, 27, 519 3, 519, 27, 49 ステートメントが

int a = 49;
int b = 519;
a ^= b ^= a ^= b;

価値を交換することができます。なぜ配列の要素が間違っているのかわかりません。

4

1 に答える 1

6

2 番目のステートメントは、シーケンス ポイント間で同じ値を 2 回変更しているため、未定義の動作を呼び出します。

これが未定義の動作である理由は、コンパイラが効率的なコードを生成できるようにするには、このような緩いルールが必要だからです。あなたの例では明らかではありませんが、次のような関数がある場合:

void
foo(int *a, int *b, int *c, int *d)
{
    *a ^= *b ^= *c ^= *d;
}

これは、マシンコードで次のように変換されます。

load r1 (register 1) with value at d
load r2 with value at c
load r3 with value at b
load r4 with value at a
r2 = r1 xor r2
r3 = r2 xor r3
r4 = r3 xor r4
store r2 at c
store r3 at b
store r4 at a

ただし、コンパイラは、ポインターが同じメモリを指しているかどうかを知りません。したがって、この関数の厳密な順序付けを強制したい場合は、次のようにする必要があります。

load r1 with value at d
load r2 with value at c
r2 = r1 xor r2
store r2 at c
load r1 with value at b
r2 = r1 xor r2
store r2 at b
load r1 with value at a
r2 = r1 xor r2
store r2 at a

さて、これは同じ量の作業のように見えますね。同じ量の命令を異なる順序で、おまけとして使用するレジスタを減らしています。では、なぜですか?その理由は、メモリが遅いためです。CPU は次の命令を実行する前に前の命令が終了するまで実際には待機しないため、命令の最初のシーケンスはより高速に実行されます。開始され、まだ終了していない多数の命令を持つことができます。最初の例で r1 の値が必要になるまでに、メモリからさらに 3 つの読み込みを開始しました。これは大したことではないように聞こえますが、このようなことは合計されます。

したがって、C標準では、コンパイラが生成するために必要な場合、シーケンスポイント間で必要なことをほとんど実行できると判断しました(この回答の最初の文で、シーケンスポイントとは何かを説明するwikiページをリンクしました)。効率的なコード。これは、計算の正しい結果を保証したい場合は、特定の規則に従わなければならないことを意味します (そして、コンパイラはほとんどの場合、警告しません)。2 つのシーケンス ポイント間で同じ値を 2 回変更することはできません。2 つのシーケンス ポイント間の値を読み取って変更する場合は、変更を計算するためだけに読み取ることができます。等々。何が未定義かについてはたくさんのルールがあり、それらのほとんどすべてが未定義です。これは、コンパイラがさまざまな CPU アーキテクチャで高速なコードを生成できるようにするためです。

于 2013-10-16T07:19:33.617 に答える