web-dev-qa-db-ja.com

これらのコンストラクトがプリおよびポストインクリメントの未定義の動作を使用しているのはなぜですか?

#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 %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   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?
}
760
PiX

Cには未定義の動作の概念があります。つまり、一部の言語構造は構文的に有効ですが、コードの実行時の動作を予測することはできません。

私の知る限り、標準は未定義の動作の概念が存在する理由を明示的にwhyと言っていません。私の考えでは、言語設計者は、すべての実装が整数オーバーフローをまったく同じ方法で処理することを要求するのではなく、セマンティクスにある程度の余裕があることを望んだからです。未定義。整数オーバーフローを引き起こすコードを記述した場合、何でも起こります。

それで、それを念頭に置いて、なぜこれらの「問題」がありますか?この言語は、特定のことが 未定義の動作 につながると明確に述べています。問題はありません。「すべき」ではありません。関係する変数の1つがvolatileとして宣言されたときに未定義の動作が変更されても、それは何も証明も変更もしません。 undefined;あなたは行動について推論することはできません。

最も興味深い見た目の例、

u = (u++);

未定義の振る舞いの教科書の例です(ウィキペディアの シーケンスポイント に関するエントリを参照)。

544
unwind

コードの行をコンパイルして逆アセンブルします。取得するものがどれだけ正確かを知りたい場合は。

これは私がマシン上で何をしているのかと一緒に起こっていると思うことです:

$ 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.

(私は... 0x00000014命令が何らかのコンパイラ最適化であったと仮定しますか?)

76
badp

C99標準の関連部分は6.5式、§2であると思います

前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトは、式の評価によって、格納された値を最大で1回変更しなければなりません。さらに、保存する値を決定するためにのみ、以前の値を読み取ります。

および6.5.16代入演算子、§4:

オペランドの評価の順序は指定されていません。代入演算子の結果を変更したり、次のシーケンスポイントの後に結果にアクセスしようとした場合、動作は未定義です。

59
Christoph

ここでの回答のほとんどは、これらの構造の動作が未定義であることを強調するC標準から引用されています。 これらの構造の動作が未定義である理由を理解するために、C11標準に照らしてこれらの用語を最初に理解しましょう。

シーケンス:(5.1.2.3)

ABの2つの評価が与えられた場合、ABの前にシーケンスされている場合、Aの実行はBの実行に先行します。

シーケンスなし:

ABの前後にシーケンスされていない場合、ABはシーケンスされません。

評価は次の2つのいずれかになります。

  • 値の計算、式の結果を計算します。そして
  • 副作用、これはオブジェクトの修正です。

シーケンスポイント:

ABの評価の間にシーケンスポイントが存在するということは、Aに関連付けられているすべての value compute および side effect が前にシーケンスされることを意味しますBに関連付けられているすべての値の計算および副作用

今、質問に来て、のような表現のために

int i = 1;
i = i++;

標準は言う:

6.5式:

スカラーオブジェクトに対する副作用が次と比較して順序付けられていない場合いずれか同じスカラーオブジェクトに対する異なる副作用または同じスカラーオブジェクトの値を使用した値の計算、 動作は未定義。 [...]

したがって、上記の式はUBを呼び出します。これは、同じオブジェクトiに対する2つの副作用が互いに対して順序付けられていないためです。つまり、iへの代入による副作用が、++による副作用の前に行われるか、後に行われるかは順序付けされていません。
割り当てがインクリメントの前または後に発生するかどうかに応じて、異なる結果が生成されます。これは未定義の動作の場合の1つです。

代入の左側の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つの異なる結果12が発生します。これは、割り当てと++による副作用のシーケンスに依存するため、UBを呼び出します。

49
haccks

nspecified behaviorndefined behavior の両方を呼び出すため、動作を実際に説明することはできません。したがって、このコードについて一般的な予測を行うことはできません。 Olve Maudal'sDeep C および nspecified and Undefined などの動作特定のコンパイラと環境で非常に特定のケースで適切な推測ができる場合がありますが、しないでください生産の近くでそれを行います。

