Verwirrung über die Array-Initialisierung in C.


102

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

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

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

Aber wenn ich ein Array wie dieses 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]drucken 1statt 0? Ist es undefiniertes Verhalten?

Hinweis: Diese Frage wurde in einem Interview gestellt.


35
Der Ausdruck a[2]=1ergibt 1.
Tkausl

14
Eine sehr tiefe Frage. Ich frage mich, ob der Interviewer die Antwort selbst kennt. Ich nicht. Tatsächlich angeblich der Wert des Ausdrucks a[2] = 1ist 1, aber ich bin nicht sicher , ob Sie das Ergebnis eines bestimmten initialiser Ausdrucks als der Wert des ersten Elements nehmen dürfen. Die Tatsache, dass Sie das Anwalt-Tag hinzugefügt haben, bedeutet, dass wir eine Antwort brauchen, die den Standard zitiert.
Bathseba

15
Nun, wenn das ihre Lieblingsfrage ist, sind Sie vielleicht einer Kugel ausgewichen. Persönlich bevorzuge ich eine schriftliche Programmierübung (mit Zugriff auf einen Compiler und einen Debugger), die über einige Stunden durchgeführt wird, anstatt Fragen im "Ace" -Stil wie die oben genannten. Ich könnte eine Antwort vermuten , aber ich glaube nicht, dass sie eine echte sachliche Grundlage haben würde.
Bathsheba

1
@Bathsheba Ich würde das Gegenteil tun, da die Antwort hier jetzt beide Fragen beantwortet.
Auf Wiedersehen SE

1
@ Badsheba wäre das Beste. Trotzdem würde ich OP die Anerkennung für die Frage geben, als er das Thema aufstellte. Aber es liegt nicht an mir zu entscheiden, was ich für "das Richtige" halte.
Auf Wiedersehen SE

Antworten:


95

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 gesetzt, 1weil der Zuweisungsoperator den zugewiesenen Wert zurückgibt. Alles andere ist unklar.

