#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 に答える
C には未定義の動作という概念があります。つまり、一部の言語構造は構文的に有効ですが、コードを実行したときの動作を予測することはできません。
私の知る限り、未定義の動作の概念が存在する理由について、標準は明示的に述べていません。私の考えでは、言語設計者がセマンティクスにある程度の余裕を持たせたいと考えていたからです。つまり、すべての実装が整数オーバーフローをまったく同じ方法で処理することを要求するのではなく、深刻なパフォーマンス コストが発生する可能性が非常に高く、動作をそのままにしておきました。 undefined であるため、整数オーバーフローを引き起こすコードを記述した場合、何かが発生する可能性があります。
では、それを念頭に置いて、なぜこれらの「問題」があるのでしょうか? この言語は、特定のことが未定義の動作につながることを明確に示しています。問題はありません。関係する「すべき」はありません。関連する変数の 1 つが宣言されたときに未定義の動作が変更された場合volatile
、それは何も証明または変更しません。未定義です。行動について推論することはできません。
あなたの最も興味深い例は、
u = (u++);
未定義の動作の教科書の例です (ウィキペディアのシーケンス ポイントに関するエントリを参照してください)。
ここでの回答のほとんどは、これらの構造の動作が未定義であることを強調する C 標準から引用されています。これらのコンストラクトの動作が undefined である理由を理解するために、まず 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 式:
スカラー オブジェクトに対する副作用が、同じスカラー オブジェクトに対する別の副作用、または同じスカラー オブジェクトの値を使用した値の計算に対して順序付けされていない場合、動作は 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 が呼び出されます。
C99標準の関連部分は6.5 Expressions、§2だと思います
前のシーケンス ポイントと次のシーケンス ポイントの間で、オブジェクトの格納値は、式の評価によって最大 1 回変更されます。さらに、以前の値は、保存する値を決定するためにのみ読み取られます。
および 6.5.16 代入演算子、§4:
オペランドの評価順序は規定されていません。代入演算子の結果を変更したり、次のシーケンス ポイントの後にアクセスしようとした場合の動作は未定義です。
取得しているものを取得する方法を正確に知りたい場合は、コード行をコンパイルして逆アセンブルするだけです。
これは私が自分のマシンで得たものであり、私が考えていることと一緒です:
$ 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命令はある種のコンパイラ最適化だったと思いますか?)
未指定の動作と未定義の動作の両方を呼び出すため、この動作を実際に説明することはできません。したがって、このコードについて一般的な予測を行うことはできません。特定のコンパイラと環境を使用した非常に特殊なケースで推測しますが、本番環境の近くでは行わないでください。
したがって、unspecified behaviorに移ると、ドラフト c99 の標準セクションの6.5
段落3には次のように書かれています (強調鉱山):
演算子とオペランドのグループ化は、構文によって示されます.74) 後で指定される場合を除き (関数呼び出し ()、&&、||、?:、およびコンマ演算子について)、部分式の評価の順序およびどちらの副作用が発生するかは、どちらも特定されていません。
したがって、次のような行がある場合:
i = i++ + ++i;
i++
最初に評価されるかどうか、または評価されるかどうかはわかりません++i
。これは主に、最適化のためのより良いオプションをコンパイラに提供するためです。
プログラムが変数 ( 、など) をシーケンス ポイント間で複数回変更しているため、ここでも未定義の動作があります。ドラフト標準セクションのパラグラフ2 (強調鉱山) から:i
u
6.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
ています。
この国際規格が要件を課していない、移植性のない、または誤ったプログラム構造または誤ったデータの使用時の動作
そして、次のことに注意してください。
未定義の可能性のある動作は、状況を完全に無視して予測不能な結果をもたらすことから、翻訳中またはプログラム実行中に、環境に特有の文書化された方法で動作すること (診断メッセージの発行の有無にかかわらず)、翻訳または実行の終了 (診断メッセージの発行を伴う) にまで及びます。診断メッセージの)。
これに答えるもう 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の残りの質問も参照してください。
多くの場合、この質問は次のようなコードに関連する質問の複製としてリンクされています
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
もう 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
インクリメントi
し6
て生成します。その後、ポストインクリメントによるものになります。i++
i
6
j
i
7
したがって、関数呼び出しのコンマがコンマ演算子である場合、
printf("%d %d\n", ++i, i++);
問題になりません。ただし、ここのコンマは区切り文字であるため、未定義の動作が発生します。
未定義の動作に慣れていない人は、すべての C プログラマーが未定義の動作について知っておくべきことを読ん で、C の未定義の動作の概念と他の多くのバリエーションを理解することをお勧めします。
この投稿:未定義、未指定、および実装定義の動作も関連しています。
コンパイラやプロセッサが実際にそうする可能性は低いですが、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を使用する場合 (p
q
(*p)
(*q)
i
p
2 回) と の両方に同じオブジェクトのアドレスが渡された場合に発生するデッドロックを、コンパイラが認識または回避する必要はありませんq
。
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
最新のコンパイラは、これを非常にうまく最適化します。実際、最初に書いたコードよりも優れている可能性があります (期待どおりに機能したと仮定して)。