it-swarm.com.de

Verwirrung bei der Array-Initialisierung in C

Wenn Sie in C-Sprache ein Array wie folgt initialisieren:

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

dann werden alle Elemente des Arrays, die nicht explizit initialisiert werden, implizit mit Nullen initialisiert.

Aber wenn ich ein Array so initialisiere:

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

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

Ausgabe:

1 0 1 0 0

Ich verstehe nicht, warum a[0] anstelle von 10 gedruckt wird? Ist es undefiniertes Verhalten?

Hinweis: Diese Frage wurde in einem Interview gestellt.

98
M.S Chaudhari

TL; DR: Ich denke nicht, dass das Verhalten von int a[5]={a[2]=1}; Gut definiert ist, zumindest in C99.

Der lustige Teil ist, dass das einzige Bit, das für mich Sinn macht, der Teil ist, nach dem Sie fragen: a[0] Wird auf 1 Gesetzt, weil der Zuweisungsoperator den zugewiesenen Wert zurückgibt. Alles andere ist unklar.

Wenn der Code int a[5] = { [2] = 1 } Gewesen wäre, wäre alles einfach gewesen: Dies ist eine festgelegte Initialisierungseinstellung a[2] Auf 1 Und alles andere auf 0. Aber mit { a[2] = 1 } Haben wir einen nicht bezeichneten Initialisierer, der einen Zuweisungsausdruck enthält, und wir stürzen in ein Kaninchenloch.


Folgendes habe ich bisher gefunden:

  • a muss eine lokale Variable sein.

    6.7.8 Initialisierung

    1. Alle Ausdrücke in einem Initialisierer für ein Objekt mit statischer Speicherdauer müssen konstante Ausdrücke oder Zeichenfolgenliterale sein.

    a[2] = 1 Ist kein konstanter Ausdruck, daher muss a automatisch gespeichert werden.

  • a befindet sich in seiner eigenen Initialisierung im Gültigkeitsbereich.

    6.2.1 Identifizierungsbereiche

    1. Struktur-, Vereinigungs- und Aufzählungstags haben einen Gültigkeitsbereich, der unmittelbar nach dem Auftreten des Tags in einem Typbezeichner beginnt, der das Tag deklariert. Jede Enumerationskonstante hat einen Gültigkeitsbereich, der unmittelbar nach dem Erscheinen ihres definierenden Enumerators in einer Enumeratorliste beginnt. Jeder andere Bezeichner hat Gültigkeitsbereich, der unmittelbar nach der Vervollständigung seines Deklarators beginnt.

    Der Deklarator ist a[5], Daher sind Variablen in ihrer eigenen Initialisierung im Gültigkeitsbereich.

  • a lebt in seiner eigenen Initialisierung.

    6.2.4 Speicherdauer von Objekten

    1. Ein Objekt, dessen Bezeichner ohne Verknüpfung und ohne den Speicherklassenspezifizierer static deklariert wird, hat eine automatische Speicherdauer .

    2. Für ein solches Objekt, das keinen Array-Typ mit variabler Länge hat, gilt seine Lebensdauer erstreckt sich vom Eintritt in den Block, dem es zugeordnet ist, bis die Ausführung dieses Blocks endet in irgendeiner Weise. (Wenn Sie einen eingeschlossenen Block eingeben oder eine Funktion aufrufen, wird die Ausführung des aktuellen Blocks angehalten, aber nicht beendet.) Wenn der Block rekursiv eingegeben wird, wird jedes Mal eine neue Instanz des Objekts erstellt. Der Anfangswert des Objekts ist unbestimmt. Wenn für das Objekt eine Initialisierung angegeben ist, wird diese jedes Mal ausgeführt, wenn die Deklaration bei der Ausführung des Blocks erreicht wird. Andernfalls wird der Wert bei jedem Erreichen der Deklaration unbestimmt.

  • Nach a[2]=1 Befindet sich ein Sequenzpunkt.

    6.8 Anweisungen und Blöcke

    1. Ein vollständiger Ausdruck ist ein Ausdruck, der nicht Teil eines anderen Ausdrucks oder eines Deklarators ist. Jedes der folgenden Elemente ist ein vollständiger Ausdruck: ein Initialisierer; der Ausdruck in einer Ausdrucksanweisung; der steuernde Ausdruck einer Auswahlanweisung (if oder switch); der steuernde Ausdruck einer while oder do Anweisung; jeder der (optionalen) Ausdrücke einer for -Anweisung; Der (optionale) Ausdruck in einer return -Anweisung. Das Ende eines vollständigen Ausdrucks ist ein Sequenzpunkt.

    Beachten Sie, dass z. In int foo[] = { 1, 2, 3 } ist der Teil { 1, 2, 3 } eine in geschweifte Klammern eingeschlossene Liste von Initialisierern, hinter denen jeweils ein Sequenzpunkt steht.

  • Die Initialisierung erfolgt in der Reihenfolge der Initialisierungslisten.

    6.7.8 Initialisierung

    1. Jede in geschweifte Klammern eingeschlossene Initialisierungsliste verfügt über ein zugeordnetes aktuelles Objekt . Wenn keine Bezeichnungen vorhanden sind, werden Unterobjekte des aktuellen Objekts in der Reihenfolge des Typs des aktuellen Objekts initialisiert: Array-Elemente in aufsteigender Indexreihenfolge, Strukturelemente in Deklarationsreihenfolge und das zuerst genannte Mitglied einer Union. [...]

    1. Die Initialisierung erfolgt in der Reihenfolge der Initialisierungslisten, wobei jeder für ein bestimmtes Unterobjekt vorgesehene Initialisierer alle zuvor aufgeführten Initialisierer für dasselbe Unterobjekt überschreibt. Alle Unterobjekte, die nicht explizit initialisiert werden, müssen implizit genauso initialisiert werden wie Objekte mit statischer Speicherdauer.
  • Initialisierungsausdrücke werden jedoch nicht unbedingt in der richtigen Reihenfolge ausgewertet.

    6.7.8 Initialisierung

    1. Die Reihenfolge, in der Nebenwirkungen in den Ausdrücken der Initialisierungsliste auftreten, ist nicht angegeben.