Wenn der Code gewesen wäre int a[5] = { [2] = 1 }, wäre alles einfach gewesen: Das ist eine festgelegte Initialisierungseinstellung a[2]für 1und alles andere für 0. Aber mit haben { a[2] = 1 }wir einen nicht bezeichneten Initialisierer, der einen Zuweisungsausdruck enthält, und wir fallen 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] = 1ist kein konstanter Ausdruck, amuss also automatisch gespeichert werden.

  • a ist im Umfang in seiner eigenen Initialisierung.

    6.2.1 Bereiche von Bezeichnern

    1. Struktur-, Vereinigungs- und Aufzählungs-Tags haben einen Bereich, der unmittelbar nach dem Erscheinen des Tags in einem Typspezifizierer beginnt, der das Tag deklariert. Jede Aufzählungskonstante hat einen Gültigkeitsbereich, der unmittelbar nach dem Erscheinen ihres definierenden Aufzählers in einer Aufzählerliste beginnt. Jeder andere Bezeichner hat einen Gültigkeitsbereich, der unmittelbar nach Abschluss seines Deklarators beginnt.

    Der Deklarator ist a[5], also sind Variablen in ihrer eigenen Initialisierung im Geltungsbereich.

  • a lebt in seiner eigenen Initialisierung.

    6.2.4 Lagerdauer von Gegenständen

    1. Ein Objekt, dessen Bezeichner ohne Verknüpfung und ohne den Speicherklassenspezifizierer deklariert ist, statichat eine automatische Speicherdauer .

    2. Für ein solches Objekt, das keinen Array-Typ variabler Länge hat, erstreckt sich seine Lebensdauer vom Eintritt in den Block, dem es zugeordnet ist, bis die Ausführung dieses Blocks in irgendeiner Weise endet . (Durch das Eingeben eines eingeschlossenen Blocks oder das Aufrufen einer Funktion 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.

  • Es gibt einen Sequenzpunkt danach a[2]=1.

    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 ( ifoder switch); der kontrollierende Ausdruck einer whileoder doAnweisung; jeder der (optionalen) Ausdrücke einer forAnweisung; der (optionale) Ausdruck in einer returnAnweisung. Das Ende eines vollständigen Ausdrucks ist ein Sequenzpunkt.

    Beachten Sie, dass sich z. B. in int foo[] = { 1, 2, 3 }dem { 1, 2, 3 }Teil eine in Klammern eingeschlossene Liste von Initialisierern befindet, nach denen jeweils ein Sequenzpunkt steht.

  • Die Initialisierung erfolgt in der Reihenfolge der Initialisierungsliste.

    6.7.8 Initialisierung

    1. Jeder in Klammern eingeschlossenen Initialisierungsliste ist ein aktuelles Objekt zugeordnet . Wenn keine Bezeichnungen vorhanden sind, werden Unterobjekte des aktuellen Objekts in der Reihenfolge des Typs des aktuellen Objekts initialisiert: Array-Elemente in aufsteigender Reihenfolge, Strukturelemente in Deklarationsreihenfolge und das erste benannte Mitglied einer Union. [...]

     

    1. Die Initialisierung erfolgt in der Reihenfolge der Initialisierungsliste, wobei jeder Initialisierer für ein bestimmtes Unterobjekt vorgesehen ist und alle zuvor aufgelisteten 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 der Reihe nach 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 soll der gespeicherte Wert eines Objekts höchstens einmal durch Auswertung eines Ausdrucks geändert werden . Darüber hinaus darf 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 widerlegt:

    J.2 Undefiniertes Verhalten

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

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

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


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

  1. Speicher für awird zugewiesen, wenn der enthaltende Block eingegeben wird. Der Inhalt ist zu diesem Zeitpunkt unbestimmt.
  2. Der (einzige) Initialisierer wird ausgeführt ( a[2] = 1), gefolgt von einem Sequenzpunkt. Dies speichert 1in a[2]und kehrt zurück 1.
  3. Dies 1wird zum Initialisieren verwendet a[0](der erste Initialisierer initialisiert das erste Unterobjekt).

Aber hier Dinge Fuzzy weil die übrigen Elemente ( a[1], a[2], a[3], a[4]) sollen initialisiert werden 0, aber es ist nicht klar , wann: Ist es schon mal vorkommen a[2] = 1ausgewertet wird? Wenn ja, a[2] = 1würde "gewinnen" und überschreiben a[2], aber hätte diese Zuweisung ein undefiniertes Verhalten, da zwischen der Nullinitialisierung und dem Zuweisungsausdruck kein Sequenzpunkt liegt? Sind Sequenzpunkte überhaupt relevant (siehe oben)? Oder erfolgt eine Nullinitialisierung, nachdem alle Initialisierer ausgewertet wurden? Wenn ja, a[2]sollte am Ende sein 0.

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


1
Anstelle von undefiniert würde ich argumentieren, dass es nicht spezifiziert ist , was die Interpretationsmöglichkeiten für die Implementierungen offen lässt.
Einige Programmierer Typ

1
"Wir fallen in ein Kaninchenloch" LOL! Ich habe das noch nie für ein UB oder nicht spezifiziertes Zeug gehört.
BЈовић

2
@Someprogrammerdude Ich glaube nicht, dass es nicht spezifiziert werden kann (" Verhalten, bei dem diese Internationale Norm zwei oder mehr Möglichkeiten bietet und keine weiteren Anforderungen stellt, die in jedem Fall gewählt werden "), weil die Norm keine Möglichkeiten bietet, unter denen dies möglich ist wählen. Es sagt einfach nicht, was passiert, was meiner Meinung nach unter " Undefiniertes Verhalten wird [...] in dieser internationalen Norm [...] durch das Weglassen einer expliziten Definition des Verhaltens angezeigt. "
Melpomene

2
@ BЈовић Es ist auch eine sehr schöne Beschreibung nicht nur für undefiniertes Verhalten, sondern auch für definiertes Verhalten, für dessen Erklärung ein Thread wie dieser erforderlich ist.
Gnasher729

1
@JohnBollinger Der Unterschied besteht darin, dass Sie das a[0]Unterobjekt vor der Auswertung seines Initialisierers nicht initialisieren können und die Auswertung eines Initialisierers einen Sequenzpunkt enthält (da es sich um einen "vollständigen Ausdruck" handelt). Daher glaube ich, dass das Ändern des Teilobjekts, das wir initialisieren, ein faires Spiel ist.
Melpomene

22

Ich verstehe nicht, warum a[0]drucken 1statt 0?

Vermutlich wird zuerst a[2]=1initialisiert a[2], und das Ergebnis des Ausdrucks wird zur Initialisierung verwendet a[0].

Aus N2176 (Entwurf C17):

6.7.9 Initialisierung

  1. Die Auswertungen der Initialisierungslistenausdrücke sind unbestimmt aufeinander bezogen, und daher ist die Reihenfolge, in der Nebenwirkungen auftreten, nicht spezifiziert. 154)

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

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


1
Dieser Teil gilt nicht: Es gibt hier nur einen Initialisierungsausdruck, sodass er mit nichts sequenziert werden muss.
Melpomene

@melpomene Es ist der {...}Ausdruck, der initialisiert a[2]zu 0und a[2]=1Unterausdruck , der initialisiert a[2]zu 1.
user694733

1
{...}ist eine geschweifte Initialisiererliste. Es ist kein Ausdruck.
Melpomene

