5

私は読んだ、誰かがこれらの未定義の振る舞い(i = i ++ + ++ i、i = i ++など)を説明し、2時間以上無駄にした後に「comp.lang.cFAQ」のシーケンスポイントを理解しようとし ましたか? gccコンパイラで次の結果を説明しようとしています。

expression(i=1;j=2)     i       j       k
k = i++ + j++;          2       3       3
k = i++ + ++j;          2       3       4
k = ++i + j++;          2       3       4
k = ++i + ++j;          2       3       5

k = i++ + i++;          3               2
k = i++ + ++i;          3               4
k = ++i + i++;          3               4
k = ++i + ++i;          3               6

i = i++ + j++;          4       3
i = i++ + ++j;          5       3
i = ++i + j++;          4       3
i = ++i + ++j;          5       3

i = i++ + i++;          4
i = i++ + ++i;          5
i = ++i + i++;          5
i = ++i + ++i;          6

質問:

  1. 上図に示されているすべての式(4つのグループ)に未定義の動作があるかどうかを知りたいですか?それらの一部だけが未定義の動作をしている場合、どれが実行し、どれが実行しないのですか?

  2. 定義された動作式については、 コンパイラがそれらをどのように評価するかを示してください(説明はしません) 。念のために言っておきますが、このプリインクリメントとポストインクリメントを正しく取得したかどうか。

バックグラウンド:

今日、私はキャンパスのインタビューに参加しました。そこではi++ + ++i、与えられた値のの結果を説明するように求められましたi。その表現をgccでコンパイルした後、インタビューで答えた答えが間違っていることに気づきました。私は将来そのような間違いをしないことに決めたので、インクリメント前とインクリメント後の演算子の可能なすべての組み合わせをコンパイルし、それらをgccでコンパイルしてから、結果を説明しようとしました。私は2時間以上苦労しました。これらの表現の評価の単一の振る舞いを見つけることができませんでした。それで、私はあきらめて、stackoverflowに目を向けました。アーカイブを少し読んだ後sequence point、未定義の動作のようなものがあることがわかりました。

4

7 に答える 7

9

最初のグループを除いて、他の3つのグループのすべての式には未定義の動作があります。

定義された動作の評価方法(グループ1):

i=1, j=2;

k=i++ + j++; // 1 + 2 = 3
k=i++ + ++j; // 1 + 3 = 4
k=++i + ++j; // 2 + 3 = 5
k=++i + j++; // 2 + 2 = 4

それはかなり簡単です。ポストインクリメントとプレインクリメントの違い。

グループ2とグループ4では、未定義の動作を簡単に確認できます。

=オペレーターがシーケンスポイントを導入しないため、グループ2の動作は未定義です。

于 2012-11-28T22:11:55.027 に答える
5

上図に示されているすべての式(4つのグループ)に未定義の動作があるかどうかを知りたいですか?

2行目から5行目:

k = i++ + j++;
k = i++ + ++j;
k = ++i + ++j;
k = ++i + j++;

すべて明確に定義されています。他のすべての式は未定義です。これらはすべて、シーケンスポイント間で式を複数回評価することでオブジェクトの値を変更しようとするためです(これらの例では、シーケンスポイントは各ステートメントを終了する';'で発生します)。たとえば、は、割り当てと接尾辞の両方を介して、介在するシーケンスポイントなしでi = i++;の値を変更しようとしているため、未定義です。参考 までに、オペレーターはシーケンスポイントを導入しません。演算子はシーケンスポイントを導入しますi++=|| && ?:,comma

定義された動作式については、コンパイラがそれらをどのように評価するかを示してください(説明はしません)。

から始めましょう

k = i++ + j++;

式はの現在の値にa++評価され、次のシーケンスポイントの前のある時点で、1ずつ増分されます。したがって、論理的には、評価は次のようになります。aa

k = 1 + 2; // i++ evaluates to 1, j++ evaluates to 2
i = i + 1; // i is incremented and becomes 2
j = j + 1; // j is incremented and becomes 3

でも...

i++式とが評価される正確な順序j++、およびそれらの副作用が適用される順序は指定されていません。以下は、(疑似アセンブリコードを使用した)完全に合理的な操作の順序です。

mov j, r0        ; read the value of j into register r0
mov i, r1        ; read the value of i into register r1
add r0, r1, r2   ; add the contents of r0 to r1, store result to r2
mov r2, k        ; write result to k
inc r1           ; increment value of i
inc r0           ; increment value of j
mov r0, j        ; store result of j++
mov r1, i        ; store result of i++

算術式の左から右への評価を想定しないでください。評価の直後にオペランドが更新されると想定しない++--ください。

このため、のような式の結果はi++ + ++i、コンパイラー、コンパイラー設定、さらには周囲のコードによっても異なります。動作は未定義のままであるため、コンパイラは「正しいことを行う」必要はありません。結果は得られますが、必ずしも期待した結果になるとは限らず、すべてのプラットフォームで一貫しているとは限りません

見つめている

k = i++ + ++j;

論理的評価は

