これらの構造がインクリメント前およびインクリメント後の未定義の動作を使用しているのはなぜですか?
-
09-09-2019 - |
質問
#include <stdio.h>
int main(void)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 3
i = 1;
i = (i++);
printf("%d\n", i); // 2 Should be 1, no ?
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 1
u = 1;
u = (u++);
printf("%d\n", u); // 2 Should also be one, no ?
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); // 3 (Should be the same as u ?)
int w = 0;
printf("%d %d\n", ++w, w); // shouldn't this print 1 1
int x[2] = { 5, 8 }, y = 0;
x[y] = y ++;
printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
解決
Cはすなわち、未定義の動作の概念を持っているいくつかの言語構造は、構文的に有効ですが、コードが実行されると、あなたは行動を予測することはできません。
私の知る限りでは、標準では明示的に定義されていない動作のの理由の概念が存在すると言うことはありません。私の心の中で、それは言語設計者は、すべての実装は非常に可能性の高いパフォーマンスに重大なコストを課すとまったく同じ方法で、中に整数オーバーフローを処理することを要求するいくつかの意味での余裕、代わりのつまりがあるように望んでいたので、単純だが、彼らは行動を左にあなたは、整数オーバーフローが発生するコードを記述する場合、何が起こることができるように未定義ます。
だから、このことを念頭に置いて、なぜこれらの「問題」はありますか?言語は明らかに、特定の物事がhref="http://en.wikipedia.org/wiki/Undefined_behavior" rel="noreferrer">未定義の動作のを導くことを言います。全く問題には関与して、「すべきである」が存在しない、ありません。関与の変数の一つがvolatile
宣言されたときに未定義の動作が変更された場合、それが証明するか、何も変わりません。これは、の未定義のです。あなたが行動について推論することはできません。
あなたの最も興味深い見えるたとえば、
と1u = (u++);
は、未定義の動作のテキストブックの例(シーケンスがのポイント上のWikipediaのエントリを参照)です。
他のヒント
あなたはそれはあなたが取得しているものを手に入れる方法を正確に知っているので、傾斜している場合だけで、コードのあなたの行をコンパイルし、逆アセンブルます。
これは、私は一緒に私が起こっていると思うものと、私のマシン上で得るものです。
$ cat evil.c
void evil(){
int i = 0;
i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
0x00000000 <+0>: push %ebp
0x00000001 <+1>: mov %esp,%ebp
0x00000003 <+3>: sub $0x10,%esp
0x00000006 <+6>: movl $0x0,-0x4(%ebp) // i = 0 i = 0
0x0000000d <+13>: addl $0x1,-0x4(%ebp) // i++ i = 1
0x00000011 <+17>: mov -0x4(%ebp),%eax // j = i i = 1 j = 1
0x00000014 <+20>: add %eax,%eax // j += j i = 1 j = 2
0x00000016 <+22>: add %eax,-0x4(%ebp) // i += j i = 3
0x00000019 <+25>: addl $0x1,-0x4(%ebp) // i++ i = 4
0x0000001d <+29>: leave
0x0000001e <+30>: ret
End of assembler dump.
(I ... 0x00000014の命令は、コンパイラの最適化のいくつかの種類だったと仮定?)
私はC99標準の関連部分は、§2
6.5式をしていると思いますオブジェクトが格納された値を持たなければならない前と次のシーケンス点との間 式の評価で最も一度に変更。さらに、前の値 格納されるべき値を決定するために読み取り専用されなければならない。
と6.5.16代入演算子、§4ます:
オペランドの評価順序は不定です。試みが変更になっている場合 または代入演算子の結果は、次のシーケンスポイントの後にそれにアクセスします 動作は未定義です。
この動作は両方を呼び出すため、実際には説明できません。 不特定の動作 そして 未定義の動作, したがって、このコードについて一般的な予測はできませんが、読んでいただければわかります。 オルブ・モーダルの のような仕事 ディープC そして 未指定および未定義 特定のコンパイラと環境を使用すると、非常に特殊な場合に適切な推測ができる場合がありますが、運用環境に近い場所ではそれを行わないでください。
それで次に進みます 不特定の動作, 、 で C99標準草案 セクション6.5
段落 3 言う(私の強調):
オペレーターとオペランドのグループ化は、syntax.74で示されます。74)後で指定されている場合を除き(&&、||、?:、およびコンマオペレーターの場合)、 部分式の評価順序と副作用が発生する順序はどちらも未指定です。
したがって、次のような行があるとします。
i = i++ + ++i;
かどうかはわかりません i++
または ++i
最初に評価されます。これは主にコンパイラに与えるためです 最適化のためのより良いオプション.
私たちも持っています 未定義の動作 プログラムが変数を変更しているため、ここでも同様です(i
, u
, 、など) の間に複数回 シーケンスポイント. 。ドラフト標準セクションより 6.5
段落 2(私の強調):
前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトはその保存値をせいぜい1回変更しなければならない 式の評価によって。さらに、 前の値は、保存する値を決定するためにのみ読み取らなければなりません.
次のコード例が未定義として引用されています。
i = ++i + 1;
a[i++] = i;
これらすべての例で、コードは同じシーケンス ポイント内でオブジェクトを複数回変更しようとしています。 ;
これらのそれぞれの場合:
i = i++ + ++i;
^ ^ ^
i = (i++);
^ ^
u = u++ + ++u;
^ ^ ^
u = (u++);
^ ^
v = v++ + ++v;
^ ^ ^
不特定の動作 で定義されています C99標準草案 セクション内 3.4.4
として:
不特定の値、またはこの国際基準が2つ以上の可能性を提供し、どの例でも選択されている要件を課さない他の行動の使用
そして 未定義の動作 セクションで定義されています 3.4.3
として:
動作は、この国際標準が要件を課していない、携帯や誤ったプログラムコンストラクトまたは誤ったデータの使用時に
そして次のように指摘しています。
考えられる未定義の動作は、状況を完全に無視して予期しない結果をもたらすものから、翻訳またはプログラムの実行中にその環境に特徴的な文書化された方法で動作する (診断メッセージの発行の有無にかかわらず)、翻訳または実行を終了する (診断メッセージの発行あり) まで多岐にわたります。診断メッセージの内容)。
ここでの回答のほとんどは C 標準から引用されており、これらの構成体の動作が未定義であることを強調しています。理解するために これらの構成要素の動作が未定義である理由, まず、C11 標準に照らしてこれらの用語を理解しましょう。
順序付け: (5.1.2.3)
任意の 2 つの評価が与えられた場合
A
そしてB
, 、 もしA
前に配列されますB
, 、次に実行A
~の実行に先立つものとするB
.
順序なし:
もし
A
前後に順序付けされていませんB
, 、 それからA
そしてB
順序付けられていません。
評価は次の 2 つのいずれかになります。
- 値の計算, 、式の結果を計算します。そして
- 副作用, 、オブジェクトの変更です。
シーケンスポイント:
式の評価間のシーケンス ポイントの存在
A
そしてB
すべてのことを意味します 値の計算 そして 副作用 と関連したA
すべての前に順序付けされます 値の計算 そして 副作用 と関連したB
.
ここで次のような表現について質問します。
int i = 1;
i = i++;
標準では次のように述べられています。
6.5 式:
スカラー オブジェクトの副作用が相対的に順序付けされていない場合 どちらか 同じスカラー オブジェクトに対する異なる副作用 または、同じスカラー オブジェクトの値を使用した値の計算、 動作は未定義です. [...]
したがって、同じオブジェクトに対する 2 つの副作用があるため、上記の式は UB を呼び出します。 i
相互に順序付けされていません。つまり、への割り当てによる副作用が順序付けされていないことを意味します。 i
副作用の前後に行われます。 ++
.
割り当てがインクリメントの前に行われるか後に行われるかに応じて、異なる結果が生成されます。これは次の場合です。 未定義の動作.
名前を変更しましょう i
割り当ての左側にある il
そして代入の右側(式内) i++
) なれ ir
, 、その場合、式は次のようになります
il = ir++ // Note that suffix l and r are used for the sake of clarity.
// Both il and ir represents the same object.
重要な点 Postfixについて ++
演算子は次のとおりです。
という理由だけで
++
変数の後に来るということは、増分が遅く起こるという意味ではありません. 。インクリメントはコンパイラが望むだけ早く実行できます。 コンパイラが元の値が使用されることを保証する限り.
という表現を意味します il = ir++
次のように評価できます
temp = ir; // i = 1
ir = ir + 1; // i = 2 side effect by ++ before assignment
il = temp; // i = 1 result is 1
または
temp = ir; // i = 1
il = temp; // i = 1 side effect by assignment before ++
ir = ir + 1; // i = 2 result is 2
2 つの異なる結果が得られます 1
そして 2
これは代入による副作用の順序によって異なります。 ++
したがってUBを呼び出します。
これに答えるもう 1 つの方法は、シーケンス ポイントや未定義の動作の難解な詳細に囚われるのではなく、単純に次のように尋ねることです。 それらは何を意味するのでしょうか? プログラマーは何をしようとしていたのでしょうか?
最初のフラグメントで尋ねられたのは、 i = i++ + ++i
, 、私の本では明らかに非常識です。実際のプログラムでこれを記述する人は誰もいないでしょう。それが何をするのかは明らかではありません。誰かがコードを作成しようとして、このような特定の人為的な一連の操作が生じるようなアルゴリズムは考えられません。そして、それが何をすべきかはあなたにも私にも明らかではないので、私の本では、コンパイラが何をすべきかを理解できなくても問題ありません。
2番目の断片、 i = i++
, 、もう少し分かりやすくなります。誰かが明らかに i をインクリメントし、その結果を i に代入しようとしています。ただし、C でこれを行う方法がいくつかあります。i に 1 を加算し、その結果を i に代入する最も基本的な方法は、ほとんどすべてのプログラミング言語で同じです。
i = i + 1
もちろん、C には便利なショートカットがあります。
i++
これは、「i に 1 を加算し、その結果を i に代入する」ことを意味します。したがって、次のようにして 2 つの寄せ集めを構築すると、
i = i++
私たちが実際に言いたいのは、「i に 1 を加算し、その結果を i に代入し、その結果を i に代入し直す」ということです。私たちは混乱しているので、コンパイラも混乱してもあまり気にしません。
現実的には、これらのクレイジーな式が書かれるのは、人々が ++ がどのように動作するかという人為的な例としてそれらを使用するときだけです。そしてもちろん、++ がどのように機能するかを理解することが重要です。ただし、++ を使用する場合の実際的なルールの 1 つは、「++ を使用した式の意味が明らかでない場合は、その式を記述しない」ということです。
私たちは以前、comp.lang.c でこのような表現について議論するのに数え切れないほどの時間を費やしていました。 なぜ それらは未定義です。その理由を実際に説明しようとする私の長い回答のうち 2 つは、ウェブ上にアーカイブされています。
こちらも参照 質問3.8 そして残りの質問は セクション3 の C よくある質問一覧.
多くの場合、この質問は、次のようなコードに関連する質問の重複としてリンクされています。
printf("%d %d\n", i, i++);
または
printf("%d %d\n", ++i, i++);
または同様の亜種。
これもそうですが、 未定義の動作 すでに述べたように、次の場合には微妙な違いがあります。 printf()
次のようなステートメントと比較するときに関係します。
x = i++ + i++;
次のステートメントでは次のようになります。
printf("%d %d\n", ++i, i++);
の 評価の順序 の引数の printf()
は 不特定. 。つまり、式 i++
そして ++i
任意の順序で評価できます。 C11規格 これについては関連する説明がいくつかあります。
付録 J、不特定の動作
関数指定子、引数、および引数内のサブエクスペレーションが関数呼び出し(6.5.2.2)で評価される順序。
3.4.4、不特定の動作
不特定の価値、またはこの国際基準が2つ以上の可能性を提供し、どの例でも選択された要件を課すことはありません。
例の不特定の動作の例は、関数の引数が評価される順序です。
の 不特定の動作 それ自体は問題ではありません。次の例を考えてみましょう。
printf("%d %d\n", ++x, y++);
これにもあります 不特定の動作 なぜなら評価の順番は ++x
そして y++
は不特定です。しかし、これは完全に合法的で有効な発言です。あるよ いいえ このステートメントでは未定義の動作が行われます。変更があるため (++x
そして y++
) が行われます 明確な オブジェクト。
次のステートメントを解釈するもの
printf("%d %d\n", ++i, i++);
として 未定義の動作 これら 2 つの式が 同じ 物体 i
介入なしで シーケンスポイント.
もう一つの詳細は、 コンマ printf() 呼び出しに関係するのは、 セパレーター, ではなく、 カンマ演算子.
これは重要な違いです。 カンマ演算子 を導入します シーケンスポイント オペランドの評価の間で次のことが有効になります。
int i = 5;
int j;
j = (++i, i++); // No undefined behaviour here because the comma operator
// introduces a sequence point between '++i' and 'i++'
printf("i=%d j=%d\n",i, j); // prints: i=7 j=6
カンマ演算子はオペランドを左から右に評価し、最後のオペランドの値のみを返します。それで j = (++i, i++);
, ++i
増分 i
に 6
そして i++
の古い値が得られます i
(6
) に割り当てられている j
. 。それから i
になる 7
ポストインクリメントのため。
したがって、 コンマ 関数呼び出しではカンマ演算子にする必要がありました。
printf("%d %d\n", ++i, i++);
問題ありません。しかし、それは呼び起こします 未定義の動作 なぜなら コンマ がここにあります セパレーター.
初めての方へ 未定義の動作 読むと役に立つだろう すべての C プログラマーが未定義の動作について知っておくべきこと C における未定義の動作の概念とその他の多くの変種を理解するため。
この郵便受け: 未定義、未仕様、実装定義の動作 も関連します。
、それは順序で「私は++」を実装するようにコンパイラのため、Cの標準の下で、法的になります:
In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value
私は、任意のプロセッサが1を簡単に、このような行動は、マルチスレッドコードを簡単に(例えば、それは2つのスレッドがしようとすることを保証するになるだろう状況を想像することができ、そのようなことは効率的に行うことを可能にするためのハードウェアをサポートとは思いませんが同時に、i
が2ずつ増加になるだろう上記のシーケンスを実行する)と、いくつかの将来のプロセッサは、そのような機能は何かを提供するかもしれないということは全く考えられないではありません。
i++
を書くとしたら、それは実現しなかった場合は、は次のいずれかということに気づき他の命令は、コンパイラがデッドロックとなる命令のシーケンスを生成することが可能(および法的)になり、i
にアクセスするために起こりました。確かに、コンパイラは、ほぼ確実に同じ変数i
は、両方の場所で使用されている場合に問題を検出するだろうが、日常的には二つのポインタのp
とq
への参照を受け入れ、むしろ上記の式(中(*p)
と(*q)
を使用している場合コンパイラが認識または同じオブジェクトのアドレスがi
とp
の両方のために渡された場合に生じるデッドロックを回避するために必要とされないであろう)を2回q
を使うよります。
C規格では、変数が2つのだけのシーケンスポイント間で最も一度割り当てられるべきであると述べています。例えばセミコロンは、配列点です。
だから、フォームのすべての文:
i = i++;
i = i++ + ++i;
というようにそのルールに違反します。標準はまた、動作は未定義と未指定ではないと言います。いくつかのコンパイラは、これらを検出し、何らかの結果を生み出すんが、これは、標準ごとではありません。
しかし、2つの異なる変数は、2つの配列点の間にインクリメントすることができる。
while(*src++ = *dst++);
/解析文字列をコピーしながら、上記の一般的なコーディング慣行である。
一方、 構文 のような表現のうち a = a++
または a++ + a++
合法です、 行動 これらの構成要素のうち、 未定義 なぜなら するだろう C 規格には準拠していません。 C99 6.5p2:
- 前のシーケンス ポイントと次のシーケンス ポイントの間で、オブジェクトの格納値は式の評価によって最大 1 回変更されます。[72] さらに、以前の値は、保存する値を決定するためにのみ読み取られるものとします [73]
と 脚注 73 それをさらに明確にする
この段落では、次のような未定義のステートメント式が表示されます。
i = ++i + 1; a[i++] = i;
許可しながら
i = i + 1; a[i] = i;
さまざまなシーケンス ポイントは、次の付録 C にリストされています。 C11 (そして C99):
5.1.2.3 で説明されているシーケンス ポイントは次のとおりです。
- 関数呼び出しにおける関数指定子と実際の引数の評価と実際の呼び出しの間。(6.5.2.2)。
- 次の演算子の 1 番目と 2 番目のオペランドの評価の間:論理 AND && (6.5.13);論理または|| (6.5.14);カンマ , (6.5.17)。
- 条件式 ? の最初のオペランドの評価間:演算子と 2 番目と 3 番目のオペランドのどちらかが評価されます (6.5.15)。
- 完全な宣言子の終わり:宣言子 (6.7.6);
- 完全な式の評価と次に評価される完全な式の間。以下は完全な表現です。複合リテラル (6.7.9) の一部ではない初期化子。式ステートメント内の式 (6.8.3)。選択ステートメントの制御式 (if または switch) (6.8.4)。while または do ステートメントの制御式 (6.8.5)。for ステートメントの各 (オプションの) 式 (6.8.5.3)。return ステートメント内の (オプションの) 式 (6.8.6.4)。
- ライブラリ関数がリターンする直前 (7.1.4)。
- 各書式設定された入出力関数変換指定子に関連付けられたアクションの後 (7.21.6、7.29.2)。
- 比較関数の各呼び出しの直前および直後、また、比較関数の呼び出しとその呼び出しに引数として渡されるオブジェクトの移動の間も同様です (7.22.5)。
同じ文言 C11の段落 は:
- スカラー オブジェクトの副作用が、同じスカラー オブジェクトの別の副作用、または同じスカラー オブジェクトの値を使用した値の計算に対して順序付けされていない場合、その動作は未定義です。式の部分式に許容される順序が複数ある場合、いずれかの順序でそのような順序付けされていない副作用が発生した場合の動作は未定義です。84)
たとえば、最新バージョンの GCC を使用すると、プログラム内のこのようなエラーを検出できます。 -Wall
そして -Werror
, そうすると、GCC はプログラムのコンパイルを完全に拒否します。以下は、gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005 の出力です。
% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function ‘main’:
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
i = i++ + ++i;
~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
i = (i++);
~~^~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
u = u++ + ++u;
~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
u = (u++);
~~^~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
v = v++ + ++v;
~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors
重要な部分は知っておくことです シーケンスポイントとは何ですか -- そして とは シーケンスポイントと何 そうではありません. 。たとえば、 カンマ演算子 はシーケンスポイントなので、
j = (i ++, ++ i);
明確に定義されており、増加します i
1 ずつ加算すると古い値が得られ、その値は破棄されます。次に、カンマ演算子で副作用を解決します。そして増加します i
1 ずつ増加し、結果の値が式の値になります。つまり、これは単に人為的な書き方です j = (i += 2)
これもまた「賢い」書き方です
i += 2;
j = i;
しかし ,
関数の引数リストには ない カンマ演算子であり、個別の引数の評価間にシーケンス ポイントはありません。代わりに、それらの評価は相互に順序付けされていません。したがって、関数呼び出し
int i = 0;
printf("%d %d\n", i++, ++i, i);
もっている 未定義の動作 なぜなら の評価間に順序点はありません。 i++
そして ++i
関数の引数で, 、および の値 i
したがって、両方によって 2 回変更されます。 i++
そして ++i
, 、前のシーケンス ポイントと次のシーケンス ポイントの間。
https://stackoverflow.com/questions/29505280/incrementing-array-index-で-C の誰かのような声明について質問します:
int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);
7を印刷... OP 6を印刷することを期待します。
++i
刻みは計算の残りの部分の前に全て完全に保証するものではありません。実際には、異なるコンパイラは、ここでは異なる結果が得られます。あなたが提供した例では、最初の2 ++i
実行は、その後、k[]
の値は、最後++i
を読んだ後、k[]
ます。
num = k[i+1]+k[i+2] + k[i+3];
i += 3
現代のコンパイラは非常によく、これを最適化します。実際には、あなたが最初に書いたコードよりも、おそらくより良い(それはあなたが望んでいた道を働いていたと仮定)。
あなたの質問はおそらく、「なぜこれらの構成要素が C で未定義の動作をするのか?」ということではありません。あなたの質問はおそらく、「なぜこのコードを(使用して) ++
) 期待した値が得られませんか?」というメッセージが表示され、誰かがあなたの質問を重複としてマークし、ここに送信しました。
これ 答えはその質問に答えようとします:コードではなぜ期待した答えが得られなかったのか、期待どおりに動作しない式を認識する (および回避する) 方法を学ぶにはどうすればよいでしょうか。
C の基本的な定義は聞いたことがあると思います。 ++
そして --
これまでの演算子とプレフィックスの形式 ++x
後置形式とは異なります x++
. 。しかし、これらの演算子は考えるのが難しいので、理解していることを確認するために、次のような小さなテスト プログラムを作成したかもしれません。
int x = 5;
printf("%d %d %d\n", x, ++x, x++);
しかし、驚いたことに、このプログラムは、 ない 理解に役立ちます -- 奇妙な、予期しない、説明できない出力が出力され、おそらく次のことを示唆しています。 ++
あなたが思っていたこととはまったく異なる、まったく異なることをします。
あるいは、次のようなわかりにくい表現を見ているかもしれません。
int x = 5;
x = x++ + ++x;
printf("%d\n", x);
おそらく誰かがそのコードをパズルとして与えてくれたのでしょう。このコードも、特に実行する場合には意味がありません。2 つの異なるコンパイラでコンパイルして実行すると、2 つの異なる答えが得られる可能性があります。どうしたの?どちらの答えが正しいでしょうか?(そして答えは、両方ともそうであるか、どちらもそうではないということです。)
もうお聞きのとおり、これらの表現はすべて 未定義, これは、C 言語が何を行うかについては保証しないことを意味します。これは奇妙で驚くべき結果です。おそらく、コンパイルして実行する限り、作成できるプログラムはどれも、明確に定義された固有の出力を生成すると考えていたからです。しかし、未定義の動作の場合はそうではありません。
式が未定義になるのはなぜですか?を含む表現ですか ++
そして --
常に未定義ですか?もちろん違います:これらは便利な演算子であり、適切に使用すれば、完全に明確に定義されます。
私たちが話している式の場合、式が未定義になるのは、一度に多くのことが行われている場合、物事がどのような順序で起こるかわからないが、順序が得られる結果にとって重要である場合です。
この回答で使用した 2 つの例に戻りましょう。私が書いたとき
printf("%d %d %d\n", x, ++x, x++);
質問は電話する前に printf
, 、コンパイラは次の値を計算しますか? x
最初に、または x++
, 、 または多分 ++x
?しかし、結局のところ 私たちは知りません. 。C には、関数の引数が左から右、右から左、またはその他の順序で評価されるという規則はありません。したがって、コンパイラが実行するかどうかはわかりません x
まず ++x
, 、 それから x++
, 、 または x++
それから ++x
それから x
, 、または他の順序。しかし、コンパイラーが使用する順序に応じて、明らかに異なる結果が出力されるため、順序は明らかに重要です。 printf
.
このクレイジーな表現はどうでしょうか?
x = x++ + ++x;
この式の問題は、x の値を変更する 3 つの異なる試みが含まれていることです。(1) x++
部分は x に 1 を加算し、新しい値を格納しようとします。 x
, 、の古い値を返します。 x
;(2) ++x
部分は x に 1 を加算し、新しい値を格納しようとします。 x
, 、の新しい値を返します。 x
;(3) x =
部分は、他の 2 つの合計を x に代入しようとします。試みられた 3 つの割り当てのうち、どれが「勝つ」のでしょうか?3 つの値のうち実際に割り当てられるのはどれですか x
?繰り返しになりますが、おそらく驚くべきことですが、C には教えられるルールがありません。
優先順位、結合性、または左から右への評価によって、物事がどのような順序で起こるかがわかると想像するかもしれませんが、そうではありません。信じられないかもしれませんが、私の言葉を信じてください。もう一度言います。優先順位と結合性は、C の式の評価順序のすべての側面を決定するわけではありません。特に、1 つの式内に次のようなものに新しい値を代入しようとする複数の異なる箇所がある場合、 x
, 、優先順位と結合性は、 ない それらの試行のどれが最初に行われたのか、最後に行われたのか、または何かを教えてください。
背景と導入はこれくらいにして、すべてのプログラムが明確に定義されていることを確認したい場合、どの式を作成でき、どの式を作成できないでしょうか?
これらの表現はすべて問題ありません。
y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;
これらの式はすべて未定義です。
x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);
最後の質問は、どの式が適切に定義されており、どの式が未定義であるかをどのようにして判断できるのかということです。
前に述べたように、未定義の式は、一度に多くの処理が行われ、物事がどのような順序で発生するか確信が持てず、順序が重要な場合に使用されます。
- 1 つの変数が 2 つ以上の異なる場所で変更 (割り当て) されている場合、どの変更が最初に行われるかをどうやって知ることができるでしょうか。
- ある場所で変更され、その値が別の場所で使用されている変数がある場合、その変数が古い値を使用しているか新しい値を使用しているかをどうやって知ることができるでしょうか。
#1 の例として、次の式で
x = x++ + ++x;
`x を変更しようとする試みは 3 回あります。
#2 の例として、次の式で
y = x + x++;
私たちは両方とも次の値を使用します x
, を変更してください。
それが答えです:作成する式では、各変数が最大 1 回変更されることを確認し、変数が変更された場合は、その変数の値を他の場所で使用しようとしないようにしてください。
この種の計算で何が起こるかについての詳しい説明は、ドキュメントに記載されています。 n1188 から ISO W14 サイト.
考え方を説明します。
この状況に適用される標準 ISO 9899 の主なルールは 6.5p2 です。
前のシーケンス ポイントと次のシーケンス ポイントの間で、オブジェクトの格納値は式の評価によって最大 1 回変更されます。さらに、以前の値は、保存する値を決定するためにのみ読み取られるものとします。
次のような式内のシーケンスポイント i=i++
前にいる i=
以降 i++
.
上で引用した論文では、プログラムは小さなボックスで形成されており、各ボックスには 2 つの連続するシーケンス ポイント間の命令が含まれていると理解できると説明されています。シーケンス ポイントは、規格の付録 C で定義されています。 i=i++
完全な式を区切る 2 つのシーケンス ポイントがあります。このような式は、次のエントリと構文的に同等です。 expression-statement
Backus-Naur 形式の文法 (文法は規格の付録 A に記載されています)。
したがって、ボックス内の指示の順序には明確な順序はありません。
i=i++
次のように解釈できます
tmp = i
i=i+1
i = tmp
またはとして
tmp = i
i = tmp
i=i+1
これらすべての形式はコードを解釈するためです i=i++
は有効ですが、どちらも異なる答えを生成するため、動作は未定義です。
したがって、シーケンス ポイントは、プログラムを構成する各ボックスの最初と最後でわかります [ボックスは C のアトミック単位です]。ボックス内では、命令の順序がすべての場合に定義されるわけではありません。この順序を変更すると、結果が変わる場合があります。
編集:
このような曖昧さを説明するための他の良い情報源は、次のエントリです。 よくある質問 サイト(こちらも公開) 本として)、つまり ここ そして ここ そして ここ .
その理由は、プログラムが未定義の動作を実行しているためです。C++98 標準に従ってシーケンス ポイントが必要ないため、問題は評価順序にあります (C++11 の用語によれば、操作の前後にシーケンスはありません)。
ただし、1 つのコンパイラに固執すると、関数呼び出しやポインターを追加しない限り、動作が永続的になり、動作がさらに複雑になります。
まず最初に GCC について説明します。使用する ヌウェン・ミンGW 15 GCC 7.1 では次のものが得られます。
#include<stdio.h> int main(int argc, char ** argv) { int i = 0; i = i++ + ++i; printf("%d\n", i); // 2 i = 1; i = (i++); printf("%d\n", i); //1 volatile int u = 0; u = u++ + ++u; printf("%d\n", u); // 2 u = 1; u = (u++); printf("%d\n", u); //1 register int v = 0; v = v++ + ++v; printf("%d\n", v); //2
}
GCC はどのように機能しますか?右側 (RHS) の左から右の順序で部分式を評価し、その値を左側 (LHS) に割り当てます。これはまさに Java と C# が動作し、標準を定義する方法です。(はい、Java と C# の同等のソフトウェアには動作が定義されています)。RHS ステートメント内の各部分式を左から右の順序で 1 つずつ評価します。各部分式について:++c (プリインクリメント) が最初に評価され、次に値 c が演算に使用され、次にポストインクリメント c++)。
によると GCC C++:オペレーター
GCC C ++では、オペレーターの優先順位は、個々のオペレーターが評価される順序を制御します
GCC が理解する定義済み動作 C++ の同等のコード:
#include<stdio.h>
int main(int argc, char ** argv)
{
int i = 0;
//i = i++ + ++i;
int r;
r=i;
i++;
++i;
r+=i;
i=r;
printf("%d\n", i); // 2
i = 1;
//i = (i++);
r=i;
i++;
i=r;
printf("%d\n", i); // 1
volatile int u = 0;
//u = u++ + ++u;
r=u;
u++;
++u;
r+=u;
u=r;
printf("%d\n", u); // 2
u = 1;
//u = (u++);
r=u;
u++;
u=r;
printf("%d\n", u); // 1
register int v = 0;
//v = v++ + ++v;
r=v;
v++;
++v;
r+=v;
v=r;
printf("%d\n", v); //2
}
それでは、次へ行きます ビジュアルスタジオ. 。Visual Studio 2015 では、次のものが得られます。
#include<stdio.h>
int main(int argc, char ** argv)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 3
i = 1;
i = (i++);
printf("%d\n", i); // 2
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 3
u = 1;
u = (u++);
printf("%d\n", u); // 2
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); // 3
}
Visual Studio はどのように機能しますか。別のアプローチが取られます。最初のパスですべての事前インクリメント式を評価し、2 番目のパスで演算に変数値を使用し、3 番目のパスで RHS から LHS に代入し、最後のパスですべての値を評価します。ポストインクリメント式を 1 つのパスで実行します。
したがって、Visual C++ が理解する、定義された動作 C++ と同等のものは次のとおりです。
#include<stdio.h>
int main(int argc, char ** argv)
{
int r;
int i = 0;
//i = i++ + ++i;
++i;
r = i + i;
i = r;
i++;
printf("%d\n", i); // 3
i = 1;
//i = (i++);
r = i;
i = r;
i++;
printf("%d\n", i); // 2
volatile int u = 0;
//u = u++ + ++u;
++u;
r = u + u;
u = r;
u++;
printf("%d\n", u); // 3
u = 1;
//u = (u++);
r = u;
u = r;
u++;
printf("%d\n", u); // 2
register int v = 0;
//v = v++ + ++v;
++v;
r = v + v;
v = r;
v++;
printf("%d\n", v); // 3
}
Visual Studio のドキュメントに記載されているように、 評価の優先順位と順序:
複数の演算子が一緒に出現する場合、それらの優先順位は同等であり、結合性に従って評価されます。表内の演算子については、「後置演算子」で始まるセクションで説明します。