35

私たちのクラスは、Cプログラミングの教授から次の質問を受けました。

コードが与えられます:

int x=1;
printf("%d",++x,x+1);

常にどのような出力が生成されますか?

ほとんどの学生は未定義の行動を言いました。なぜそうなのか誰かが私に理解するのを手伝ってもらえますか?

編集と回答に感謝しますが、私はまだ混乱しています。

4

8 に答える 8

40

出力は、すべての妥当なケースで 2 になる可能性があります。実際に、未定義の動作があります。

具体的には、標準は次のように述べています。

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

関数への引数を評価するのシーケンスポイントと、すべての引数が評価されたのシーケンス ポイントがあります (ただし、関数はまだ呼び出されていません)。これら 2 つの間に (つまり、引数が評価されている間)、シーケンス ポイントはありません&& ||(引数がor,演算子を使用するなど、内部的にシーケンス ポイントを含む式である場合を除きます)。

つまり、 への呼び出しは、格納されている値 (つまり)決定するためと、2 番目の引数の値(つまり ) を決定するためにprintf以前の値を読み取っていることを意味します。これは明らかに上記の要件に違反しており、未定義の動作が発生します。++xx+1

変換指定子が指定されていない追加の引数を指定したという事実は、未定義の動作にはなりません。変換指定子よりも少ない引数を指定した場合、または引数の (昇格された) 型が変換指定子の型と一致しない場合は、未定義の動作が発生しますが、追加のパラメーターを渡すことはできませ

于 2010-08-10T15:59:56.073 に答える
14

プログラムの動作が定義されていないときはいつでも、何かが起こる可能性があります — 古典的な言い回しは、「悪魔が鼻から飛び出すかもしれない」というものです — ほとんどの実装はそこまで行きません.

関数の引数は、概念的には並行して評価されます (専門用語では、評価の間にシーケンス ポイントはありません)。これは式++xを意味しx+1、この順序、逆の順序、またはインターリーブされた方法で評価される可能性があります。変数を変更してその値に並行してアクセスしようとすると、動作が未定義になります。

多くの実装では、引数は順番に評価されます (ただし、常に左から右に評価されるとは限りません)。したがって、現実の世界では 2 以外はほとんど見られません。

ただし、コンパイラは次のようなコードを生成できます。

  1. x を register にロードしますr1
  2. x+1に 1 を加えて計算しr1ます。
  3. ++xに 1 を加えて計算しr1ます。xに読み込まれているので問題ありませんr1。コンパイラがどのように設計されたかを考えると、ステップ 2 で を変更することはできません。これは、 が 2 つのシーケンス ポイント間で読み取られ、書き込まれたr1場合にのみ発生する可能性があるためです。xこれは C 標準で禁止されています。
  4. に格納r1xます。

そして、この (架空の、しかし正しい) コンパイラーでは、プログラムは 3 を出力します。

(編集:に追加の引数を渡すことprintfは正しいです (§7.19.6.1-2 in N1256 ; Prasoon Sauravに感謝) これを指摘してください。また: 例を追加しました。)

于 2010-08-10T15:38:43.133 に答える
12

正解は、コードが未定義の動作を生成することです。

動作が定義されていない理由は、2 つの式++xx + 1が (変更とは) 無関係な理由で変更xおよび読み取りxを行っており、これら 2 つのアクションがシーケンス ポイントで区切られていないためです。これにより、C (および C++) で未定義の動作が発生します。この要件は、C 言語標準の 6.5/2 で規定されています。

この場合の未定義の動作は、 printf関数に 1 つの書式指定子と 2 つの実引数しか与えられていないという事実とはまったく関係がないことに注意してください。printfフォーマット文字列にあるフォーマット指定子よりも多くの引数を与えることは、C では完全に合法です。繰り返しますが、問題は C 言語の式評価要件の違反に根ざしています。

また、この議論の一部の参加者は未定義の動作の概念を理解できず、それを未定義の動作の概念と混同することを主張していることにも注意ください。違いをよりよく説明するために、次の簡単な例を考えてみましょう

int inc_x(int *x) { return ++*x; }
int x_plus_1(int x) { return x + 1; }

int x = 1;
printf("%d", inc_x(&x), x_plus_1(x));

上記のコードは、関連する操作がx関数にラップされていることを除いて、元のコードと「同等」です。この最新の例では何が起こるでしょうか?

このコードには未定義の動作はありません。しかし、printf引数の評価順序が規定されていないため、このコードは規定されていない動作を生成します。つまり、 asまたは asprintfが呼び出される可能性があります。どちらの場合も、出力は確かに になります。ただし、このバリアントの重要な違いは、へのすべてのアクセスが各関数の最初と最後にあるシーケンス ポイントにラップされるため、このバリアントは未定義の動作を生成しないことです。printf("%d", 2, 2)printf("%d", 2, 3)2x

これはまさに、他のポスターが元の例に強制しようとしている理由です。しかし、それはできません。元の例では未定義の動作が発生しますが、これはまったく別のものです。彼らは明らかに、実際には未定義の動作は常に未指定の動作と同等であると主張しようとしています。これは、作成者の専門知識が不足していることを示しているだけの、まったくにせの主張です。元のコードでは、未定義の動作、ピリオドが生成されます。

例を続けるために、前のコード サンプルを次のように変更しましょう。

printf("%d %d", inc_x(&x), x_plus_1(x));

コードの出力は一般的に予測不能になります。印刷できる2 2か、または印刷できます2 3。ただし、動作が予測不能であっても、未定義の動作は生成されないことに注意してください。動作は未規定ですが、ビットは未定義ではありません。2 2未指定の動作は、またはの 2 つの可能性に制限されます2 3。未定義の動作は何にも制限されません。何かを印刷する代わりに、ハードドライブをフォーマットできます。違いを感じます。