k = 1 + 3  // i++ evaluates to i (1), ++j evaluates to j + 1 (2 + 1 = 3)
i = i + 1
j = j + 1

繰り返しになりますが、操作の可能な順序は次のとおりです。

mov j, r0
inc r0
mov i, r1
add r0, r1, r2
mov r2, k
mov r0, j
inc r1
mov r1, i

またはそれは何か他のことをすることができます。コンパイラーは、操作のより効率的な順序につながる場合、個々の式が評価される順序を自由に変更できます(私の例ではほとんど間違いありません)。

于 2012-11-28T22:46:26.997 に答える
4

これらのステートメントのいずれにもシーケンスポイントはありませんそれらの間にはシーケンスポイントがあります。

連続するシーケンスポイント間で同じオブジェクトを2回変更した場合(この場合、=プレフィックスまたはポストフィックスを介して、または経由して++)、動作は定義されていません。したがって、4つのステートメントの最初のグループの動作は明確に定義されています。他の人の振る舞いは未定義です。

動作が定義されている場合は、のの値をi++生成し、副作用としてそれに追加することで変更します。 に追加して変更し、変更された値を生成します。ii1++ii1

于 2012-11-28T22:17:23.410 に答える
2

最初のグループはすべて定義されています。iこれらはすべて、次のシーケンスポイントの前に両方の値とj副作用として値をインクリメントするためi、2とj3のままになります。さらに、i++1と++i評価され、2と評価j++され、2と++j評価され、3と評価されます。 1つ目はに割り当て1 + 2k2つ目はに割り当て1 + 3k3つ目はに割り当て2 + 3k4つ目はに割り当て2 + 2ますk

残りはすべて未定義の動作です。2番目と3番目のグループでiは、シーケンスポイントの前に2回変更されます。4番目のグループiでは、シーケンスポイントの前に3回変更されます。

于 2012-11-28T22:18:40.873 に答える
0

コンパイラが2つの左辺値式が同じオブジェクトを識別することを認識できる場合、それを何らかの賢明な方法で動作させることに意味のあるコストはありません。より興味深いシナリオは、1つ以上のオペランドが逆参照ポインターであるシナリオです。

与えられたコード:

void test(unsigned *a, unsigned *b, unsigned *c)
{
  (*a) = (*b)++ + (*c)++;
}

コンパイラがそれを処理する多くの賢明な方法があります。bとcをロードして追加し、結果をaに格納してから、bとcをインクリメントするか、aとbをロードして、a + b、a + 1、およびb + 1を計算し、それらを書き込むことができます。任意のシーケンス、または無数の他の操作シーケンスのいずれかを実行します。一部のプロセッサーでは、一部の配置が他の配置よりも効率的である可能性があり、コンパイラーは、プログラマーが任意の配置を他の配置よりも適切であると見なすと期待する理由がないはずです。

ほとんどのハードウェアプラットフォームでは、a、b、およびcに同一のポインタを渡すことで生じる可能性のある動作の数は限られていますが、標準の作成者は、もっともらしい結果ともっともらしい結果を区別する努力をしていません。多くの実装は基本的にゼロコストで簡単に動作保証を提供できますが(たとえば、上記のようなコードが常に、、、およびを設定することを保証*aます*b*c他の副作用のないいくつかのおそらく不特定の値に)、そしてそのような保証が時々役立つかもしれないとしても(オブジェクトの値が重要である場合にポインタが別個のオブジェクトを識別するが、そうでなければそうしないかもしれない場合)それはファッショナブルですコンパイラーの作成者にとって、任意に破壊的な副作用を引き起こすためにカルテブランシュが付与されたときに達成できる有用な最適化のわずかな可能性を考慮することは、プログラマーが制約された動作の保証から受け取ることができる価値よりも価値があります。

于 2017-05-18T19:23:49.753 に答える
0

カンマは少し注意が必要です。それらはペアの場合は左から右に移動します(実際にはforループのvarsの場合)。カンマで区切られたステートメントは、2つ以上のステートメントに配置された場合、特定の順序で評価されるとは限りません。また、関数の引数と宣言がコンマで区切られている場合、実行の順序は保証されないことに注意してください。

それで

int a=0;
function_call(++a, ++a, ++a);

予測できない結果になる可能性があります。

于 2019-06-04T13:49:40.773 に答える
-1

ほとんどの場合、gccは最初に事前増分を実装し、それらの値を操作で使用し、その後、事後増分を評価します。

例えば。ブロック2では、事前に増分がないため、i1が使用されます

k = i++ + i++ // hence k = 1+1=2

そして、iの2つのポスト増分なのでi = 3

1つの事前インクリメントによりiが2に変更されます

k = i++ + ++i // hence k= 2+2= 4

iそうで1ポストインクリメントi= 3

についても同じk= ++i + i++

2つの事前増分でi3になります

k=++i + ++i // hence k=3+3= 6

i = 3

それが少し説明することを願っています。しかし、それは純粋にコンパイラに依存します。

于 2017-05-18T15:38:48.933 に答える