未指定の動作ドラフトc99標準 section6.5パラグラフ says(emphasis mine):

演算子とオペランドのグループ化は構文で示されます74)後で指定する場合を除き(関数呼び出し()、&&、||、?:、およびコンマ演算子の場合)、評価の順序副次式と副作用が発生する順序はどちらも指定されていません。

したがって、次のような行がある場合:

i = i++ + ++i;

i++または++iが最初に評価されるかどうかはわかりません。これは主に、コンパイラに 最適化のためのより良いオプション を与えるためです。

また、プログラムは シーケンスポイント の間に変数(iuなど)を複数回変更しているため、ここでも未定義の動作があります。ドラフト標準セクション6.5段落から2emphasis mine):

前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトは、式の評価によって、格納された値を最大で1回変更します。さらに、保存する値を決定するために、以前の値のみを読み取ります

次のコード例を未定義として引用しています。

i = ++i + 1;
a[i++] = i; 

これらのすべての例で、コードは同じシーケンスポイントでオブジェクトを複数回変更しようとしています。これらの各ケースでは;で終わります。

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

指定されていない動作は、セクション3.4.4ドラフトc99標準 で次のように定義されています。

指定されていない値の使用、またはこの国際標準が2つ以上の可能性を提供し、いずれの場合でも選択される追加の要件を課さない他の動作

未定義の動作は、セクション3.4.3で次のように定義されています。

移植性のない、またはエラーのあるプログラム構成、またはエラーのあるデータを使用した場合の動作。この国際規格は要件を課していない

そして、次のことに注意してください:

予測できない結果を伴う状況を完全に無視することから、環境に特有の文書化された方法での診断またはプログラム実行中の動作(診断メッセージの発行の有無にかかわらず)、翻訳または実行の終了(発行を伴う)診断メッセージの)。

48
Shafik Yaghmour

これに答える別の方法は、シーケンスポイントと未定義の動作の不可解な詳細に行き詰まるのではなく、単に尋ねることです、どういう意味ですか?プログラマーは何をしようとしていましたか?

質問された最初のフラグメント、i = i++ + ++iは、私の本では非常にはっきりと狂っています。誰も実際のプログラムでそれを書くことはありません、それが何をするのかは明らかではありません、誰かがこの特定の不自然な操作シーケンスをもたらすコードを試みた可能性のあるアルゴリズムはありません。そして、あなたと私にはそれが何をすべきかは明らかではないので、コンパイラがそれが何をすべきかを理解できない場合でも、私の本では問題ありません。

2番目のフラグメントi = i++は、少しわかりやすいです。誰かが明らかにiをインクリメントし、結果をiに割り当てようとしています。しかし、Cでこれを行うにはいくつかの方法があります。1をiに追加し、結果をiに戻す最も基本的な方法は、ほとんどすべてのプログラミング言語で同じです。

i = i + 1

Cにはもちろん便利なショートカットがあります:

i++

これは、「iに1を追加し、結果をiに戻す」ことを意味します。したがって、次のように記述することで、2つの寄せ集めを構築する場合、

i = i++

私たちが本当に言っているのは、「iに1を追加し、結果をiに戻し、結果をiに戻す」ということです。私たちは混乱しているので、コンパイラも混乱してもそれほど気にしません。

現実的には、これらのクレイジーな式が書かれるのは、++がどのように機能するかの人工的な例として人々が使用しているときだけです。そしてもちろん、++の仕組みを理解することは重要です。しかし、++を使用するための1つの実用的なルールは、「++を使用する式の意味が明らかでない場合は、記述しないでください」です。

Comp.lang.cでこれらのような式とwhyが定義されていないことを議論するために無数の時間を費やしていました。理由を本当に説明しようとする2つの長い回答は、Webにアーカイブされています。

31
Steve Summit

多くの場合、この質問は、次のようなコードに関連する質問の複製としてリンクされています

printf("%d %d\n", i, i++);

または

printf("%d %d\n", ++i, i++);

または類似のバリアント。

