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

833
PiX 2009-06-04 23:17.
#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?
}

14 answers

573
unwind 2009-06-04 23:20.

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

私の知る限り、この規格では、未定義の振る舞いの概念が存在する理由を明確に述べていません。私の考えでは、言語設計者がセマンティクスにある程度の余裕があることを望んでいたからです。つまり、すべての実装が整数オーバーフローをまったく同じ方法で処理する必要があり、深刻なパフォーマンスコストがかかる可能性が非常に高いため、動作をそのままにしました。未定義なので、整数オーバーフローを引き起こすコードを記述した場合、何が起こる可能性もあります。

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

あなたの最も興味深く見える例、

u = (u++);

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

76
badp 2010-05-25 03:26.

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

これは、私が自分のマシンで行っていることと、私が考えていることです。

$ 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命令はある種のコンパイラ最適化だったと思いますか?)

66
Christoph 2009-06-04 23:35.

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

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

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

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

61
haccks 2015-06-27 14:27.

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

シーケンス:(5.1.2.3)

任意の二つの評価を与えられたABすれば、A前に配列決定されB、その後の実行はAの実行に先行するものB

シーケンスなし:

A前または後Bにシーケンスされていない場合、AおよびBはシーケンスされていません。

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

  • 式の結果を計算する値の計算。そして
  • オブジェクトの変更である副作用

シーケンスポイント:

式の評価の間の配列点の存在AとはB意味し、そのすべての値の計算副作用に関連付けられているAすべての前に配列決定された値の計算副作用と関連しますB

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

int i = 1;
i = i++;

標準によると:

6.5式:

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

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

i割り当ての左側をbeにil、割り当ての右側(式内i++)をbeirに名前変更すると、式は次のようになります。

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  

二つの異なる結果が得られる1と、2その割当により副作用の配列に依存し++、したがってUBを呼び出します。

54
Shafik Yaghmour 2013-08-16 09:25.

それは両方の呼び出しので、行動は本当に説明できない、不特定の行動や未定義の動作を、我々はこのコードについての一般的な予測を立てることができないので、あなたが読んでいる場合が、OLVE Maudalのような仕事ディープCおよび未指定と未定義を、時にはあなたが良いを作ることができます特定のコンパイラと環境で非常に特定の場合に推測しますが、本番環境の近くでは行わないでください。

したがって、不特定の動作に移ると、ドラフトc99標準セクションの6.5パラグラフ3で次のように述べられています(強調鉱山):

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

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

i = i++ + ++i;

私たちは、かどうかわからないi++か、++i最初に評価されます。これは主に、コンパイラに最適化のためのより良いオプションを提供するためです。

我々はまた、持っている未定義の動作をプログラム変数を変更する(しているので、ここでも同様にiu間に複数回、等。)シーケンスポイント。ドラフト標準セクション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ように:

この国際規格が要件を課していない、移植不可能または誤ったプログラム構成または誤ったデータの使用時の動作

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

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

38
Steve Summit 2015-06-19 01:55.

これに答える別の方法は、シーケンスポイントの不可解な詳細や未定義の動作にとらわれるのではなく、単に質問することです。それら何を意味するのでしょうか。プログラマーは何をしようとしていましたか?

について尋ねられた最初の断片は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つは、Webにアーカイブされています。

  • なぜ標準はこれらが何をするかを定義しないのですか?
  • 演算子の優先順位によって評価の順序が決まりませんか?

質問3.8およびCFAQリストのセクション3の残りの質問も参照してください。

27
P.P 2015-12-31 10:26.

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

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++);

これは、あまりにもあり、不特定の行動を評価する順序ため++xy++規定されていません。しかし、それは完全に合法で有効な声明です。このステートメントに未定義の動作はありません。変更(++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する6i++歩留まりの古い値i6に割り当てられています)j。その後、ポストインクリメントによるものにiなり7ます。

その場合はカンマ関数呼び出しでは、カンマ演算子であることをしました

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

問題にはなりません。ただし、ここのコンマ区切り文字であるため、未定義の動作が発生します


未定義の振る舞い不慣れな人は、すべてのCプログラマーが未定義の振る舞いについて知っておくべきことを読んで、Cの未定義の振る舞いの概念や他の多くの変形を理解することをお勧めします。

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

23
supercat 2012-12-06 08:30.

コンパイラやプロセッサが実際にそうする可能性は低いですが、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つのスレッドが上記を実行しようとすると保証されます)シーケンスを同時に実行すると、i2つインクリメントされます)。将来のプロセッサがそのような機能を提供する可能性があることはまったく考えられません。

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

18
Antti Haapala 2017-03-27 04:58.

またはのような式の構文は有効ですが、C標準のshallに従わないため、これらの構造の動作定義されていません。C99 6.5p2:a = a++a++ + a++

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