@melpomene Ok, vielleicht bist du genau dort. Aber ich würde immer noch argumentieren, dass es immer noch zwei konkurrierende Nebenwirkungen gibt, so dass der Absatz steht.
user694733

@melpomene Es gibt zwei Dinge zu sequenzieren: den ersten Initialisierer und die Einstellung anderer Elemente auf 0
MM

6

Ich denke, der C11-Standard deckt dieses Verhalten ab und sagt, dass das Ergebnis nicht spezifiziert ist , und ich denke, dass C18 keine relevanten Änderungen in diesem Bereich vorgenommen hat.

Die Standardsprache ist nicht einfach 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. Da a[2] = 1es sich zweifellos um einen Zuweisungsausdruck handelt, ist er in Initialisierern für Arrays mit nicht statischer Dauer zulässig:

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

Einer der wichtigsten Absätze ist:

§19 Die Initialisierung erfolgt in der Reihenfolge der Initialisiererliste, wobei jeder Initialisierer für ein bestimmtes Unterobjekt vorgesehen ist und alle zuvor aufgelisteten Initialisierer für dasselbe Unterobjekt überschreibt. 151) Alle Unterobjekte, die nicht explizit initialisiert werden, müssen implizit genauso initialisiert werden wie Objekte mit statischer Speicherdauer.

151) Ein Initialisierer für das Unterobjekt, der überschrieben wird und daher nicht zum Initialisieren dieses Unterobjekts verwendet wird, wird möglicherweise überhaupt nicht ausgewertet.

Und ein weiterer wichtiger Absatz ist:

§23 Die Auswertungen der Initialisierungslistenausdrücke sind unbestimmt aufeinander bezogen und daher ist die Reihenfolge, in der Nebenwirkungen auftreten, nicht spezifiziert. 152)

152) Insbesondere muss die Auswertungsreihenfolge nicht mit der Reihenfolge der Unterobjektinitialisierung übereinstimmen.

Ich bin mir ziemlich sicher, dass Absatz 23 die Notation in der Frage angibt:

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

führt zu nicht näher bezeichnetem Verhalten. Die Zuordnung zu a[2]ist ein Nebeneffekt, und die Bewertungsreihenfolge der Ausdrücke ist unbestimmt in Bezug zueinander geordnet. Folglich glaube ich nicht, dass es eine Möglichkeit gibt, sich auf den Standard zu berufen und zu behaupten, dass ein bestimmter Compiler dies richtig oder falsch handhabt.


Es gibt nur einen Initialisierungslistenausdruck, daher ist §23 nicht relevant.
Melpomene

2

Mein Verständnis ist, dass a[2]=1der Wert 1 zurückgegeben wird, sodass Code wird

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

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

Daher druckt es 1 für eine [0]

Beispielsweise

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


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

2
Dies ist eine [Sprachanwalt] -Frage, aber dies ist keine Antwort, die mit dem Standard funktioniert und ihn daher irrelevant macht. Außerdem gibt es zwei viel ausführlichere Antworten, und Ihre Antwort scheint nichts hinzuzufügen.
Auf Wiedersehen SE

Ich habe Zweifel. Ist das Konzept, das ich gepostet habe, falsch? Könnten Sie mich damit klarstellen?
Karthika

1
Sie spekulieren nur aus Gründen, während es bereits eine sehr gute Antwort mit relevanten Teilen des Standards gibt. Nur zu sagen, wie es passieren könnte, ist nicht das, worum es bei der Frage geht. Es geht darum, was der Standard sagt, sollte passieren.
Auf Wiedersehen SE

Aber die Person, die die obige Frage gestellt hat, fragte nach dem Grund und warum passiert das? Also habe nur ich diese Antwort fallen lassen. Aber das Konzept ist korrekt. Richtig?
Karthika

OP fragte " Ist es undefiniertes Verhalten? ". Ihre Antwort sagt nicht.
Melpomene

1

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

  1. Zuerst a[2] = 1wird gesetzt. Das heißt, das Array sagt:0 0 1 0 0
  2. Aber siehe, vorausgesetzt, du hast es in der { } Klammern , die verwendet werden, um das Array in der richtigen Reihenfolge zu initialisieren, nimmt es den ersten Wert (der ist 1) und setzt diesen auf a[0]. Es ist, als int a[5] = { a[2] };würde es bleiben, wo wir schon sind a[2] = 1. Das resultierende Array lautet jetzt:1 0 1 0 0

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

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 = 5ist keine Deklaration (oder Initialisierung). Es ist ein normaler Ausdruck, der analysiert wird, A = (B = (C = 5))weil der =Operator richtig assoziativ ist. Das hilft nicht wirklich bei der Erklärung, wie die Initialisierung funktioniert. Das Array beginnt tatsächlich zu existieren, wenn der Block eingegeben wird, in dem es definiert ist. Dies kann lange dauern, bis die eigentliche Definition ausgeführt wird.
Melpomene