これも ndefined behaviour ですが、次のようなステートメントと比較するときにprintf()が関係する場合、微妙な違いがあります。

x = i++ + i++;

次の文では:

printf("%d %d\n", ++i, i++);

printf()の引数の 評価の順序指定なし です。つまり、式i++および++iは任意の順序で評価できます。 C11標準 には、これに関するいくつかの関連する説明があります。

Annex J、不特定の動作

関数指定子、引数、および引数内の部分式が関数呼び出しで評価される順序(6.5.2.2)。

3.4.4、未指定の動作

指定されていない値の使用、またはこの国際規格が2つ以上の可能性を提供し、いずれの場合でも選択される追加の要件を課さない他の動作。

例指定されていない動作の例は、関数の引数が評価される順序です。

nspecified behaviour自体は問題ではありません。この例を考えてみましょう:

printf("%d %d\n", ++x, y++);

++xy++の評価の順序が指定されていないため、これもnspecified behaviourを持っています。しかし、それは完全に合法かつ有効な声明です。このステートメントにはno未定義の動作があります。変更(++xおよびy++)はdistinctオブジェクトに対して行われるためです。

次のステートメントをレンダリングするもの

printf("%d %d\n", ++i, i++);

as ndefined behaviourは、これら2つの式がsame object isequence point を介在させずに変更するという事実です。


もう1つの詳細は、printf()呼び出しに関係するcommacomma operator ではなくseparatorであるということです。

コンマ演算子はオペランドの評価の間にシーケンスポイントを導入するため、これは重要な違いです。

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++);では、++ii6にインクリメントし、i++iの古い値(6)をjに割り当てます。その後、iは、ポストインクリメントにより7になります。

そのため、関数呼び出しのcommaがコンマ演算子になる場合

printf("%d %d\n", ++i, i++);

問題になりません。ただし、commaseparatorであるため、ndefined behaviourを呼び出します。


ndefined behaviourに慣れていない人にとっては、Cの未定義動作の概念や他の多くのバリエーションを理解するために すべてのCプログラマが未定義動作について知っておくべきこと .

この投稿: 未定義、未指定、実装定義の動作 も関連しています。

22
P.P.

コンパイラやプロセッサが実際にそうする可能性は低いですが、C標準では、コンパイラがシーケンスで「i ++」を実装することは合法です。

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

プロセッサがハードウェアをサポートしてそのようなことを効率的に行えるとは思いませんが、そのような動作がマルチスレッドコードを簡単にする状況を簡単に想像できます(たとえば、2つのスレッドが上記を実行しようとすると、同時にシーケンスすると、iは2ずつ増加します)、将来のプロセッサがそのような機能を提供する可能性はまったく考えられません。

コンパイラが上記のi++を記述し(標準では合法)、式全体の評価全体にわたって上記の命令を散在させる(また正当な)場合、および他の命令のいずれかに気付かなかった場合たまたまiにアクセスした場合、コンパイラがデッドロックする一連の命令を生成する可能性があります。確かに、コンパイラは、同じ変数iが両方の場所で使用される場合に問題をほぼ確実に検出しますが、ルーチンが2つのポインターpおよびqへの参照を受け入れ、上記の式で(*p)および(*q)を使用する場合(むしろiを2回使用するよりも)コンパイラは、pqの両方に同じオブジェクトのアドレスが渡された場合に発生するデッドロックを認識または回避する必要はありません。

22
supercat

C標準では、変数は2つのシーケンスポイント間で最大1回しか割り当てられないと規定されています。たとえば、セミコロンはシーケンスポイントです。
したがって、フォームのすべてのステートメント:

i = i++;
i = i++ + ++i;

などはそのルールに違反しています。標準では、動作は未定義であり、指定されていないということでもあります。一部のコンパイラはこれらを検出して結果を生成しますが、これは標準に準拠していません。

ただし、2つの異なる変数は、2つのシーケンスポイント間でインクリメントできます。

while(*src++ = *dst++);

上記は、文字列をコピー/分析する際の一般的なコーディング方法です。

14
Nikhil Vidhani

