web-dev-qa-db-ja.com

これはCでの未定義の動作ですか?出力を論理的に予測しない場合

コード1

_#include <stdio.h>
int f(int *a, int b) 
{
  b = b - 1;
  if(b == 0) return 1;
  else {
    *a = *a+1;

    return *a + f(a, b);
  }
}

int main() {
  int X = 5;
  printf("%d\n",f(&X, X));
}
_

このCコードを考えてみましょう。ここでの問題は、出力を予測することです。論理的には、出力として31を取得します。 ( マシンでの出力

Returnステートメントをに変更すると

_return f(a, b) + *a;
_

論理的に37を取得します。( マシンでの出力

私の友人の一人は、returnステートメントを計算しているときに

_return *a + f(a, b);
_

ツリーの深さを移動しながらaの値を計算します。つまり、最初に計算されてからf(a, b)が呼び出されますが、

_return f(a,b) + *a;
_

戻りながら解決されます。つまり、最初にf(a, b)が計算され、次に_*a_が呼び出されます。

このアプローチで、私は次のコードの出力を自分で予測しようとしました。

コード2

_#include <stdio.h>
int foo(int n) 
{
    static int r;
    if(n <= 1)
        return 1;

    r = n + r;
    return r + foo(n - 2);
} 

int main () {
   printf("value : %d",foo(5));
}
_

return(r+foo(n-2));の場合

enter image description here

論理的に出力として14を取得しています( マシン上の出力

return(foo(n-2)+r);の場合

enter image description here

出力として17を取得します。 ( マシンでの出力

ただし、システムでコードを実行すると、どちらの場合も17になります。

私の質問:

  • 友達のアプローチは正しいですか?
  • もしそうなら、なぜ私はマシンで実行したときにコード2で同じ出力を得るのですか?
  • そうでない場合、コード1コード2を解釈する正しい方法は何ですか?
  • Cは参照渡しをサポートしていないため、未定義の動作はありますか? コード1で使用されているので、ポインタを使用して実装できますか?

一言で言えば、私は上記の4つのケースで出力を予測する正しい方法を知りたかっただけです。

30
user5863049

コード1

コード1の場合、return *a + f(a, b);(およびreturn f(a, b) + *a;)の用語の評価順序が標準で指定されておらず、関数が値を変更するためaが指していること、コードの動作は不特定であり、さまざまな答えが可能です。

コメントの怒りからわかるように、「未定義の振る舞い」、「不特定の振る舞い」などの用語は、C標準では技術的な意味を持ち、この回答の以前のバージョンでは、「未定義の振る舞い」を使用すべき場所で誤用していました。不特定」。

質問のタイトルは「Cではこれは未定義の振る舞いですか?」であり、答えは「いいえ。未定義の振る舞いではなく、不特定の振る舞いです」です。

コード2—改訂版

コード2が修正された場合、関数の動作も指定されていません。静的変数rの値は再帰呼び出しによって変更されるため、評価順序を変更すると結果が変わる可能性があります。

コード2—事前改訂

コード2の場合、最初にint f(static int n) { … }で示されているように、コードはコンパイルされません(または、少なくともコンパイルされるべきではありません)。関数の引数の定義で許可されているストレージクラスはregisterのみであるため、staticが存在するとコンパイルエラーが発生するはずです。

ISO/IEC 9899:2011§6.7.6.3関数宣言子(プロトタイプを含む)¶2パラメーター宣言で発生する唯一のストレージクラス指定子はregisterです。

このように、macOS Sierra10.12.2でGCC6.3.0をコンパイルします(追加の警告は要求されないことに注意してください)。

$ gcc -O ub17.c -o ub17
ub17.c:3:27: error: storage class specified for parameter ‘n’
 int foo(static int n)
                    ^

番号;示されているようにコンパイルされません—少なくとも、最新バージョンのGCCを使用している私にとってはそうではありません。

ただし、それが固定されていると仮定すると、関数には undefined 不特定の動作:静的変数rの値は再帰呼び出しによって変更されるため、評価順序を変更すると結果が変わる可能性があります。

18

C規格は次のように述べています

6.5.2.2/10関数呼び出し:

関数指定子と実際の引数の評価の後、実際の呼び出しの前にシーケンスポイントがあります。呼び出された関数の本体の実行の前後に特にシーケンスされていない、呼び出し元の関数(他の関数呼び出しを含む)のすべての評価は、不確定にシーケンスされます1 呼び出された関数の実行に関して。94)

そして脚注86(セクション6.5/3)は次のように述べています:

ある式ではプログラムの実行中に複数回評価され、シーケンスされていない、および不確定にシーケンスされているその部分式の評価は、さまざまな評価で一貫して実行されました

return f(a,b) + *a;およびreturn *a + f(a,b);では、部分式の評価*aは不確定にシーケンスされます。この場合、同じプログラムで異なる結果が見られます。
aの副作用は上記の式で順序付けられていますが、順序は指定されていないことに注意してください。


1.評価AとBは、AがBの前または後にシーケンスされる場合、不確定にシーケンスされますが、どちらかは指定されていません。 (C11- 5.1.2.3/3)

13
haccks

最初の例の定義に焦点を当てます。

最初の例は、不特定の動作で定義されています。これは、考えられる結果が複数あることを意味しますが、動作は未定義ではありません。 (そして、コードがそれらの結果を処理できる場合、動作が定義されます。)

指定されていない動作の簡単な例は次のとおりです。

int a = 0;
int c = a + a;

nsequencedであるため、左aと右aのどちらが最初に評価されるかは指定されていません。 +演算子はシーケンスポイントを指定しません1。可能な順序は2つあり、左aが最初に評価され、次に右aが評価されるか、またはその逆です。どちらの側も変更されていないので2、動作が定義されています。


左または右にシーケンスポイントなしで変更された場合、つまりnsequencedの場合、動作は未定義になります2

int a = 0;
int c = ++a + a;


左または右にシーケンスポイントを挟んで変更した場合、左側と右側は不確定にシーケンスされます。3。これは、それらが順序付けられていることを意味しますが、どちらが最初に評価されるかは指定されていません。動作が定義されます。コンマ演算子がシーケンスポイントを導入することに注意してください4

int a = 0;
int c = a + ((void)0,++a,0);

2つの可能な順序があります。

左側が最初に評価されると、評価は0になります。次に、右側が評価されます。最初に(void)0が評価され、続いてシーケンスポイントが評価されます。次に、aがインクリメントされ、その後にシーケンスポイントが続きます。次に、0は0として評価され、左側に追加されます。結果は0です。

右側が最初に評価される場合、(void)0が評価され、次にシーケンスポイントが評価されます。次に、aがインクリメントされ、その後にシーケンスポイントが続きます。次に、0が0として評価されます。次に、左側が評価され、評価が1になります。結果は1になります。


オペランドが不確定にシーケンスされているため、例は後者のカテゴリに分類されます。関数呼び出しは同じ目的を果たします5 上記の例のコンマ演算子として。あなたの例は複雑なので、私はあなたの例にも当てはまる私のものを使用します。唯一の違いは、私の例よりも多くの可能な結果があることですが、理由は同じです。

void Function( int* a)
{
    ++(*a);
    return 0;
}
int a = 0;
int c = a + Function( &a );
assert( c == 0 || c == 1 );

2つの可能な順序があります。

左側が最初に評価されると、aは0に評価されます。次に、右側が評価され、シーケンスポイントがあり、関数が呼び出されます。次に、aがインクリメントされ、その後に完全な式の終わりによって導入された別のシーケンスポイントが続きます6、その終わりはセミコロンで示されます。次に、0が返され、0に追加されます。結果は0です。

右側を最初に評価すると、シーケンスポイントがあり、関数が呼び出されます。次に、aがインクリメントされ、その後に完全な式の終わりによって導入された別のシーケンスポイントが続きます。その後、0が返されます。次に、左側が評価され、評価が1になり、0に加算されます。結果は1です。


(引用元:ISO/IEC 9899:201x)

1 (6.5式3)
後で指定する場合を除いて、部分式の副作用と値の計算は順序付けられていません。

2 (6.5式2)
スカラーオブジェクトの副作用が、同じスカラーオブジェクトの異なる副作用、または同じスカラーオブジェクトの値を使用した値の計算に比べて順序付けされていない場合、動作は定義されていません。

3 (5.1.2.3プログラムの実行)
評価AとBは、AがBの前または後にシーケンスされる場合、不確定にシーケンスされますが、どちらが指定されていません。

4 (6.5.17コンマ演算子2)
コンマ演算子の左側のオペランドはvoid式として評価されます。その評価と右オペランドの評価の間にはシーケンスポイントがあります。

5 (6.5.2.2関数呼び出し10)
関数指定子と実際の引数の評価の後、実際の呼び出しの前にシーケンスポイントがあります。

6 (6.8ステートメントとブロック4)
完全な式の評価と、評価される次の完全な式の評価の間には、シーケンスポイントがあります。

8
2501