Cでの配列の初期化に関する混乱


102

C言語で、次のように配列を初期化する場合:

int a[5] = {1,2};

明示的に初期化されていない配列のすべての要素は、ゼロで暗黙的に初期化されます。

しかし、私がこのような配列を初期化すると:

int a[5]={a[2]=1};

printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);

出力:

1 0 1 0 0

わかりません、なぜ代わりにa[0]印刷1するの0ですか?未定義の動作ですか?

注:この質問はインタビューで尋ねられました。


35
式はにa[2]=1評価され1ます。
tkausl 2018

14
非常に深い質問です。面接官は自分で答えを知っているのでしょうか。私はしません。実際、表向き式の値がa[2] = 1ある1が、あなたは最初の要素の値として指定された初期化子式の結果を取ることが許可されている場合、私はよく分かりません。弁護士タグを追加したということは、基準を引用して回答が必要だと私は思います。
バトシェバ2018

15
まあそれが彼らのお気に入りの質問であれば、あなたは弾丸を避けたかもしれません。個人的には、上記のような「エース」形式の質問ではなく、書面によるプログラミング演習(コンパイラとデバッガにアクセスできる)を数時間かけて行うことを好みます。私は可能性がconject答えを、私はそれがどんな本当の事実上の根拠を持っているとは思いません。
バトシェバ2018

1
@Bathshebaここでの答えは両方の質問に答えるようになるので、私は反対のことをします。
Goodbye SE

1
@Bathshebaが最高です。それでも彼がトピックを思いついたとき、私はOPに質問の信用を与えます。しかし、これは私が「正しいこと」だと思うものだけを決めるのではありません。
Goodbye SE

回答:


95

TL; DR:の動作はint a[5]={a[2]=1};、少なくともC99では明確に定義されていないと思います。

面白い部分は、私にとって意味のある唯一のビットはあなたが尋ねている部分です:割り当て演算子が割り当てられた値を返すためにa[0]設定1されています。それ以外は不明確です。

コードがだった場合int a[5] = { [2] = 1 }、すべてが簡単でした。それは、に指定された初期化子設定a[2]1あり、それ以外のすべてはです0。しかし{ a[2] = 1 }、割り当て式を含む指定されていない初期化子があり、うさぎの穴に落ちました。


これが私がこれまでに見つけたものです:

  • a ローカル変数でなければなりません。

    6.7.8初期化

    1. 静的格納期間を持つオブジェクトの初期化子のすべての式は、定数式または文字列リテラルでなければなりません。

    a[2] = 1は定数式ではないため、a自動ストレージが必要です。

  • a 独自の初期化ではスコープ内にあります。

    6.2.1識別子のスコープ

    1. 構造タグ、共用体タグ、および列挙タグには、タグを宣言する型指定子でタグが出現した直後から始まるスコープがあります。各列挙定数には、列挙子リストに定義する列挙子が現れた直後から始まるスコープがあります。他の識別子には、宣言子の完了直後に始まるスコープがあります。

    宣言子はa[5]なので、変数は独自の初期化のスコープ内にあります。

  • a 独自の初期化で生きています。

    6.2.4オブジェクトの保存期間

    1. 識別子がリンクなしで宣言され、ストレージクラス指定子なしで宣言されているオブジェクトにstaticは、自動ストレージ期間があります。

    2. 可変長配列型を持たないそのようなオブジェクトの場合、その存続期間は、関連付けられているブロックへのエントリーからそのブロックの実行が何らかの形で終了するまで続きます。(囲まれたブロックに入るか、関数を呼び出すと、現在のブロックの実行が中断されますが、終了しません。)ブロックが再帰的に入力されると、オブジェクトの新しいインスタンスが毎回作成されます。オブジェクトの初期値は不定です。オブジェクトに初期化が指定されている場合、ブロックの実行で宣言に到達するたびに初期化が実行されます。それ以外の場合、値は宣言に到達するたびに不確定になります。

  • の後にシーケンスポイントがありa[2]=1ます。

    6.8ステートメントとブロック

    1. 完全な発現は、別の式の又は宣言の一部ではない表現です。以下のそれぞれが完全な表現である:初期化子。式ステートメント内の式。選択ステートメントの制御式(ifまたはswitch); whileor doステートメントの制御式。forステートメントの各(オプションの)式。returnステートメント内の(オプションの)式。完全な式の終わりはシーケンスポイントです。

    たとえばint foo[] = { 1, 2, 3 }{ 1, 2, 3 }部分には中括弧で囲まれたイニシャライザのリストがあり、各イニシャライザの後にシーケンスポイントがあります。

  • 初期化は、初期化子リストの順序で実行されます。

    6.7.8初期化

    1. ブレースで囲まれた初期化子リストにはそれぞれ、現在のオブジェクトが関連付けられています。指定がない場合、現在のオブジェクトのサブオブジェクトは、現在のオブジェクトのタイプに応じて順番に初期化されます。添字の昇順の配列要素、宣言の順序の構造体メンバー、および共用体の最初の名前付きメンバー。[...]

     

    1. 初期化はイニシャライザリストの順序で行われ、各サブイニシャライザは特定のサブオブジェクトに提供され、同じサブオブジェクトの以前にリストされたイニシャライザをオーバーライドします。明示的に初期化されていないすべてのサブオブジェクトは、静的ストレージ期間を持つオブジェクトと同じように暗黙的に初期化されます。
  • ただし、初期化式は必ずしも順番に評価されるわけではありません。

    6.7.8初期化

    1. 初期化リスト式の中で副作用が発生する順序は指定されていません。