a = a++a++ + a++などの式のsyntaxは有効ですが、これらの構成体のbehaviourndefinedです。これはshall C標準では従わない。 C99 6.5p2

  1. 前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトは、式の評価によって、格納された値を最大で1回変更しなければなりません。 [72]さらに、保存する値を決定するために、事前の値のみを読み取らなければならない[73]

脚注7 でさらに明確に

  1. この段落は、次のような未定義のステートメント式をレンダリングします。

    i = ++i + 1;
    a[i++] = i;
    

    許可しながら

    i = i + 1;
    a[i] = i;
    

さまざまなシーケンスポイントは、 C11 (および C99 )の付録Cにリストされています。

  1. 以下は、5.1.2.3で説明されているシーケンスポイントです。

    • 関数呼び出しと実際の呼び出しにおける関数指定子と実際の引数の評価の間。 (6.5.2.2)。
    • 次の演算子の第1オペランドと第2オペランドの評価の間:論理AND &&(6.5.13);論理OR || (6.5.14);コンマ(6.5.17)。
    • 条件付き?の第1オペランドの評価の間:演算子と、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の段落 の文言は:

  1. スカラーオブジェクトの副作用が、同じスカラーオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用した値の計算のいずれかに対して順序付けられていない場合、動作は未定義です。式の部分式の許容される順序が複数ある場合、そのような順序付けられていない副作用が順序付けのいずれかで発生する場合、動作は未定義です84)。

たとえば、-Wallおよび-Werrorを含む最新バージョンのGCCを使用することにより、プログラムでこのようなエラーを検出できます。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

重要な部分は、 シーケンスポイントとは何か-とwhatとはシーケンスポイントとは何ですかis n't を知ることです=。たとえば、カンマ演算子はシーケンスポイントなので、

j = (i ++, ++ i);

明確に定義されており、iを1つ増やし、古い値を生成し、その値を破棄します。次に、カンマ演算子で副作用を解決します。 iを1つインクリメントすると、結果の値が式の値になります-つまり、これはj = (i += 2)を書くための単なる工夫された方法であり、これもまた「賢い」書き方です

i += 2;
j = i;

ただし、関数引数リストの,notコンマ演算子であり、異なる引数の評価の間にシーケンスポイントはありません。代わりに、それらの評価は相互に関して順序付けられていません。そのため、関数呼び出し

int i = 0;
printf("%d %d\n", i++, ++i, i);

関数の引数にi++++iの評価の間にシーケンスポイントがないであるため、undefined behaviourがあり、したがってiの値はi++によって2回変更されますおよび++i、前のシーケンスポイントと次のシーケンスポイントの間。

13
Antti Haapala

https://stackoverflow.com/questions/29505280/incrementing-array-index-in-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[]の値が読み取られ、最後の++ik[]の順に読み取られました。

num = k[i+1]+k[i+2] + k[i+3];
i += 3

最新のコンパイラはこれを非常にうまく最適化します。実際、おそらくあなたが最初に書いたコードよりも良いかもしれません(あなたが望んでいた方法で動作したと仮定して)。

9
TomOnTime

あなたの質問は、おそらく「これらの構成体がCで定義されていない動作なのはなぜですか?」ではなかったでしょう。あなたの質問はおそらく「このコード(++を使用)が期待した値を与えなかったのはなぜですか?」であり、誰かがあなたの質問を重複としてマークし、ここに送りました。

This answerはその質問に答えようとします:なぜあなたのコードはあなたに期待した答えを与えなかったのか、そしてどうやって期待通りに動かない表現を認識する(そして避ける)ことを学ぶことができますか?.

Cの++および--演算子の基本的な定義を聞いたことがあると思います。また、接頭辞形式++xが接尾辞形式x++とどのように異なるかを聞いたと思います。しかし、これらの演算子は考えるのが難しいので、理解してもらうために、おそらく次のような小さなテストプログラムを書いたでしょう

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