于 2010-08-10T16:46:51.483 に答える
2

常にどのような出力が生成されますか?

私が考えることができるすべての環境で2を生成します。ただし、C99 標準を厳密に解釈すると、動作が未定義になります。これは、x へのアクセスがシーケンス ポイント間に存在する要件を満たしていないためです。

ほとんどの学生は、未定義の動作を述べました。なぜそうなのかを理解するのを手伝ってくれる人はいますか?

ここで、私が理解している 2 番目の質問に取り組みます。「なぜ、私のクラスのほとんどの生徒は、示されているコードが未定義の動作を構成すると言うのですか?」これまでに回答したポスターは他にないと思います。生徒の一部は、次のような式の未​​定義の値の例を覚えているでしょう。

f(++i,i)

与えられたコードはこのパターンに適合しますが、printf は最後のパラメーターを無視するため、学生はとにかく動作が定義されていると誤って考えます。このニュアンスは多くの学生を混乱させます。学生の別の部分は、David Thornley と同様に標準に精通しており、上記で説明した正しい理由で「未定義の動作」と言うでしょう。

于 2010-08-10T15:33:53.430 に答える
2

ほとんどの学生は、未定義の動作を述べました。なぜそうなのかを理解するのを手伝ってくれる人はいますか?

関数パラメータが計算される順序が指定されていないためです。

于 2010-08-10T15:28:59.060 に答える
1

未定義の動作に関する指摘は正しいですが、もう 1 つ問題があります。printf が失敗する可能性があります。ファイルIOを実行しています。失敗する可能性のある理由はいくつもありますが、完全なプログラムとそれが実行されるコンテキストを知らずにそれらを排除することは不可能です。

于 2010-08-10T17:37:14.530 に答える
0

codaddict を繰り返すと、答えは 2 です。

printf は引数 2 で呼び出され、それを出力します。

このコードが次のようなコンテキストに置かれた場合:

void do_something()
{
    int x=1;
    printf("%d",++x,x+1);
}

次に、その関数の動作が完全かつ明確に定義されます。もちろん、これが良いとか正しいとか、x の値が後で決定できると主張しているわけではありません。

于 2010-08-11T00:28:51.223 に答える
-1

出力は常に (最も重要な標準準拠のコンパイラとシステムの 99.98% で) 2.

標準によると、これは定義上、「未定義の動作」であり、自己正当化され、実際に何が起こり得るか、特にその理由について何も述べていない定義/回答のようです。

ユーティリティsplint (標準準拠チェック ツールではない)、および splint のプログラマーは、これを「不特定の動作」と見なします。これは基本的に、の更新が実際(x+1)にいつ行われたかによって、 の評価が 1+1 または 2+1 になることを意味します。xただし、式は破棄されるため (printf 形式は 1 つの引数を読み取ります)、出力は影響を受けず、2 であると言えます。

undefined.c:7:20: 引数 2 は x を変更し、引数 3 で使用されます (実際のパラメーターの評価順序は定義されていません): printf("%d\n", ++x, x + 1) コードの動作は規定されていません。関数パラメーターまたは部分式の評価の順序は定義されていないため、評価順序を制約するシーケンス ポイントで区切られていない別の場所で値が使用および変更された場合、式の結果は規定されません。

(x+1)前に述べたように、指定されていない動作は、ステートメント全体やその他の式ではなく、の評価のみに影響します。したがって、「不特定の動作」の場合、出力は 2 であり、誰も異議を唱えることはできませんでした。

しかし、これは未定義の動作ではなく、「未定義の動作」のようです。そして、「未定義の動作」は、単一の式ではなくステートメント全体に影響を与えるものでなければならないようです。これは、「未定義の動作」が実際に発生する場所 (つまり、何が正確に影響するか) に関するミステリーによるものです。

「未定義の動作」を式だけに付加する動機がある場合、「未定義の動作(x+1)」の場合のように、出力は常に (100%) であると言えます。 2. 「未定義の動作」を単に(x+1)は、1+1 か 2+1 かを判断できないことを意味します。それはただの「何でも」です。しかし、繰り返しますが、その「何でも」は printf のためにドロップされます。これは、答えが「常に (100%) 2」になることを意味します。

代わりに、ミステリアスな非対称性のため、「未定義の動作」をだけに関連付けることはできませんが、ステートメント全体ではないにしても、少なくとも に影響を与える必要があります(ちなみに、これは未定義の動作の原因です)。式だけに感染した場合、出力は「未定義の値」、つまり、-5847834 や 9032 などの任意の整数になります。ステートメント全体に感染した場合、コンソール出力にガーガベが表示される可能性があります。おそらく、CPU が詰まる前に、ctrl-c でプログラムします。x+1++x++x

都市伝説によると、「未定義の動作」はプログラム全体だけでなく、コンピューターと物理法則にも感染し、プログラムによって不思議な生き物が作成され、飛び去ったり、あなたを食べたりする可能性があります。

トピックについて有能に説明できる答えはありません。それらは単なる「ああ、標準がこれを言っているのを見てください」です(そして、それはいつものように単なる解釈です!)。したがって、少なくとも「標準が存在する」ことを学び、それらは教育的な質問を取り除きます(もちろん、未定義/未指定の動作主義やその他の標準的な事実に関係なく、コードが間違っていることを忘れないでください)、論理的な議論と役に立たない目的のない深い調査と理解。

于 2010-08-10T19:32:40.363 に答える