ただし、それでも未解決の質問がいくつか残っています。

  • シーケンスポイントは適切ですか?基本的なルールは:

    6.5式

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

    a[2] = 1 式ですが、初期化ではありません。

    これは附属書Jとは少し矛盾しています。

    J.2未定義の動作

    • 2つのシーケンスポイント間で、オブジェクトが複数回変更されるか、変更されて、保存する値を決定する以外に以前の値が読み取られます(6.5)。

    附属書Jは、式による変更だけでなく、変更の数を数えると述べています。しかし、附属書は非規範的であることを考えると、おそらくそれは無視できます。

  • サブオブジェクトの初期化は、イニシャライザ式に関してどのようにシーケンスされますか?すべてのイニシャライザが最初に(ある順序で)評価され、次にサブオブジェクトが結果で初期化されます(イニシャライザリストの順序)?または、インターリーブできますか?


int a[5] = { a[2] = 1 }は次のように実行されると思います:

  1. のストレージaは、その包含ブロックに入るときに割り当てられます。現時点では内容は不定です。
  2. (のみ)初期化子が実行され(a[2] = 1)、その後にシーケンスポイントが続きます。これはに格納さ1a[2]て返されます1
  3. これ1は初期化に使用されますa[0](最初の初期化子は最初のサブオブジェクトを初期化します)。

しかし、ここで物事が(残りの要素のでファジー取得a[1]a[2]a[3]a[4])に初期化されることになっている0が、ときそれは明確ではありません:ん、それは前に起こるがa[2] = 1評価されますか?もしそうなら、a[2] = 1「勝つ」と上書きしますa[2]が、ゼロの初期化と割り当て式の間にシーケンスポイントがないため、その割り当ては未定義の動作をしますか?シーケンスポイントは関連していますか(上記を参照)?または、すべてのイニシャライザが評価された後にゼロの初期化が発生しますか?もしそうa[2]なら、最終的にはになるはず0です。

C標準では、ここで何が起こるかを明確に定義していないため、動作は(省略により)定義されていないと思います。


1
代わりに、未定義の私はそれがだと主張するだろう、不特定のものが実装による解釈のために開いたままにしています、。
一部のプログラマー、

1
「うさぎの穴に落ちる」笑!UBや不特定のもののためにそれを聞いたことがない。
BЈовић

2
@Someprogrammerdude私はそれが不特定であるとは思わない(「この国際標準が2つ以上の可能性を提供し、どのインスタンスで選択されるものにもそれ以上の要件を課さない振る舞い」)は、標準がその間で本当に可能性を提供しないためです選ぶ。それは単に何が起こるかを言わない、私は「未定義の振る舞いはこの国際標準で示されている[...]振る舞いの明示的な定義の省略によって示される。
melpomene

2
@BЈовићこれは、未定義の動作だけでなく、このようなスレッドで説明する必要のある定義済みの動作についても非常にわかりやすい説明です。
gnasher729 2018

1
@JohnBollinger違いはa[0]、イニシャライザを評価する前にサブオブジェクトを実際に初期化することはできず、イニシャライザの評価にはシーケンスポイントが含まれることです(これは「完全式」であるため)。したがって、初期化しているサブオブジェクトを変更することは公正なゲームだと思います。
メルポメン

22

わかりません、なぜ代わりにa[0]印刷1するの0ですか?

おそらく最初にa[2]=1初期化しa[2]、式の結果を使用してを初期化しますa[0]

N2176(C17ドラフト)から:

