Ihre Frage lautete wahrscheinlich nicht: "Warum sind diese Konstrukte in C undefiniertes Verhalten?". Ihre Frage war wahrscheinlich: "Warum hat dieser Code (mit ++
) nicht den Wert, den ich erwartet habe?", und jemand hat Ihre Frage als Duplikat markiert und Sie hierher geschickt.
この answer versucht, diese Frage zu beantworten: Warum hat Ihr Code nicht die erwartete Antwort gegeben, und wie können Sie lernen, Ausdrücke zu erkennen (und zu vermeiden), die nicht wie erwartet funktionieren.
Ich nehme an, Sie kennen die grundlegende Definition von C's ++
y --
Operatoren, und wie die Präfixform ++x
unterscheidet sich von der Postfixform x++
. Aber diese Operatoren sind schwer zu begreifen. Um sicher zu gehen, dass Sie sie verstanden haben, haben Sie vielleicht ein kleines Testprogramm geschrieben, das etwas wie
int x = 5;
printf("%d %d %d\n", x, ++x, x++);
Aber zu Ihrer Überraschung hat dieses Programm no zu verstehen - es gab eine seltsame, unerklärliche Ausgabe, die darauf hindeutete, dass vielleicht ++
macht etwas ganz anderes, ganz und gar nicht das, was Sie dachten.
Oder Sie haben es mit einem schwer verständlichen Ausdruck zu tun wie
int x = 5;
x = x++ + ++x;
printf("%d\n", x);
Vielleicht hat Ihnen jemand den Code als Rätsel aufgegeben. Auch dieser Code ergibt keinen Sinn, vor allem wenn Sie ihn ausführen - und wenn Sie ihn mit zwei verschiedenen Compilern kompilieren und ausführen, erhalten Sie wahrscheinlich zwei verschiedene Antworten! Was ist da los? Welche Antwort ist richtig? (Und die Antwort ist, dass beide richtig sind, oder keine von beiden.)
Wie Sie bereits gehört haben, sind diese Ausdrücke undefiniert Das bedeutet, dass die Sprache C keine Garantie dafür gibt, was sie tun werden. Das ist ein seltsames und beunruhigendes Ergebnis, denn Sie dachten wahrscheinlich, dass jedes Programm, das Sie schreiben können, solange es kompiliert und ausgeführt wird, eine eindeutige, wohldefinierte Ausgabe erzeugt. Aber im Fall von undefiniertem Verhalten ist das nicht der Fall.
Was macht einen Ausdruck undefiniert? Sind Ausdrücke mit ++
y --
immer undefiniert? Natürlich nicht: Es handelt sich um nützliche Operatoren, und wenn man sie richtig einsetzt, sind sie vollkommen wohldefiniert.
Die Ausdrücke, um die es hier geht, sind dann undefiniert, wenn zu viel auf einmal passiert, wenn wir nicht sagen können, in welcher Reihenfolge die Dinge passieren werden, aber wenn die Reihenfolge für das Ergebnis wichtig ist.
Kehren wir zu den beiden Beispielen zurück, die ich in dieser Antwort verwendet habe. Als ich schrieb
printf("%d %d %d\n", x, ++x, x++);
Die Frage ist, ob man vor dem eigentlichen Aufruf printf
errechnet der Compiler den Wert von x
zuerst, oder x++
oder vielleicht ++x
? Aber es stellt sich heraus Wir wissen es nicht . Es gibt in C keine Regel, die besagt, dass die Argumente einer Funktion von links nach rechts, von rechts nach links oder in einer anderen Reihenfolge ausgewertet werden. Wir können also nicht sagen, ob der Compiler Folgendes tun wird x
zuerst, dann ++x
entonces x++
, oder x++
entonces ++x
entonces x
oder eine andere Reihenfolge. Aber die Reihenfolge spielt eine Rolle, denn je nachdem, welche Reihenfolge der Compiler verwendet, erhalten wir eine andere Zahlenreihe ausgedruckt.
Was ist mit diesem verrückten Ausdruck?
x = x++ + ++x;
Das Problem bei diesem Ausdruck ist, dass er drei verschiedene Versuche enthält, den Wert von x
(1) die x++
Teil versucht zu nehmen x
Wertes, addieren Sie 1 und speichern Sie den neuen Wert in x
und geben den alten Wert zurück; (2) die ++x
Teil versucht zu nehmen x
Wertes, addieren Sie 1 und speichern Sie den neuen Wert in x
und geben den neuen Wert zurück; und (3) die x =
Teil versucht, die Summe der beiden anderen Teile wieder auf x
. Welche dieser drei versuchten Zuweisungen wird "gewinnen"? Welcher der drei Werte wird tatsächlich den Endwert von x
? Auch hier gibt es, was vielleicht überrascht, keine Regel in C, die uns das sagt.
Man könnte meinen, dass der Vorrang oder die Assoziativität oder die Auswertung von links nach rechts Aufschluss darüber gibt, in welcher Reihenfolge die Dinge geschehen, aber das ist nicht der Fall. Sie werden mir vielleicht nicht glauben, aber nehmen Sie mich beim Wort, und ich sage es noch einmal: Vorrang und Assoziativität bestimmen nicht jeden Aspekt der Auswertungsreihenfolge eines Ausdrucks in C. Insbesondere, wenn es innerhalb eines Ausdrucks mehrere verschiedene Stellen gibt, an denen wir versuchen, einen neuen Wert für etwas wie x
, Vorrang und Assoziativität tun no Sie können uns nicht sagen, welcher dieser Versuche als erster oder als letzter oder sonst wie stattfindet.
Wenn Sie also sichergehen wollen, dass alle Ihre Programme wohldefiniert sind, welche Ausdrücke können Sie schreiben und welche können Sie nicht schreiben?
Diese Ausdrücke sind alle in Ordnung:
y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;
Diese Ausdrücke sind alle undefiniert:
x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);
Und die letzte Frage lautet: Wie kann man feststellen, welche Ausdrücke wohldefiniert und welche undefiniert sind?
Wie ich bereits sagte, sind die undefinierten Ausdrücke diejenigen, bei denen zu viel auf einmal passiert, bei denen man nicht sicher sein kann, in welcher Reihenfolge die Dinge passieren, und bei denen die Reihenfolge eine Rolle spielt:
- Wenn eine Variable an zwei oder mehr Stellen geändert (zugewiesen) wird, woher wissen Sie dann, welche Änderung zuerst erfolgt?
- Wenn es eine Variable gibt, die an einer Stelle geändert wird und deren Wert an einer anderen Stelle verwendet wird, woher wissen Sie dann, ob der alte oder der neue Wert verwendet wird?
Ein Beispiel für #1 ist der Ausdruck
x = x++ + ++x;
gibt es drei Versuche zur Änderung x
.
Als Beispiel für #2, in dem Ausdruck
y = x + x++;
verwenden wir beide den Wert von x
und ändern Sie sie.
Das ist also die Antwort: Stellen Sie sicher, dass in jedem Ausdruck, den Sie schreiben, jede Variable höchstens einmal geändert wird, und wenn eine Variable geändert wird, versuchen Sie nicht auch noch, den Wert dieser Variable an anderer Stelle zu verwenden.
Noch eine Sache. Sie fragen sich vielleicht, wie man die undefinierten Ausdrücke "repariert", die ich zu Beginn dieser Antwort vorgestellt habe.
Im Fall von printf("%d %d %d\n", x, ++x, x++);
Es ist ganz einfach - schreiben Sie es einfach als drei separate printf
Anrufe:
printf("%d ", x);
printf("%d ", ++x);
printf("%d\n", x++);
Jetzt ist das Verhalten genau definiert, und Sie werden vernünftige Ergebnisse erhalten.
Im Fall von x = x++ + ++x
Auf der anderen Seite gibt es keine Möglichkeit, es zu reparieren. Es gibt keine Möglichkeit, ihn so zu schreiben, dass er sich garantiert so verhält, wie Sie es erwarten - aber das ist in Ordnung, denn Sie würden niemals einen Ausdruck schreiben wie x = x++ + ++x
in einem echten Programm sowieso.
16 Stimmen
@Jarett, nein, ich brauchte nur ein paar Hinweise auf "Sequenzpunkte". Bei der Arbeit fand ich ein Stück Code mit i = i++, ich dachte "Das ändert den Wert von i nicht". Ich habe es getestet und mich gefragt, warum. Inzwischen habe ich diese Anweisung entfernt und durch i++ ersetzt;
232 Stimmen
Ich finde es interessant, dass jeder IMMER davon ausgeht, dass solche Fragen gestellt werden, weil der Fragesteller das fragliche Konstrukt BENUTZEN will. Meine erste Vermutung war, dass PiX weiß, dass diese Konstrukte schlecht sind, aber neugierig ist, warum sie sich so verhalten, egal welchen Compiler er/sie benutzt... Und ja, was unWind sagte... es ist undefiniert, es könnte alles tun... einschließlich JCF (Jump and Catch Fire)
41 Stimmen
Ich bin neugierig: Warum warnen Compiler anscheinend nicht vor Konstrukten wie "u = u++ + ++u;", wenn das Ergebnis undefiniert ist?
0 Stimmen
Lesen Sie auch: stackoverflow.com/questions/13615243/
0 Stimmen
Warum sollten Sie erwarten, dass i = (i++) gleich 1 ist? Klammern setzen die natürliche Reihenfolge der Auswertung außer Kraft, so dass alles, was innerhalb von Klammern steht, zuerst geschieht. Also wird i++ zuerst passieren (natürlich würde i++ nach der Zuweisung passieren), wodurch es 2 wird. 2 würde dann wieder i zugewiesen werden. i ist 2.
5 Stimmen
(i++)
ergibt unabhängig von den Klammern immer noch den Wert 13 Stimmen
Was auch immer
i = (i++);
sollte, gibt es sicherlich einen klareren Weg, es zu schreiben. Das wäre selbst dann der Fall, wenn es gut definiert wäre. Selbst in Java, das das Verhalten voni = (i++);
ist es immer noch ein schlechter Code. Einfach schreibeni++;
0 Stimmen
Nur meine Meinung: solche Anweisungen sind ein undefiniertes Verhalten, weil Sie dieselbe Variable (Speicherplatz) lesen und schreiben. Dadurch kann der Compiler eine "Optimierung" durchführen, d. h. er räumt Ihren eigenen Mist auf. Natürlich bringt dies einige Einschränkungen mit sich. Das Lesen aus dem Speicher ist LANGSAM, daher werden Register verwendet und dann mit dem echten Speicher synchronisiert. Der Nebeneffekt ist, dass der Compiler nun nicht mehr weiß, welchen Wert er verwenden soll: den aus dem Speicher oder den im Register, der bereits geändert wurde. (Ich erkläre es gerne so, das macht am meisten Sinn)
3 Stimmen
@LearnOpenGLES: Das tun sie.
1 Stimmen
Ich habe einen Artikel geschrieben über Erkennung von undefiniertem Verhalten in Ausdrücken die viele ähnliche Beispiele abdeckt, aber in Bezug auf die C++11-Regeln für die Sequenzierung definiert ist. Vielleicht ist es für einige der Leser hier nützlich.
1 Stimmen
Das erinnert mich an die Fragen, die viele Softwarefirmen in Indien bei Vorstellungsgesprächen stellen. Obwohl das Verhalten undefiniert ist, versuchen sie immer noch, der Ausgabe eine Logik aufzuerlegen. Ähnliche Fragen werden auch in vielen C-Büchern von Yashwant Kanetkar gestellt. Diese Art von Fragen machen mich wirklich krank :(
1 Stimmen
@LearnOpenGLES: Mein Compiler (gcc 4.8.1) warnt mich bei Konstrukten wie u=u++ & j=i++ + ++i;
2 Stimmen
Auch wenn es in dieser Frage um
C
Es könnte für einige Aspekte von Interesse sein, dass sich dies mit der nächsten Version vonC++
mit den gewählten Garantien für die C++17-Auswertungsreihenfolge (P0145R2) Mehr: stackoverflow.com/questions/38501587/0 Stimmen
Wie in einigen Kommentaren erwähnt, gibt es in C / C++ keine expliziten Regeln für die Reihenfolge der Auswertung. Der ungewöhnlichste Fall ist APL (A Programming Language), die Ausdrücke von rechts nach links auswertet (was mehrere Zuweisungen in einer einzigen Zeile ermöglicht), wobei Klammern verwendet werden, um die Reihenfolge der Auswertung außer Kraft zu setzen.
0 Stimmen
C-Compiler liefern andere Ergebnisse als Java-Compiler:
int i=5; System.out.printf(",%d,%d,%d,%d,%d",i++,i--,++i,--i,i);
gcc 5.3.0: Output: 4,5,5,5,5,5 Java1.8 Ausgabe: 5,6,6,5,50 Stimmen
@i_am_zero: Die Tatsache, dass der Standard ein Verhalten in einer bestimmten Situation nicht vorschreibt, bedeutet nicht, dass keine Implementierungen so detailliert spezifizieren, wie sie den Code verarbeiten, dass nur ein mögliches Verhalten mit der Spezifikation vereinbar ist. Ein Problem des Standards ist, dass er nie versucht hat, alle Fälle zu katalogisieren, in denen eine Implementierung einen Umweg gehen müsste pas sich in vorhersehbarer Weise zu verhalten (z. B. durch
memcpy
in Fällen, in denen Quelle und Ziel gelegentlich gleich sind, z.B. weil die Kosten einer gelegentlichen redundanten Kopie geringer sind als...0 Stimmen
...die Kosten für die Kontrolle von jede Operation, ob die Kopie notwendig war). IMHO wäre der Standard besser, wenn er ein grundlegendes Ausführungsmodell spezifizieren würde und dann die Arten von Optimierungen, die Programmierer aktivieren können. Gegeben
x=(*p)++ + (*q)++; b=*p; c=*p;
Zum Beispiel kann es vernünftig sein, zu sagen, dass ein Compiler, wenn einige Optimierungen aktiviert sind, nach eigenem Ermessen Folgendes behandeln kannb
yc
entweder die Eins plus den Wert, der vor dem Inkrement von*p
oder einen Wert enthalten, der aus*p
zu einem beliebigen Zeitpunkt zwischen der Erhöhung und der Zuordnung zub
oc
.0 Stimmen
@i_am_zero: Solche Regeln würden den Compilern fast die gesamte nützliche Flexibilität geben, die sie unter dem gegenwärtigen Standard haben, aber wenn sie mit Möglichkeiten kombiniert werden, unbestimmte Werte in beliebige Werte zu konvertieren, könnten einige Arten von Code effizienter geschrieben werden, als es derzeit möglich ist.