テストケースは次のとおりです。
void foo(int i, int j)
{
printf("%d %d", i, j);
}
...
test = 0;
foo(test++, test);
「0 1」の出力が得られると期待していましたが、「0 0」が得られました。何が得られますか??
テストケースは次のとおりです。
void foo(int i, int j)
{
printf("%d %d", i, j);
}
...
test = 0;
foo(test++, test);
「0 1」の出力が得られると期待していましたが、「0 0」が得られました。何が得られますか??
これは、不特定の動作の例です。標準では、引数を評価する順序は規定されていません。これは、コンパイラの実装上の決定です。コンパイラは、関数への引数を任意の順序で自由に評価できます。
この場合、予想される左から右ではなく、実際には右から左に引数を処理しているように見えます。
一般に、引数で副作用を行うことは、プログラミングの悪い習慣です。
foo(test++, test);の代わりに foo(test, test+1); と書く必要があります。テスト++;
これは、達成しようとしているものと意味的に同等です。
編集:Anthonyが正しく指摘しているように、シーケンスポイントを介在させずに単一の変数を読み取りおよび変更することは定義されていません。したがって、この場合、動作は実際にはundefinedです。したがって、コンパイラは必要なコードを自由に生成できます。
これは単なる不特定の動作ではなく、実際には未定義の動作です。
はい、引数評価の順序は指定されていませんが、読み取りが新しい値の計算のみを目的としている場合を除き、シーケンス ポイントを介在させずに単一の変数を読み取りおよび変更することは定義されていません。関数の引数の評価の間にシーケンス ポイントがないため、動作f(test,test++)
は未定義ですtest
。一方の引数が読み取られ、他方が変更されます。変更を関数に移動すると、問題ありません。
int preincrement(int* p)
{
return ++(*p);
}
int test;
printf("%d %d\n",preincrement(&test),test);
これは、 への入り口と出口にシーケンス ポイントがあるためですpreincrement
。そのため、単純な読み取りの前または後に呼び出しを評価する必要があります。現在、順序は未指定です。
コンマ演算子はシーケンス ポイントを提供することにも注意してください。
int dummy;
dummy=test++,test;
問題ありません --- 増分は読み取りの前に発生するためdummy
、新しい値に設定されます。
私が最初に言ったことはすべて間違っています!副作用が計算される時点は指定されていません。Visual C++ は、test がローカル変数の場合、foo() の呼び出し後にインクリメントを実行しますが、test が static または global として宣言されている場合は、foo() の呼び出しの前にインクリメントされ、異なる結果が生成されます。テストは正しいでしょう。
インクリメントは、実際には foo() の呼び出し後に別のステートメントで行う必要があります。動作が C/C++ 標準で指定されていたとしても、混乱を招きます。C++ コンパイラはこれを潜在的なエラーとしてフラグを立てると思うでしょう。
これは、シーケンスポイントと未指定の動作の適切な説明です。
<----間違った間違いの始まり---->
「test++」の「++」ビットは、foo の呼び出し後に実行されます。したがって、(1,0) ではなく、(0,0) を foo に渡します。
Visual Studio 2002 からのアセンブラー出力は次のとおりです。
mov ecx, DWORD PTR _i$[ebp]
push ecx
mov edx, DWORD PTR tv66[ebp]
push edx
call _foo
add esp, 8
mov eax, DWORD PTR _i$[ebp]
add eax, 1
mov DWORD PTR _i$[ebp], eax
インクリメントは foo() の呼び出し後に行われます。この動作は設計によるものですが、一般の読者にとっては紛らわしいので、おそらく避けるべきです。インクリメントは、foo() の呼び出し後に別のステートメントで行う必要があります。
<----間違った間違いの終わり---->
これは「指定されていない動作」ですが、実際には、C コール スタックが指定されている方法では、ほとんどの場合、0, 0 として表示され、1, 0 として表示されないことが保証されます。
誰かが指摘したように、VC によるアセンブラー出力は、最初にスタックの一番右のパラメーターをプッシュします。これは、C 関数呼び出しがアセンブラーで実装される方法です。これは、C の「無限パラメーター リスト」機能に対応するためです。パラメータを右から左の順序でプッシュすることにより、最初のパラメータがスタックの一番上の項目になることが保証されます。
printf の署名を取得します。
int printf(const char *format, ...);
これらの楕円は、不明な数のパラメーターを示します。パラメータが左から右にプッシュされた場合、フォーマットはサイズがわからないスタックの一番下になります。
C (および C++) ではパラメーターが左から右に処理されることを知っていれば、関数呼び出しを解析して解釈する最も簡単な方法を判断できます。パラメーター リストの最後に到達し、プッシュを開始し、複雑なステートメントを評価します。
ただし、ほとんどの C コンパイラには関数を "Pascal スタイル" で解析するオプションがあるため、これでも節約できません。つまり、関数パラメーターは左から右にスタックにプッシュされます。たとえば、printf が Pascal オプションでコンパイルされた場合、出力はおそらく 1, 0 になります (ただし、printf は楕円を使用するため、Pascal スタイルでコンパイルできるとは思えません)。
C では、関数呼び出しでのパラメーターの評価順序が保証されていないため、これを使用すると、結果が "0 1" または "0 0" になる場合があります。順序はコンパイラごとに変わる可能性があり、同じコンパイラが最適化パラメーターに基づいて異なる順序を選択する可能性があります。
foo(test, test + 1) と書いて、次の行で ++test を実行する方が安全です。とにかく、可能であれば、コンパイラはそれを最適化する必要があります。
他の人が言ったことを繰り返すと、これは不特定の振る舞いではなく、むしろ未定義です。このプログラムは、合法的に何でも何でも出力したり、nを任意の値のままにしたり、上司に侮辱的な電子メールを送信したりすることができます。
実際のところ、コンパイラの作成者は通常、最も簡単に作成できることを実行します。これは、通常、プログラムがnを1回または2回フェッチし、関数を呼び出し、いつかインクリメントすることを意味します。これは、他の考えられる動作と同様に、標準によれば問題ありません。コンパイラ間、バージョン間、または異なるコンパイラオプションで同じ動作を期待する理由はありません。同じプログラム内の2つの異なるが似たような例を一貫してコンパイルする必要がある理由はありませんが、それは私が賭ける方法です。
要するに、これをしないでください。興味がある場合は、さまざまな状況でテストしてください。ただし、正しい結果または予測可能な結果が1つあると偽ってはいけません。
C 標準によると、単一のシーケンス ポイント (ここでは、ステートメントまたは関数へのパラメーターと考えることができます) 内の変数への複数の参照を持つことは、未定義の動作です。前後の変更。foo(f++,f) <-- f がインクリメントするタイミングについては未定義です。同様に (これはユーザー コードで常に見られます): *p = p++ + p;
通常、コンパイラはこの種の動作を変更しません (メジャー リビジョンを除く)。
警告をオンにして注意を払うことで回避してください。
ええと、OPが一貫性のために編集されたので、回答と同期していません. 評価の順序に関する基本的な答えは正しいです。ただし、特定の可能な値は foo(++test, test); では異なります。場合。
++testは渡される前にインクリメントされるため、最初の引数は常に 1 になります。2 番目の引数は、評価順序に応じて 0 または 1 になります。
関数への引数の評価順序は定義されていません。この場合、右から左に行ったようです。
(基本的に、シーケンス ポイント間で変数を変更すると、コンパイラは必要なことを何でも実行できます。)
コンパイラは、期待どおりの順序で引数を評価していない可能性があります。