web-dev-qa-db-ja.com

Cでの配列初期化についての混乱

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を表示するのか未定義の動作ですか。

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

100
msc

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またはswitchwhileまたはdoステートメントの制御式forステートメントの各(オプションの)式。 returnステートメント内の(オプションの)式。 完全な式の終わりはシーケンスポイントです。

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

  • 初期化はイニシャライザリストの順序で行われます。

    6.7.8初期化

    1. それぞれの中括弧で囲まれた初期化子リストは関連する現在のオブジェクトを持ちます。指定が存在しない場合、現在のオブジェクトのサブオブジェクトは、現在のオブジェクトの型に従って昇順に初期化されます。添字の昇順の配列要素、宣言順の構造体メンバ、および共用体の最初の名前付きメンバです。 [...]

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

    6.7.8初期化

    1. 初期化リスト式の中で副作用が発生する順序は不定です。

しかし、それでもまだいくつかの質問に答えがないままです。

  • シーケンスポイントは関連性がありますか?基本的な規則は次のとおりです。

    6.5式

    1. 前のシーケンスポイントと次のシーケンスポイントの間で、オブジェクトは格納された値をせいぜい一度だけ修正しなければならない式の評価によってさらに、以前の値は、格納される値を決定するためにのみ読み取られるものとします。

    a[2] = 1は式ですが、初期化はしていません。

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

    J.2未定義の動作

    • 2つのシーケンス点の間で、オブジェクトは2回以上修正されるか、または修正されそして記憶されるべき値を決定する以外に前の値が読まれる(6.5)。

    附属書Jは、表現による修正だけではなく、あらゆる修正の重要性を述べている。しかし、附属書は非規範的であることを考えると、私たちはおそらくそれを無視することができます。

  • 初期化式に関してサブオブジェクトの初期化はどのように順序付けされますか。すべての初期化子が最初に(何らかの順序で)評価され、次にサブオブジェクトがその結果で(初期化子リストの順序で)初期化されますか?それともそれらはインターリーブすることができますか?


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

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

ただし、残りの要素(a[1]a[2]a[3]a[4])は0に初期化されることになっているため、ここでは曖昧になります。ただし、a[2] = 1が評価される前に発生するのでしょうか。もしそうなら、a[2] = 1は "win"してa[2]を上書きするでしょうが、ゼロ初期化と代入式の間にシーケンスポイントがないので、その代入は未定義の振る舞いをするでしょうか?シーケンスポイントは関連性がありますか?(上記参照)それとも、すべての初期化子が評価された後にゼロ初期化が行われますか?もしそうなら、a[2]0になるはずです。

C標準では、ここで起こることを明確に定義していないので、その振る舞いは(省略によって)未定義であると思います。

94
melpomene

わからない、どうしてa[0]1の代わりに0を表示するのか

おそらくa[2]=1a[2]を最初に初期化し、式の結果はa[0]を初期化するために使用されます。

N2176から(C17ドラフト):

6.7.9初期化

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

そのため、1 0 0 0 0の出力も可能だったはずです。

結論:初期化された変数をその場で変更するイニシャライザを書かないでください。

22
user694733

私は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つがassignment-expressionであり、a[2] = 1は必ず代入式であるため、静的でない期間を持つ配列の初期化子の内側では許可されます。 :

§4静的またはスレッド記憶期間を持つオブジェクトに対する初期化子中のすべての式は、定数式または文字列リテラルでなければならない。

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

§19初期化子リスト順に初期化を行わなければならず、特定のサブオブジェクトに対して与えられた各初期化子は、同じサブオブジェクトに対する以前に列挙された初期化子を上書きする。151) 明示的に初期化されていないすべてのサブオブジェクトは、静的記憶期間を持つオブジェクトと同じように暗黙的に初期化されます。

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

そしてもう一つの重要なパラグラフは:

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

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

私は、パラグラフ§23が問題の表記法を示していると確信しています。

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

不特定の動作につながります。 a[2]への代入は副作用であり、式の評価順序は互いに不確定になっています。その結果、私は標準に訴える方法がないと思い、特定のコンパイラがこれを正しくまたは間違って処理していると主張します。

6

私の理解しているのはa[2]=1が値を返す1ですので、コードは次のようになります。

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

int a[5]={1}a [0] = 1に値を代入します

したがって、1 for a [0]と表示されます。

例えば

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


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

私はパズルのための短くて簡単な答えを与えようとします: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

もう1つの例: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
Battle

代入a[2]= 1は値1を持つ式であり、あなたは本質的にint a[5]= { 1 };を書きました(a[2]にも1が代入されるという副作用があります)。

0
Yves Daoust