877
#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?
}
4

14 に答える 14

603

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

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

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

あなたの最も興味深い例は、

u = (u++);

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

于 2009-06-04T09:20:59.763 に答える
83

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

シーケンス: (5.1.2.3)

任意の 2 つの評価Aとが与えられBた場合、Aが の前に配列されている場合B、 の実行は のA実行に先行するものとしBます。

配列なし:

Aが の前後に配列されていないB場合、とAは配列されていませんB

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

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

シーケンス ポイント:

A式との評価の間にシーケンス ポイントが存在するということは、に関連付けられたすべての値の計算副作用が、 に関連付けられたすべての値の計算と副作用の前に並べられることをB意味します。AB

次のような式について、質問に来ます

int i = 1;
i = i++;

標準は次のように述べています。

6.5 式:

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

したがって、上記の式は UB を呼び出します。これは、同じオブジェクトに対する 2 つの副作用iが相互に順序付けられていないためです。つまり、 への代入による副作用が による副作用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  

結果は 2 つの異なる結果1になり2、代入による一連の副作用に依存する++ため、UB が呼び出されます。

于 2015-06-27T00:27:48.540 に答える
79

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

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

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

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

于 2009-06-04T09:35:47.813 に答える
76

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

これは私が自分のマシンで得たものであり、私が考えていることと一緒です:

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

于 2010-05-24T13:26:05.523 に答える
61

未指定の動作未定義の動作両方呼び出すため、この動作を実際に説明することはできません。したがって、このコードについて一般的な予測を行うことはできませ。特定のコンパイラと環境を使用した非常に特殊なケースで推測しますが、本番環境の近くでは行わないでください。

したがって、unspecified behaviorに移ると、ドラフト c99 の標準セクションの6.5段落3には次のように書かれています (強調鉱山):

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

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

i = i++ + ++i;

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

プログラムが変数 ( 、など) をシーケンス ポイント間で複数回変更しているため、ここでも未定義の動作があります。ドラフト標準セクションのパラグラフ2 (強調鉱山) から:iu6.5

前のシーケンス ポイントと次のシーケンス ポイントの間で、オブジェクトの格納値は、式の評価によって最大 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ています。

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

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

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

于 2013-08-15T19:25:21.450 に答える
44

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

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

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

i = i + 1

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

i++

これは、「i に 1 を加算し、その結果を i に代入する」という意味です。したがって、次のように書くことで、2つの寄せ集めを構築する場合

i = i++

私たちが実際に言っているのは、「1 を i に加算し、結果を i に代入し、結果を i に代入する」ということです。私たちは混乱しているので、コンパイラが混乱してもあまり気にしません。

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

私たちは comp.lang.c で数え切れないほどの時間を費やして、これらのような式と、それらが未定義である理由について議論していました。理由を実際に説明しようとする私のより長い回答のうちの2つは、Web上にアーカイブされています。

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

于 2015-06-18T11:55:45.607 に答える
31

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

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++++xy++

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

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

未定義の動作は、これら 2 つの式が間にシーケンス ポイントなしで同じオブジェクトを変更するという事実です。i


もう 1 つの詳細は、printf() 呼び出しに含まれるコンマが、コンマ operatorではなく、セパレーターであることです。

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

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インクリメントi6て生成します。その後、ポストインクリメントによるものになります。i++i6ji7

したがって、関数呼び出しのコンマがコンマ演算子である場合

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

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


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

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

于 2015-12-30T20:26:30.360 に答える
23

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

于 2012-12-05T18:30:27.523 に答える
11

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

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

于 2015-04-08T03:20:31.417 に答える