Dennoch bleiben einige Fragen offen:

  • Sind Sequenzpunkte überhaupt relevant? Die Grundregel lautet:

    6.5 Ausdrücke

    1. Zwischen dem vorherigen und dem nächsten Sequenzpunkt darf der gespeicherte Wert eines Objekts höchstens einmal geändert werden durch Auswertung eines Ausdrucks. Darüber hinaus soll der vorherige Wert nur gelesen werden, um den zu speichernden Wert zu bestimmen.

    a[2] = 1 Ist ein Ausdruck, die Initialisierung jedoch nicht.

    Dies wird in Anhang J leicht widersprochen:

    J.2 Undefiniertes Verhalten

    • Zwischen zwei Sequenzpunkten wird ein Objekt mehr als einmal geändert oder es wird geändert und der vorherige Wert wird gelesen, außer um den zu speichernden Wert zu bestimmen (6.5).

    Anhang J besagt, dass jede Änderung zählt, nicht nur Änderungen durch Ausdrücke. Aber da Anhänge nicht normativ sind, können wir das wahrscheinlich ignorieren.

  • Wie werden die Unterobjektinitialisierungen in Bezug auf Initialisierungsausdrücke sequenziert? Werden alle Initialisierer zuerst ausgewertet (in einer bestimmten Reihenfolge), dann werden die Unterobjekte mit den Ergebnissen initialisiert (in der Reihenfolge der Initialisiererliste)? Oder können sie verschachtelt werden?


Ich denke, int a[5] = { a[2] = 1 } Wird wie folgt ausgeführt:

  1. Speicher für a wird zugewiesen, wenn der zugehörige Block eingegeben wird. Der Inhalt ist an dieser Stelle unbestimmt.
  2. Der (einzige) Initialisierer wird ausgeführt (a[2] = 1), Gefolgt von einem Sequenzpunkt. Dies speichert 1 In a[2] Und gibt 1 Zurück.
  3. Mit diesem 1 Wird a[0] Initialisiert (der erste Initialisierer initialisiert das erste Unterobjekt).

Aber hier wird es unscharf, weil die restlichen Elemente (a[1], a[2], a[3], a[4]) Auf 0 Initialisiert werden sollen. , aber es ist nicht klar, wann: Geschieht dies, bevor a[2] = 1 ausgewertet wird? Wenn ja, würde a[2] = 1 "Gewinnen" und a[2] Überschreiben, aber würde diese Zuweisung ein undefiniertes Verhalten haben, da es keinen Sequenzpunkt zwischen der Nullinitialisierung und dem Zuweisungsausdruck gibt? Sind Sequenzpunkte überhaupt relevant (siehe oben)? Oder erfolgt eine Nullinitialisierung, nachdem alle Initialisierer ausgewertet wurden? In diesem Fall sollte a[2] Am Ende 0 Lauten.

Da der C-Standard nicht klar definiert, was hier passiert, glaube ich, dass das Verhalten undefiniert ist (durch Auslassung).

94
melpomene

Ich verstehe nicht, warum a[0] anstelle von 10 gedruckt wird?