6.7.9初期化

  1. 初期化リスト式の評価は相互に不確定に順序付けられているため、副作用が発生する順序は規定されていません。 154)

したがって、出力1 0 0 0 0も可能だったと思われます。

結論:オンザフライで初期化された変数を変更する初期化子を書かないでください。


1
その部分は当てはまりません。ここには初期化式が1つしかないため、何もシーケンス処理する必要はありません。
メルポメン2018

@melpomeneあり{...}初期化式a[2]0、及びa[2]=1初期化サブ表現a[2]には1
user694733 2018

1
{...}ブレース付きの初期化子リストです。それは表現ではありません。
メルポメン2018

@melpomeneわかりました、あなたはそこにいるかもしれません。しかし、私はまだ段落が立つように2つの競合する副作用があると主張します。
user694733 2018

@melpomeneシーケンスされる2つの事柄があります:最初の初期化子と他の要素の0への設定
MM

6

C11標準はこの動作をカバーし、結果は不特定であると述べています。C18がこの領域に関連する変更を行ったとは思いません。

標準言語は解析が容易ではありません。標準の関連セクションは §6.7.9初期化です。構文は次のように文書化されています。

initializer:
                assignment-expression
                { initializer-list }
                { initializer-list , }
initializer-list:
                designationopt initializer
                initializer-list , designationopt initializer
designation:
                designator-list =
designator-list:
                designator
                designator-list designator
designator:
                [ constant-expression ]
                . identifier

用語の1つは割り当て式であることに注意してください。これa[2] = 1は間違いなく割り当て式であるため、非静的期間の配列の初期化子内で許可されます。

§4静的またはスレッドの保存期間を持つオブジェクトの初期化子のすべての式は、定数式または文字列リテラルでなければなりません。

重要な段落の1つは次のとおりです。

§19初期化はイニシャライザリストの順序で行われます。各イニシャライザは特定のサブオブジェクトに提供され、同じサブオブジェクトの以前にリストされた初期化子をオーバーライドします。151) 明示的に初期化されていないすべてのサブオブジェクトは、静的ストレージ期間を持つオブジェクトと同じように暗黙的に初期化されます。

151)オーバーライドされ、そのサブオブジェクトの初期化に使用されないサブオブジェクトの初期化子は、まったく評価されない可能性があります。

もう1つの重要な段落は次のとおりです。

§23初期化リスト式の評価は、相互に不定に順序付けられているため、副作用が発生する順序は規定されていません。152)

152)特に、評価順序はサブオブジェクトの初期化の順序と同じである必要はありません。

私は、§23項が問題の表記法を示していることをかなり確信しています:

int a[5] = { a[2] = 1 };

不特定の行動につながります。への代入a[2]は副作用であり、式の評価順序は相互に不確定に順序付けられます。したがって、標準にアピールし、特定のコンパイラがこれを正しくまたは誤って処理していると主張する方法はないと思います。


初期化リスト式は1つしかないため、§23は関係ありません。
melpomene

2

私の理解は a[2]=11を返すため、コードは

int a[5]={a[2]=1} --> int a[5]={1}

int a[5]={1}a [0] = 1に値を割り当てる

したがって、a [0]には1が出力さます

例えば

char str[10]={‘H’,‘a’,‘i’};


char str[0] = H’;
char str[1] = a’;
char str[2] = i;

2
これは[language-lawyer]の質問ですが、これは標準で機能する回答ではないため、無関係です。さらに、利用可能な詳細な回答が2つあり、あなたの回答は何も追加しないようです。
Goodbye SE、

疑問があります。投稿したコンセプトは間違っていますか?これで私を明確にしてもらえますか?
Karthika、2018

1
標準の関連部分ですでに与えられた非常に良い答えがある一方で、あなたは単に理由で推測します。それがどのように起こり得るかを言うだけでは、問題は何についてではありません。それは規格が何を起こすべきかと言うことについてです。
Goodbye SE、

しかし、上記の質問を投稿した人が理由を尋ね、なぜそれが起こるのですか?だから私だけがこの答えを落としましたが、コンセプトは正しいですよね?
Karthika、2018

OPは、「未定義の動作ですか?尋ねました。あなたの答えは言いません。
メルポメン

1

私はパズルに対して短く簡単な答えを出そうとしています: int a[5] = { a[2] = 1 };

  1. まずはa[2] = 1セットです。それは配列が言うことを意味します:0 0 1 0 0
  2. ただし{ }、配列を順番に初期化するために使用される角かっこでそれを行ったとすると、最初の値(つまり1)を取り、それをに設定しa[0]ます。それはint a[5] = { a[2] };、私たちがすでに得た場所に残るかのようa[2] = 1です。結果の配列は次のようになります。1 0 1 0 0