脚注73がさらにあることを明確に

  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オペランドの評価の間:論理積&&(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)。

あなたは、例えばとGCCの最近のバージョンを使用して、プログラムの中で、このようなエラーを検出することができます-Wallし、-Werrorして、GCCが完全にあなたのプログラムをコンパイルすることを拒否します。以下は、gcc(Ubuntu 6.2.0-5ubuntu12)6.2.020161005の出力です。

% 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);

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

i += 2;
j = i;

ただし、,関数の引数リストにはないコンマ演算子、および別個の引数の評価の間にはシーケンスポイントは存在しません。代わりに、それらの評価は相互に順序付けられていません。したがって、関数呼び出し

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

関数の引数の評価関数引数の間にシーケンスポイントがないため、の動作未定義です。したがって、の値は、前のシーケンスポイントと次のシーケンスポイントの間で、との両方によって2回変更されます。i++++iii++++i

14
Nikhil Vidhani 2014-09-12 02:36.

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

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

などがそのルールに違反します。規格はまた、振る舞いは未定義であり、不特定ではないと述べています。一部のコンパイラはこれらを検出して結果を生成しますが、これは標準ではありません。

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

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

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

11
TomOnTime 2015-04-08 17:20.

に 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[]の最後そして、読んだ++i後、k[]

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

最新のコンパイラはこれを非常にうまく最適化します。実際、おそらく最初に書いたコードよりも優れています(期待どおりに機能したと仮定します)。

6
Steve Summit 2018-08-17 01:54.

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

#1の例として、式で

x = x++ + ++x;