1
" Es geht von links nach rechts, jeweils beginnend mit der internen Deklaration " ist falsch. Der C-Standard sagt ausdrücklich " Die Reihenfolge, in der irgendwelche Nebenwirkungen unter den Ausdrücken der Initialisierungsliste auftreten, ist nicht spezifiziert. "
Melpomene

1
Sie testen den Code aus meinem Beispiel ausreichend oft und prüfen, ob die Ergebnisse konsistent sind. “ So funktioniert das nicht. Sie scheinen nicht zu verstehen, was undefiniertes Verhalten ist. Alles in C hat standardmäßig ein undefiniertes Verhalten. Es ist nur so, dass einige Teile ein Verhalten haben, das durch den Standard definiert ist. Um zu beweisen, dass etwas Verhalten definiert hat, müssen Sie den Standard zitieren und zeigen, wo er definiert, was passieren soll. Ohne eine solche Definition ist das Verhalten undefiniert.
Melpomene

1
Die Behauptung in Punkt (1) ist hier ein enormer Sprung über die Schlüsselfrage: Tritt die implizite Initialisierung von Element a [2] auf 0 auf, bevor der Nebeneffekt des a[2] = 1Initialisiererausdrucks angewendet wird? Das beobachtete Ergebnis ist so, als ob es wäre, aber der Standard scheint nicht zu spezifizieren, dass dies der Fall sein sollte. Das ist das Zentrum der Kontroverse, und diese Antwort übersieht sie völlig.
John Bollinger

1
"Undefiniertes Verhalten" ist ein Fachbegriff mit einer engen Bedeutung. Es bedeutet nicht "Verhalten, bei dem wir uns nicht wirklich sicher sind". Die wichtigste Erkenntnis hierbei ist, dass kein Test ohne Compiler jemals zeigen kann, dass sich ein bestimmtes Programm gemäß dem Standard gut verhält oder nicht , denn wenn ein Programm ein undefiniertes Verhalten aufweist, darf der Compiler alles tun - einschließlich der Arbeit auf perfekt vorhersehbare und vernünftige Weise. Es ist nicht einfach ein Problem der Qualität der Implementierung, bei dem die Compiler-Autoren Dinge dokumentieren - das ist nicht spezifiziertes oder implementierungsdefiniertes Verhalten.
Jeroen Mostert

0

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


Es ist jedoch unklar, wann die Nebenwirkung bewertet wird und das Verhalten sich je nach Compiler ändern kann. Der Standard scheint auch zu besagen, dass dies ein undefiniertes Verhalten ist, weshalb Erklärungen für compilerspezifische Realisierungen nicht hilfreich sind.
Auf Wiedersehen SE

@KamiKaze: Sicher, der Wert 1 ist versehentlich dort gelandet.
Yves Daoust

0

Ich glaube das int a[5]={ a[2]=1 }; ist ein gutes Beispiel für einen Programmierer, der sich in seinen eigenen Fuß schießt.

Ich könnte versucht sein zu glauben, dass Sie das gemeint haben int a[5]={ [2]=1 }; sich um ein C99-Initialisierungs-Einstellungselement von 2 zu 1 und den Rest zu Null handelt.

In dem seltenen Fall, den Sie wirklich wirklich gemeint int a[5]={ 1 }; a[2]=1;haben, wäre das eine lustige Art, es zu schreiben. Auf jeden Fall läuft Ihr Code darauf hinaus, obwohl einige hier darauf hingewiesen haben, dass er nicht genau definiert ist, wann das Schreiben a[2]tatsächlich ausgeführt wird. Die Falle hierbei ist, dass a[2]=1es sich nicht um einen bestimmten Initialisierer handelt, sondern um eine einfache Zuweisung, die selbst den Wert 1 hat.


Es sieht so aus, als würde dieses Thema für Sprachanwälte Referenzen aus Standardentwürfen anfordern. Deshalb werden Sie abgelehnt (ich habe es nicht getan, wie Sie sehen, ich bin aus demselben Grund abgelehnt). Ich denke, was Sie geschrieben haben, ist völlig in Ordnung, aber es sieht so aus, als ob all diese Sprachanwälte hier entweder vom Komitee oder so ähnlich sind. Sie bitten also überhaupt nicht um Hilfe, sondern versuchen zu überprüfen, ob der Entwurf den Fall abdeckt oder nicht, und die meisten Leute hier werden ausgelöst, wenn Sie eine Antwort geben, wie Sie ihnen helfen. Ich denke, ich werde meine Antwort nicht löschen :) Wenn dieses Thema klar formuliert wäre, wäre das hilfreich gewesen
Abdurrahim
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.