別の例:int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };-順序はいくぶん任意ですが、左から右に行くと仮定すると、次の6つのステップになります。

0 0 0 1 0 0
1 0 0 1 0 0
1 0 0 1 2 0
1 2 0 1 2 0
1 2 0 1 2 3
1 2 3 1 2 3

1
A = B = C = 5は宣言(または初期化)ではありません。これはA = (B = (C = 5))=演算子が右結合であるために解析される通常の式です。これは、初期化がどのように機能するかを説明するのに役立ちません。配列は、それが定義されているブロックに入るときに実際に存在し始めます。これは、実際の定義が実行される前に長くなる可能性があります。
メルポメン2018

1
" 左から右へ、それぞれ内部宣言で始まる "は正しくありません。C標準は、明示的に「と言う副作用が初期化リスト式の間で発生する順序が指定されていない。
メルポメネ

1
あなたは私の例のコードを十分な回数テストし、結果が一貫しているかどうかを確認します。」それはそれがどのように機能するかではありません。未定義の動作が何であるかを理解していないようです。Cのすべてはデフォルトで未定義の動作をします。規格によって定義されている動作を持つ部品があるだけです。何かが動作を定義したことを証明するには、標準を引用し、それが何が起こるかを定義する場所を示す必要があります。そのような定義がない場合、動作は未定義です。
メルポメン2018

1
ポイント(1)のアサーションは、ここでの重要な質問に対する大きな飛躍です:a[2] = 1初期化子式の副作用が適用される前に、要素a [2]の0への暗黙的な初期化が発生しますか?観察された結果はあたかもそうであるかのようですが、標準はそれがそうであるべきであると明記していません。 それが論争の中心であり、この答えはそれを完全に見過ごしています。
ジョンボリンジャー2018

1
「未定義の動作」とは、狭い意味を持つ専門用語です。それは「私たちが実際に確信が持てない行動」を意味するのではありません。ここで重要な洞察は全くテストは、ないコンパイラで、これまでに特定のプログラムを表示することができません行儀であるかではないということです標準に従ったプログラムは未定義の動作を持っている場合、コンパイラが行うことに許可されているので、何も作業を含みます-完全に予測可能で合理的な方法で。これは、コンパイラの作成者が物事を文書化する実装品質の問題ではありません。これは、不特定の動作または実装定義の動作です。
Jeroen Mostert、2018

0

割り当てa[2]= 1は、値を持つ式であり、1基本的には int a[5]= { 1 };(副作用もa[2]割り当て1られて)記述したものです。


ただし、副作用がいつ評価されるかは不明であり、コンパイラによって動作が変わる可能性があります。また、標準では、これは未定義の動作であり、コンパイラ固有の実現についての説明が役に立たないことを述べているようです。
Goodbye SE

@KamiKaze:確かに、値1は偶然そこに上陸しました。
Yves Daoust、2018年

0

int a[5]={ a[2]=1 };プログラマーが自分の足に自分を撃ち込む良い例だと思います。

あなたがint a[5]={ [2]=1 };言ったのは、C99で指定されたイニシャライザを2に1に設定し、残りを0に設定することだと思ったくなるかもしれません。

あなたが本当に本当に意味したまれなケースではint a[5]={ 1 }; a[2]=1;、それはそれを書く面白い方法でしょう。とにかく、これはあなたのコードが要約するものですが、書き込みa[2]が実際に実行されるとき、それは明確に定義されていないとここでいくつかは指摘しました。ここでの落とし穴a[2]=1は、指定された初期化子ではなく、それ自体が値1を持つ単純な割り当てです。


この言語弁護士のトピックは標準ドラフトからの参照を求めているようです。それがあなたが反対票を投じられた理由です(私が同じ理由で反対票を投じられているのを見て私はそれをしませんでした)。私はあなたが書いたものは完全に素晴らしいと思いますが、ここにいるこれらすべての言語弁護士は、委員会かそのようなもののどちらかであるように見えます。したがって、彼らはまったく助けを求めていません。ドラフトがケースをカバーしているかどうかを確認しようとしています。そして、あなたが彼らを助けるように答えると、ここにいるほとんどの人がトリガーされます。私は私の答えを間違って削除すると思います:)このトピックのルールがそれを役立たせたであろうことを明確に述べれば
Abdurrahim
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.