しかし、驚いたことに、このプログラムはnot理解に役立ちました-奇妙で予想外の不可解な出力を出力し、++があなたが思っていたものとはまったく異なる何かをするかもしれないことを示唆しました。

または、おそらくあなたは次のようなわかりにくい表現を見ています

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

おそらく誰かがそのコードをパズルとしてくれたのでしょう。このコードは、特に実行する場合には意味がありません。2つの異なるコンパイラーでコンパイルして実行すると、2つの異なる回答が得られる可能性があります。どうしたの?どの答えが正しいですか? (そして、答えは、両方ともそうであるか、どちらもそうではないということです。)

今まで聞いたことがあるように、これらの式はすべてndefinedです。つまり、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、precedence、結合性のようなものに新しい値を割り当てようとするさまざまなスポットdo notこれらの試みのどれが最初に起こるか、最後に起こるか、または何かを教えてくれます。


それで、すべてのプログラムの背景と導入を邪魔にせずに、すべてのプログラムが明確に定義されていること、どの式を書くことができ、どの式を書けないことを確認したいのですか?

これらの式はすべて問題ありません。

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つある場合、どの変更が最初に発生するかをどのように知ることができますか?
  2. ある場所で変更されている変数があり、その値が別の場所で使用されている場合、その変数が古い値を使用するか新しい値を使用するかをどのようにして知ることができますか?

#1の例として、式

x = x++ + ++x;

`xを変更する試みは3回あります。

#2の例として、式で

y = x + x++;

xの値を使用し、変更します。

そのため、答えは次のとおりです。作成する式では、各変数が1回しか変更されないようにし、変数が変更されても、その変数の値を他の場所で使用しないでください。

5
Steve Summit

この種の計算で何が起こるかについての良い説明は、 ISO W14サイト のドキュメント n1188 で提供されています。

アイデアを説明します。

この状況に適用される標準ISO 9899の主なルールは6.5p2です。

前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトは、式の評価によって、格納された値を最大で1回変更しなければなりません。さらに、保存する値を決定するためにのみ、以前の値を読み取ります。

i=i++のような式のシーケンスポイントは、i=の前とi++の後です。

上で引用した論文では、2つの連続したシーケンスポイント間の命令を含む各ボックスに小さなボックスが形成されているとしてプログラムを把握できることが説明されています。シーケンスポイントは標準の付録Cで定義されています。i=i++の場合、完全な式を区切る2つのシーケンスポイントがあります。このような式は、文法のBackus-Naur形式のexpression-statementのエントリと構文的に同等です(文法は標準の付録Aで提供されています)。

したがって、ボックス内の指示の順序には明確な順序はありません。

i=i++

として解釈することができます

tmp = i
i=i+1
i = tmp

またはとして

tmp = i
i = tmp
i=i+1

コードi=i++を解釈するためのこれらのフォームは両方とも有効であり、両方とも異なる回答を生成するため、動作は未定義です。

したがって、シーケンスポイントは、プログラムを構成する各ボックスの開始点と終了点で見ることができ(ボックスはCのアトミック単位です)、ボックス内では命令の順序はすべての場合に定義されません。その順序を変更すると、結果が変わることがあります。

編集:

このようなあいまいさを説明するための他の優れた情報源は、 c-faq サイトからのエントリ(また、本として公開された)、つまり here and here and here

4
alinsoar

その理由は、プログラムが未定義の動作を実行しているからです。問題は評価順序にあります。これは、C++ 98標準に従ってシーケンスポイントが必要ないためです(C++ 11の用語に従って、操作の前後に順序付けは行われません)。

ただし、1つのコンパイラーに固執する場合、関数呼び出しまたはポインターを追加しない限り、動作が永続的であることがわかります。

  • 最初にGCC: Nuwen MinGW 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 に進みます。 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に割り当て、最後のパスですべてを評価しますワンパスでポストインクリメント式。

したがって、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のドキュメントにあるように 評価の優先順位と順序

複数の演算子が一緒に表示される場合、それらは同じ優先順位を持ち、それらの結合性に従って評価されます。表内の演算子については、Postfix演算子で始まるセクションで説明します。

3