Vermutlich initialisiert a[2]=1 zuerst a[2] und das Ergebnis des Ausdrucks wird verwendet, um a[0] zu initialisieren.

Von N2176 (Entwurf C17):

6.7.9 Initialisierung

  1. Die Auswertungen der Ausdrücke der Initialisierungsliste sind in Bezug auf .__ unbestimmt sequenziert. einander und somit ist die Reihenfolge, in der irgendwelche Nebenwirkungen auftreten, nicht spezifiziert.  154)

Es scheint also, dass die Ausgabe 1 0 0 0 0 auch möglich gewesen wäre.

Fazit: Schreiben Sie keine Initialisierer, die die initialisierte Variable im laufenden Betrieb ändern.

22
user694733

Ich denke, der C11-Standard deckt dieses Verhalten ab und sagt, dass das Ergebnisis unspecified ist, und ich glaube nicht, dass C18 relevante Änderungen in Diesem Bereich vorgenommen hat.

Die Standardsprache ist nicht leicht zu analysieren . Der relevante Abschnitt der Norm ist §6.7.9 Initialisierung . Die Syntax ist dokumentiert als:

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

Beachten Sie, dass einer der Begriffe Zuweisungsausdruck ist, und da a[2] = 1 unbestreitbar ein Zuweisungsausdruck ist, ist er innerhalb von __.-Initialisierungen für Arrays mit nicht-statischer Dauer zulässig:

§4 Alle Ausdrücke in einem Initialisierer für ein Objekt mit Die statische oder Thread-Speicherdauer muss konstante Ausdrücke oder .__ sein. String-Literale.

Einer der wichtigsten Absätze ist:

§19 Die Initialisierung erfolgt in der Reihenfolge der Initialisierungen, jeweils Ein Initialisierer, der für ein bestimmtes Unterobjekt bereitgestellt wurde, überschreibt alle zuvor aufgelisteter Initialisierer für dasselbe Teilobjekt;151) Alle Unterobjekte, die nicht explizit initialisiert werden, lauten impliziert implizit das Gleiche wie Objekte, die statischen Speicher haben Dauer.

151) Ein beliebiger Initialisierer für das Unterobjekt, das überschrieben wird und daher nicht zur Initialisierung dieses Unterobjekts verwendet, wird es möglicherweise nicht unter .__ ausgewertet. alles.

Und ein weiterer wichtiger Absatz ist:

§23 Die Auswertungen der Initialisierungslistenausdrücke lauten unbestimmt aufeinander abgestimmt und somit die Reihenfolge, in der Nebenwirkungen auftreten, ist nicht spezifiziert.152)

152) Insbesondere muss die Evaluierungsreihenfolge nicht die .__ sein. Entspricht der Reihenfolge der Unterobjektinitialisierung.

Ich bin ziemlich sicher, dass Paragraph §23 darauf hinweist, dass die Notation in der -Frage:

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

führt zu unspezifiziertem Verhalten . Die Zuweisung zu a[2] ist ein Nebeneffekt, und die Auswertungsreihenfolge der -Ausdrücke ist unbestimmt aufeinander abgestimmt . Daher glaube ich nicht, dass ein So können Sie sich an den Standard wenden und behaupten, dass ein bestimmter Compiler dies richtig oder falsch behandelt.

6

Mein Verständnis ist a[2]=1 liefert den Wert 1 so wird code 

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

int a[5]={1} Wert zuweisen für a [0] = 1  

Also es drucken 1 für a [0]

Zum Beispiel 

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


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

Ich versuche, eine kurze und einfache Antwort für das Rätsel zu geben: int a[5] = { a[2] = 1 };

  1. Zuerst wird a[2] = 1 eingestellt. Das heißt, das Array sagt: 0 0 1 0 0
  2. Aber da Sie dies in den { }-Klammern getan haben, mit denen das Array der Reihe nach initialisiert wird, nimmt es den ersten Wert (1) und setzt diesen auf a[0]. Es ist, als würde int a[5] = { a[2] }; dort bleiben, wo wir a[2] = 1 bereits bekommen haben. Das resultierende Array lautet nun: 1 0 1 0 0

Ein anderes Beispiel: int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 }; - Auch wenn die Reihenfolge etwas willkürlich ist, vorausgesetzt es geht von links nach rechts, würde es in diesen 6 Schritten gehen:

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

Die Zuweisung a[2]= 1 ist ein Ausdruck mit dem Wert 1, und Sie haben im Wesentlichen int a[5]= { 1 }; geschrieben (mit dem Nebeneffekt, dass a[2] ebenfalls 1 zugewiesen ist).

0
Yves Daoust