`xを変更する3つの試みがあります。

#2の例として、式で

y = x + x++;

両方ともの値を使用し、xそれを変更します。

これが答えです。作成する式で、各変数が最大で1回変更されることを確認してください。変数が変更された場合は、その変数の値を別の場所で使用しようとしないでください。

5
alinsoar 2017-10-14 03:58.

計算のこの種では何が起こるかについての良い説明は、文書で提供されn1188からISO W14サイト。

アイデアを説明します。

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

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

以下のような式でシーケンスポイントがi=i++前にあるi=とした後i++

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

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

i=i++

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

tmp = i
i=i+1
i = tmp

またはとして

tmp = i
i = tmp
i=i+1

コードを解釈するためのこれらすべての形式i=i++が有効であり、両方が異なる回答を生成するため、動作は未定義です。

したがって、シーケンスポイントは、プログラムを構成する各ボックスの最初と最後で確認できます[ボックスはCの原子単位です]。ボックス内では、すべての場合に命令の順序が定義されているわけではありません。その順序を変更すると、結果が変わる場合があります。

編集:

そのような曖昧さを説明するための他の良い情報源は、c-faqサイト(本としても出版されている)からのエントリー、すなわちこことこことここです。

3
Mohamed El-Nakib 2017-06-11 12:56.

その理由は、プログラムが未定義の動作を実行しているためです。問題は、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演算子で始まるセクションで説明しています。

Related questions

MORE COOL STUFF

Reba McEntire は、彼女が息子の Shelby Blackstock と共有する「楽しい」クリスマスの伝統を明らかにしました:「私たちはたくさん笑います」

Reba McEntire は、彼女が息子の Shelby Blackstock と共有する「楽しい」クリスマスの伝統を明らかにしました:「私たちはたくさん笑います」

Reba McEntire が息子の Shelby Blackstock と共有しているクリスマスの伝統について学びましょう。

メーガン・マークルは、自然な髪のスタイリングをめぐってマライア・キャリーと結ばれました

メーガン・マークルは、自然な髪のスタイリングをめぐってマライア・キャリーと結ばれました

メーガン・マークルとマライア・キャリーが自然な髪の上でどのように結合したかについて、メーガンの「アーキタイプ」ポッドキャストのエピソードで学びましょう.

ハリー王子は家族との関係を修復できるという「希望を持っている」:「彼は父親と兄弟を愛している」

ハリー王子は家族との関係を修復できるという「希望を持っている」:「彼は父親と兄弟を愛している」

ハリー王子が家族、特にチャールズ王とウィリアム王子との関係について望んでいると主張したある情報源を発見してください。

ワイノナ・ジャッドは、パニックに陥った休暇の瞬間に、彼女がジャッド家の家長であることを認識しました

ワイノナ・ジャッドは、パニックに陥った休暇の瞬間に、彼女がジャッド家の家長であることを認識しました

ワイノナ・ジャッドが、母親のナオミ・ジャッドが亡くなってから初めての感謝祭のお祝いを主催しているときに、彼女が今では家長であることをどのように認識したかを学びましょう.

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セント ヘレナ島のジェイコブズ ラダーは 699 段の真っ直ぐ上る階段で、頂上に到達すると証明書が発行されるほどの難易度です。

The Secrets of Airline Travel Quiz

The Secrets of Airline Travel Quiz

Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?

Where in the World Are You? Take our GeoGuesser Quiz

Where in the World Are You? Take our GeoGuesser Quiz

The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!

バイオニック読書はあなたをより速く読むことができますか?

バイオニック読書はあなたをより速く読むことができますか?

BionicReadingアプリの人気が爆発的に高まっています。しかし、それは本当にあなたを速読術にすることができますか?

オーケーグッド770HPランボルギーニセンテナリオは十分に正気ではない

オーケーグッド770HPランボルギーニセンテナリオは十分に正気ではない

ランボルギーニの創設者であるフェルッチオランボルギーニが100歳になるのは毎日ではありません(そうです、彼は死んでいて、まだ死んでいると思います。

彼らが買った1台の車からAppleの車の計画について私たちが推測できること

彼らが買った1台の車からAppleの車の計画について私たちが推測できること

Appleが自動車分野に参入するという噂はかなり前から渦巻いており、AppleウォッチャーがSixtyEight Researchという会社がAppleの自動車研究開発のシェル会社である可能性が高いと判断したとき、その渦巻きは本当に渦巻いた。また、会社が購入した車は1台だけであることが知られており、その車はAppleが何を考えているかについての手がかりでいっぱいになる可能性があることも伝えています。

天文学者は太陽系の9番目の惑星の新しい証拠を見つけます

天文学者は太陽系の9番目の惑星の新しい証拠を見つけます

太陽系の外側にある架空の大きな物体である惑星Xの探索は、何十年にもわたって人間を魅了してきました。その検索の最新の章は、地球の10倍の大きさで、公転周期が15であるほど遠くにある惑星を指しています。

キャムニュートン、ゴッドダム

キャムニュートン、ゴッドダム

カムニュートンは昨日、簡単な265ヤードと3回のタッチダウンでファルコンズを引き裂き、別の素晴らしいゲームをしました。その日のハイライトは、上のタッチダウンスローでした。これは、視聴するたびにばかげているだけです。

米国のフィギュア スケートは、チーム イベントでの最終決定の欠如に「苛立ち」、公正な裁定を求める

米国のフィギュア スケートは、チーム イベントでの最終決定の欠如に「苛立ち」、公正な裁定を求める

ロシアのフィギュアスケーター、カミラ・バリエバが関与したドーピング事件が整理されているため、チームは2022年北京冬季オリンピックで獲得したメダルを待っています。

Amazonの買い物客は、わずか10ドルのシルクの枕カバーのおかげで、「甘やかされた赤ちゃんのように」眠れると言っています

Amazonの買い物客は、わずか10ドルのシルクの枕カバーのおかげで、「甘やかされた赤ちゃんのように」眠れると言っています

何千人ものAmazonの買い物客がMulberry Silk Pillowcaseを推奨しており、現在販売中. シルクの枕カバーにはいくつかの色があり、髪を柔らかく肌を透明に保ちます。Amazonで最大46%オフになっている間にシルクの枕カバーを購入してください

パデュー大学の教授が覚醒剤を扱った疑いで逮捕され、女性に性的好意を抱かせる

パデュー大学の教授が覚醒剤を扱った疑いで逮捕され、女性に性的好意を抱かせる

ラファイエット警察署は、「不審な男性が女性に近づいた」という複数の苦情を受けて、12 月にパデュー大学の教授の捜査を開始しました。

コンセプト ドリフト: AI にとって世界の変化は速すぎる

コンセプト ドリフト: AI にとって世界の変化は速すぎる

私たちの周りの世界と同じように、言語は常に変化しています。以前の時代では、言語の変化は数年または数十年にわたって発生していましたが、現在では数日または数時間で変化する可能性があります。

SF攻撃で91歳のアジア人女性が殴られ、コンクリートに叩きつけられた

犯罪擁護派のオークランドが暴力犯罪者のロミオ・ロレンゾ・パーハムを釈放

SF攻撃で91歳のアジア人女性が殴られ、コンクリートに叩きつけられた

認知症を患っている 91 歳のアジア人女性が最近、47 番街のアウター サンセット地区でロメオ ロレンゾ パーハムに襲われました。伝えられるところによると、被害者はサンフランシスコの通りを歩いていたところ、容疑者に近づき、攻撃を受け、暴行を受けました。

ℝ

“And a river went out of Eden to water the garden, and from thence it was parted and became into four heads” Genesis 2:10. ? The heart is located in the middle of the thoracic cavity, pointing eastward.

メリック・ガーランドはアメリカに失敗しましたか?

バイデン大統領の任期の半分以上です。メリック・ガーランドは何を待っていますか?

メリック・ガーランドはアメリカに失敗しましたか?

人々にチャンスを与えることは、人生で少し遅すぎると私は信じています。寛大に。

Language