#include <stdio.h>
int main(void)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 3
i = 1;
i = (i++);
printf("%d\n", i); // 2 Should be 1, no ?
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 1
u = 1;
u = (u++);
printf("%d\n", u); // 2 Should also be one, no ?
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); // 3 (Should be the same as u ?)
int w = 0;
printf("%d %d\n", ++w, w); // shouldn't this print 1 1
int x[2] = { 5, 8 }, y = 0;
x[y] = y ++;
printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
814
(i++)
immer noch zu 1, unabhängig von Klammerni = (i++);
immer beabsichtigt war, es gibt sicherlich einen klareren Weg, es zu schreiben. Das wäre wahr, selbst wenn es gut definiert wäre. Selbst in Java, das das Verhalten von definierti = (i++);
, ist es immer noch schlechter Code. Schreiben Sie einfachi++;
Antworten:
C hat das Konzept eines undefinierten Verhaltens, dh einige Sprachkonstrukte sind syntaktisch gültig, aber Sie können das Verhalten nicht vorhersagen, wenn der Code ausgeführt wird.
Soweit ich weiß, sagt der Standard nicht explizit aus, warum das Konzept des undefinierten Verhaltens existiert. In meinen Augen ist es einfach so, dass die Sprachdesigner einen gewissen Spielraum in der Semantik haben wollten, anstatt zu verlangen, dass alle Implementierungen den Integer-Überlauf genauso behandeln, was sehr wahrscheinlich ernsthafte Leistungskosten verursachen würde. Sie haben das Verhalten einfach verlassen undefiniert, sodass beim Schreiben von Code, der einen Ganzzahlüberlauf verursacht, alles passieren kann.
Warum sind diese "Probleme" in diesem Sinne? Die Sprache sagt deutlich, dass bestimmte Dinge zu undefiniertem Verhalten führen . Es gibt kein Problem, es gibt kein "sollte". Wenn sich das undefinierte Verhalten ändert, wenn eine der beteiligten Variablen deklariert
volatile
wird, beweist oder ändert dies nichts. Es ist undefiniert ; Sie können nicht über das Verhalten argumentieren.Ihr interessantestes Beispiel, das mit
ist ein Lehrbuchbeispiel für undefiniertes Verhalten (siehe Wikipedia-Eintrag zu Sequenzpunkten ).
quelle
i = ++i + 1;
.Kompilieren und zerlegen Sie einfach Ihre Codezeile, wenn Sie wissen möchten, wie genau Sie das bekommen, was Sie bekommen.
Das bekomme ich auf meine Maschine, zusammen mit dem, was ich denke:
(Ich ... nehme an, dass der Befehl 0x00000014 eine Art Compileroptimierung war?)
quelle
gcc evil.c -c -o evil.bin
undgdb evil.bin
→disassemble evil
, oder was auch immer die Windows-Entsprechungen davon sind :)Why are these constructs undefined behavior?
.gcc -S evil.c
) zu kompilieren , was alles ist, was hier benötigt wird. Das Zusammenbauen und dann das Zerlegen ist nur ein Umweg.Ich denke, die relevanten Teile des C99-Standards sind 6.5 Ausdrücke, §2
und 6.5.16 Zuweisungsoperatoren, §4:
quelle
i=i=5
ist auch undefiniertes VerhaltenA=B=5;
"Schreibsperre A; Schreibsperre B; Speichern von 5 bis A; Speichern von 5 bis B; Entsperren von B" implementieren könnte ; A entsperren; "und eine Anweisung wieC=A+B;
" Lesesperre A; Lesesperre B; Berechnung A + B; A und B entsperren; Schreibsperre C; Ergebnis speichern; C entsperren; ". Dies würde sicherstellen, dass, wenn ein Thread dies tat,A=B=5;
während ein anderer dies tat,C=A+B;
der letztere Thread entweder beide Schreibvorgänge als stattgefunden ansah oder keinen. Möglicherweise eine nützliche Garantie. Wenn ein ThreadI=I=5;
jedoch ...Die meisten der hier zitierten Antworten aus dem C-Standard betonen, dass das Verhalten dieser Konstrukte undefiniert ist. Um zu verstehen, warum das Verhalten dieser Konstrukte undefiniert ist, verstehen wir diese Begriffe zunächst im Lichte des C11-Standards:
Sequenziert: (5.1.2.3)
Nicht sequenziert:
Bewertungen können eines von zwei Dingen sein:
Sequenzpunkt:
Kommen wir nun zu der Frage, für die Ausdrücke wie
Standard sagt, dass:
6.5 Ausdrücke:
Daher ruft der obige Ausdruck UB auf, da zwei Nebenwirkungen auf dasselbe Objekt
i
relativ zueinander nicht sequenziert sind. Das heißt, es wird nicht sequenziert, ob die Nebenwirkung durch Zuweisung ani
vor oder nach der Nebenwirkung durch erfolgt++
.Abhängig davon, ob die Zuweisung vor oder nach dem Inkrement erfolgt, werden unterschiedliche Ergebnisse erzeugt, und dies ist der Fall bei undefiniertem Verhalten .
Benennen Sie das
i
links von der Zuweisung beil
und rechts von der Zuweisung (im Ausdrucki++
) be umir
, dann ist der Ausdruck wieEin wichtiger Punkt in Bezug auf den Postfix-
++
Operator ist:Dies bedeutet, dass der Ausdruck
il = ir++
entweder als bewertet werden kannoder
in zwei unterschiedlichen Ergebnissen resultierende
1
und2
davon abhängt , welche auf der Sequenz der Nebenwirkungen durch Zuordnung und++
und daher ruft UB.quelle
Das Verhalten kann nicht wirklich erklärt werden , da sie sowohl rufen nicht spezifiziert Verhalten und nicht definiertes Verhalten , so dass wir keine allgemeinen Aussagen über diesen Code machen, obwohl , wenn Sie lesen Olve Maudal der Arbeit wie Tief C und unspezifisch und undefiniert manchmal Sie gut machen Vermutungen in ganz bestimmten Fällen mit einem bestimmten Compiler und einer bestimmten Umgebung, aber bitte tun Sie dies nicht in der Nähe der Produktion.
So bewegend auf nicht näher bezeichnete Verhalten im Entwurf C99 - Standard Abschnitt
6.5
Absatz 3 sagt ( Hervorhebung von mir ):Wenn wir also eine Zeile wie diese haben:
Wir wissen nicht, ob wir zuerst bewertet werden
i++
oder++i
werden. Dies dient hauptsächlich dazu, dem Compiler bessere Optimierungsmöglichkeiten zu bieten .Wir haben auch nicht definiertes Verhalten auch hier , da die Programmvariablen werden modifiziert (
i
,u
, etc ..) mehr als einmal zwischen Sequenzpunkten . Aus dem Entwurf des Standardabschnitts6.5
Absatz 2 ( Schwerpunkt Mine ):Die folgenden Codebeispiele werden als undefiniert bezeichnet:
In all diesen Beispielen versucht der Code, ein Objekt mehr als einmal im selben Sequenzpunkt zu ändern, was
;
in jedem dieser Fälle mit dem folgenden endet :Nicht spezifiziertes Verhalten wird im Entwurf der c99-Norm im Abschnitt
3.4.4
wie folgt definiert :und undefiniertes Verhalten wird im Abschnitt definiert
3.4.3
als:und stellt fest, dass:
quelle
Eine andere Möglichkeit, dies zu beantworten, besteht darin, einfach zu fragen, was sie bedeuten sollen , anstatt sich in arkanen Details von Sequenzpunkten und undefiniertem Verhalten zu verlieren. Was versuchte der Programmierer zu tun?
Das erste Fragment, nach dem gefragt wurde
i = i++ + ++i
, ist in meinem Buch ziemlich offensichtlich verrückt. Niemand würde es jemals in ein reales Programm schreiben, es ist nicht offensichtlich, was es tut, es gibt keinen denkbaren Algorithmus, den jemand hätte versuchen können, zu codieren, der zu dieser bestimmten erfundenen Abfolge von Operationen geführt hätte. Und da es für Sie und mich nicht offensichtlich ist, was es tun soll, ist es in meinem Buch in Ordnung, wenn der Compiler auch nicht herausfinden kann, was es tun soll.Das zweite Fragment
i = i++
ist etwas leichter zu verstehen. Jemand versucht eindeutig, i zu erhöhen und das Ergebnis wieder i zuzuweisen. In C gibt es jedoch einige Möglichkeiten, dies zu tun. Die einfachste Möglichkeit, 1 zu i hinzuzufügen und das Ergebnis wieder i zuzuweisen, ist in fast jeder Programmiersprache dieselbe:C hat natürlich eine praktische Abkürzung:
Dies bedeutet, "addiere 1 zu i und ordne das Ergebnis wieder i zu". Wenn wir also ein Durcheinander der beiden konstruieren, schreiben wir
Was wir wirklich sagen, ist "addiere 1 zu i und weise das Ergebnis wieder i zu und weise das Ergebnis wieder i zu". Wir sind verwirrt, daher stört es mich nicht allzu sehr, wenn der Compiler auch verwirrt wird.
Realistisch gesehen werden diese verrückten Ausdrücke nur dann geschrieben, wenn die Leute sie als künstliche Beispiele dafür verwenden, wie ++ funktionieren soll. Und natürlich ist es wichtig zu verstehen, wie ++ funktioniert. Eine praktische Regel für die Verwendung von ++ lautet jedoch: "Wenn nicht klar ist, was ein Ausdruck mit ++ bedeutet, schreiben Sie ihn nicht."
Wir haben unzählige Stunden auf comp.lang.c verbracht, um solche Ausdrücke zu diskutieren und warum sie undefiniert sind. Zwei meiner längeren Antworten, die wirklich zu erklären versuchen, warum, sind im Internet archiviert:
Siehe auch Frage 3.8 und die restlichen Fragen in Abschnitt 3 der C-FAQ-Liste .
quelle
*p=(*q)++;
bedeutet ,if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;
dass nicht mehr der Fall. Hyper-modernes C würde das Schreiben von etwas wie der letzteren Formulierung erfordern (obwohl es keine Standardmethode gibt, um anzuzeigen, dass Code egal ist, was drin ist*p
), um die Effizienz zu erreichen, die Compiler für die erstere verwenden (dieelse
Klausel ist notwendig, um zuzulassen Der Compiler optimiert das,if
was einige neuere Compiler benötigen würden.assert
Anweisungen zu betrachten, damit der Programmierer der fraglichen Zeile eine einfache Zeile voranstellen kannassert(p != q)
. (Um diesen Kurs zu belegen, müsste natürlich auch neu geschrieben werden,<assert.h>
um Zusicherungen in Nicht-Debug-Versionen nicht sofort zu löschen, sondern sie in etwas zu verwandeln,__builtin_assert_disabled()
das der eigentliche Compiler sehen kann, und dann keinen Code für.)Oft wird diese Frage als Duplikat von Fragen verknüpft, die sich auf Code wie beziehen
oder
oder ähnliche Varianten.
Dies ist zwar auch ein undefiniertes Verhalten, wie bereits erwähnt, aber es gibt subtile Unterschiede, wenn
printf()
es um den Vergleich mit einer Aussage wie der folgenden geht:In der folgenden Aussage:
Die Reihenfolge der Bewertung der Argumente in
printf()
ist nicht angegeben . Das heißt, Ausdrückei++
und++i
können in beliebiger Reihenfolge ausgewertet werden. Der C11-Standard enthält einige relevante Beschreibungen dazu:Anhang J, nicht näher bezeichnetes Verhalten
3.4.4, nicht spezifiziertes Verhalten
Das nicht spezifizierte Verhalten selbst ist KEIN Problem. Betrachten Sie dieses Beispiel:
Auch dies hat ein nicht spezifiziertes Verhalten, da die Reihenfolge der Bewertung von
++x
undy++
nicht spezifiziert ist. Aber es ist eine vollkommen legale und gültige Aussage. Es gibt keine undefinierten Verhalten in dieser Aussage. Weil die Änderungen (++x
undy++
) an verschiedenen Objekten vorgenommen werden.Was macht die folgende Aussage
Als undefiniertes Verhalten gilt die Tatsache, dass diese beiden Ausdrücke dasselbe Objekt
i
ohne einen dazwischenliegenden Sequenzpunkt modifizieren .Ein weiteres Detail ist, dass das Komma , das am Aufruf von printf () beteiligt ist, ein Trennzeichen ist , nicht der Kommaoperator .
Dies ist eine wichtige Unterscheidung, da der Kommaoperator einen Sequenzpunkt zwischen der Auswertung seiner Operanden einführt , wodurch Folgendes zulässig ist:
Der Kommaoperator wertet seine Operanden von links nach rechts aus und gibt nur den Wert des letzten Operanden aus. So in
j = (++i, i++);
,++i
Schrittei
auf6
undi++
Ausbeuten alter Wert voni
(6
) , die zugeordnet istj
. Danni
wird7
aufgrund von Nachinkrement.Wenn also das Komma im Funktionsaufruf ein Kommaoperator sein sollte, dann
wird kein Problem sein. Es ruft jedoch undefiniertes Verhalten auf, da das Komma hier ein Trennzeichen ist .
Für diejenigen, die mit undefiniertem Verhalten noch nicht vertraut sind, würde es von Vorteil sein, zu lesen, was jeder C-Programmierer über undefiniertes Verhalten wissen sollte , um das Konzept und viele andere Varianten von undefiniertem Verhalten in C zu verstehen.
Dieser Beitrag: Undefiniertes, nicht spezifiziertes und implementierungsdefiniertes Verhalten ist ebenfalls relevant.
quelle
int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));
scheint ein stabiles Verhalten zu ergeben (Bewertung der Argumente von rechts nach links in gcc v7.3.0; Ergebnis "a = 110 b = 40 c = 60"). Liegt es daran, dass die Zuweisungen als "vollständige Anweisungen" betrachtet werden und somit einen Sequenzpunkt einführen? Sollte dies nicht zu einer Bewertung der Argumente / Aussagen von links nach rechts führen? Oder ist es nur eine Manifestation von undefiniertem Verhalten?b
undc
in 3. und 4. Argument und lesen in 2. Argument. Es gibt jedoch keine Reihenfolge zwischen diesen Ausdrücken (2., 3. und 4. Argument). gcc / clang hat eine Option, mit-Wsequence-point
der auch diese gefunden werden können.Während es unwahrscheinlich ist, dass Compiler und Prozessoren dies tatsächlich tun, wäre es nach dem C-Standard legal, dass der Compiler "i ++" mit der folgenden Reihenfolge implementiert:
Ich glaube zwar nicht, dass Prozessoren die Hardware unterstützen, damit so etwas effizient ausgeführt werden kann, aber man kann sich leicht Situationen vorstellen, in denen ein solches Verhalten Multithread-Code einfacher machen würde (z. B. würde dies garantieren, dass zwei Threads versuchen, das oben Genannte auszuführen Sequenz gleichzeitig,
i
würde um zwei erhöht werden) und es ist nicht völlig unvorstellbar, dass ein zukünftiger Prozessor eine solche Funktion bereitstellen könnte.Wenn der Compiler
i++
wie oben angegeben schreiben sollte (gemäß dem Standard legal) und die obigen Anweisungen während der Bewertung des Gesamtausdrucks (auch legal) verteilen sollte, und wenn er nicht bemerkte, dass eine der anderen Anweisungen auftrat Für den Zugriffi
wäre es dem Compiler möglich (und legal), eine Folge von Anweisungen zu generieren, die zum Stillstand kommen würden. Natürlich würde ein Compiler das Problem mit ziemlicher Sicherheit erkenneni
, wenn an beiden Stellen dieselbe Variable verwendet wird, eine Routine jedoch Verweise auf zwei Zeigerp
undq
und und verwendet(*p)
und(*q)
im obigen Ausdruck (anstatt zu verwenden) akzeptierti
zweimal) Der Compiler müsste den Deadlock nicht erkennen oder vermeiden, der auftreten würde, wenn die Adresse desselben Objekts für beidep
und übergeben würdeq
.quelle
Während die Syntax der Ausdrücke wie
a = a++
odera++ + a++
legal ist, ist das Verhalten dieser Konstrukte undefiniert, da ein Soll im C-Standard nicht eingehalten wird. C99 6.5p2 :Mit Fußnote 73 wird dies weiter klargestellt
Die verschiedenen Sequenzpunkte sind in Anhang C von C11 (und C99 ) aufgeführt:
Der Wortlaut desselben Absatzes in C11 lautet:
Sie können solche Fehler in einem Programm erkennen, indem Sie beispielsweise eine neuere Version von GCC mit
-Wall
und verwenden-Werror
, und dann wird GCC die Kompilierung Ihres Programms sofort ablehnen. Das Folgende ist die Ausgabe von gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:Der wichtige Teil ist zu wissen, was ein Sequenzpunkt ist - und was ein Sequenzpunkt ist und was nicht . Zum Beispiel ist der Kommaoperator ein Sequenzpunkt
ist gut definiert und erhöht sich
i
um eins, was den alten Wert ergibt. Verwerfen Sie diesen Wert. Beheben Sie dann beim Komma-Operator die Nebenwirkungen. und danni
um eins inkrementieren , und der resultierende Wert wird zum Wert des Ausdrucks - dh dies ist nur eine erfundene Schreibweise,j = (i += 2)
die wiederum eine "clevere" Schreibweise istDie
,
In-Function-Argumentlisten sind jedoch kein Kommaoperator, und es gibt keinen Sequenzpunkt zwischen Auswertungen verschiedener Argumente. stattdessen sind ihre Bewertungen in Bezug aufeinander nicht sequenziert; also der Funktionsaufrufhat ein undefiniertes Verhalten, da zwischen den Auswertungen von
i++
und++i
in Funktionsargumenten kein Sequenzpunkt vorhanden ist und der Wert voni
daher zweimal von beideni++
und++i
zwischen dem vorherigen und dem nächsten Sequenzpunkt geändert wird.quelle
Der C-Standard besagt, dass eine Variable höchstens einmal zwischen zwei Sequenzpunkten zugewiesen werden sollte. Ein Semikolon ist beispielsweise ein Sequenzpunkt.
Also jede Aussage des Formulars:
und so weiter gegen diese Regel verstoßen. Der Standard sagt auch, dass das Verhalten undefiniert und nicht nicht spezifiziert ist. Einige Compiler erkennen diese und liefern ein Ergebnis, dies entspricht jedoch nicht dem Standard.
Es können jedoch zwei verschiedene Variablen zwischen zwei Sequenzpunkten inkrementiert werden.
Das Obige ist eine gängige Codierungspraxis beim Kopieren / Analysieren von Zeichenfolgen.
quelle
In /programming/29505280/incrementing-array-index-in-c fragte jemand nach einer Aussage wie:
welches 7 druckt ... das OP erwartete, dass es 6 druckt.
Es
++i
ist nicht garantiert, dass die Inkremente vor dem Rest der Berechnungen vollständig sind. Tatsächlich erhalten verschiedene Compiler hier unterschiedliche Ergebnisse. Im Beispiel Sie bereitgestellt, wobei die ersten 2++i
ausgeführt, dann werden die Wertek[]
gelesen wurden, dann ist die letzte++i
dannk[]
.Moderne Compiler werden dies sehr gut optimieren. In der Tat möglicherweise besser als der Code, den Sie ursprünglich geschrieben haben (vorausgesetzt, er hat so funktioniert, wie Sie es sich erhofft hatten).
quelle
Eine gute Erklärung darüber, was bei dieser Art von Berechnung passiert, finden Sie im Dokument n1188 auf der ISO W14-Site .
Ich erkläre die Ideen.
Die Hauptregel aus der Norm ISO 9899, die in dieser Situation gilt, ist 6.5p2.
Die Sequenzpunkte in einem Ausdruck wie
i=i++
sind vorheri=
und nachheri++
.In dem oben zitierten Artikel wird erklärt, dass Sie herausfinden können, dass das Programm aus kleinen Kästchen besteht, wobei jedes Kästchen die Anweisungen zwischen zwei aufeinanderfolgenden Sequenzpunkten enthält. Die Sequenzpunkte sind in Anhang C des Standards definiert, im Fall von
i=i++
2 Sequenzpunkten, die einen vollständigen Ausdruck begrenzen. Ein solcher Ausdruck ist syntaktisch äquivalent zu einem Eintragexpression-statement
in der Backus-Naur-Form der Grammatik (eine Grammatik ist in Anhang A des Standards enthalten).Die Reihenfolge der Anweisungen in einer Box ist also nicht eindeutig.
kann interpretiert werden als
oder als
Da beide Formulare zur Interpretation des Codes
i=i++
gültig sind und beide unterschiedliche Antworten generieren, ist das Verhalten undefiniert.Ein Sequenzpunkt ist also am Anfang und am Ende jeder Box zu sehen, aus der das Programm besteht [die Boxen sind atomare Einheiten in C], und innerhalb einer Box ist die Reihenfolge der Anweisungen nicht in allen Fällen definiert. Wenn man diese Reihenfolge ändert, kann man manchmal das Ergebnis ändern.
BEARBEITEN:
Eine weitere gute Quelle für die Erklärung solcher Unklarheiten sind die Einträge von der c-faq- Site (auch als Buch veröffentlicht ), nämlich hier und hier und hier .
quelle
i=i++
sind dieser Antwort sehr ähnlich .Ihre Frage war wahrscheinlich nicht "Warum sind diese Konstrukte undefiniertes Verhalten in C?". Ihre Frage war wahrscheinlich: "Warum hat mir dieser Code (mit
++
) nicht den erwarteten Wert gegeben?", Und jemand hat Ihre Frage als Duplikat markiert und Sie hierher geschickt.Diese Antwort versucht, diese Frage zu beantworten: Warum hat Ihr Code Ihnen nicht die erwartete Antwort gegeben, und wie können Sie lernen, Ausdrücke zu erkennen (und zu vermeiden), die nicht wie erwartet funktionieren.
Ich gehe davon aus, dass Sie inzwischen die grundlegende Definition von Cs
++
und--
Operatoren gehört haben und wie++x
sich das Präfixformular vom Postfixformular unterscheidetx++
. Aber diese Operatoren sind schwer zu überlegen. Um sicherzugehen, dass Sie verstanden haben, haben Sie vielleicht ein kleines Testprogramm geschrieben, das so etwas beinhaltetZu Ihrer Überraschung hat Ihnen dieses Programm jedoch nicht geholfen, es zu verstehen - es druckte eine seltsame, unerwartete, unerklärliche Ausgabe, was darauf hindeutet, dass möglicherweise
++
etwas völlig anderes funktioniert, als Sie gedacht haben.Oder vielleicht sehen Sie einen schwer verständlichen Ausdruck wie
Vielleicht hat dir jemand diesen Code als Puzzle gegeben. Dieser Code macht auch keinen Sinn, besonders wenn Sie ihn ausführen - und wenn Sie ihn kompilieren und unter zwei verschiedenen Compilern ausführen, erhalten Sie wahrscheinlich zwei verschiedene Antworten! Was ist damit? Welche Antwort ist richtig? (Und die Antwort ist, dass beide oder keiner von ihnen sind.)
Wie Sie inzwischen gehört haben, sind alle diese Ausdrücke undefiniert , was bedeutet, dass die C-Sprache keine Garantie dafür gibt, was sie tun werden. Dies ist ein seltsames und überraschendes Ergebnis, da Sie wahrscheinlich dachten, dass jedes Programm, das Sie schreiben könnten, solange es kompiliert und ausgeführt wird, eine eindeutige, genau definierte Ausgabe erzeugen würde. Bei undefiniertem Verhalten ist dies jedoch nicht der Fall.
Was macht einen Ausdruck undefiniert? Sind Ausdrücke involviert
++
und--
immer undefiniert? Natürlich nicht: Dies sind nützliche Operatoren, und wenn Sie sie richtig verwenden, sind sie perfekt definiert.Bei den Ausdrücken, über die wir sprechen, sind sie undefiniert, wenn zu viel auf einmal passiert, wenn wir nicht sicher sind, in welcher Reihenfolge die Dinge passieren werden, aber wenn die Reihenfolge für das Ergebnis von Bedeutung ist, erhalten wir sie.
Kehren wir zu den beiden Beispielen zurück, die ich in dieser Antwort verwendet habe. Als ich schrieb
Die Frage ist,
printf
berechnet der Compiler vor dem Aufruf den Wert vonx
first oderx++
oder oder++x
? Aber es stellt sich heraus, dass wir es nicht wissen . In C gibt es keine Regel, die besagt, dass die Argumente für eine Funktion von links nach rechts, von rechts nach links oder in einer anderen Reihenfolge ausgewertet werden. So können wir nicht sagen , ob der Compiler tun wirdx
zuerst, dann++x
, dannx++
, oderx++
dann++x
dannx
oder eine andere Reihenfolge. Die Reihenfolge ist jedoch eindeutig von Bedeutung, da je nach der vom Compiler verwendeten Reihenfolge eindeutig unterschiedliche Ergebnisse gedruckt werdenprintf
.Was ist mit diesem verrückten Ausdruck?
Das Problem mit diesem Ausdruck besteht darin, dass er drei verschiedene Versuche enthält, den Wert von x zu ändern: (1) Der
x++
Teil versucht, x 1 hinzuzufügen, den neuen Wert in zu speichernx
und den alten Wert von zurückzugebenx
. (2) der++x
Teil versucht, 1 zu x hinzuzufügen, den neuen Wert in zu speichernx
und den neuen Wert von zurückzugebenx
; und (3) derx =
Teil versucht, die Summe der beiden anderen zurück zu x zuzuweisen. Welche dieser drei versuchten Aufgaben wird "gewinnen"? Welchem der drei Werte wird tatsächlich zugewiesenx
? Wiederum und vielleicht überraschend, gibt es in C keine Regel, die es uns sagt.Sie können sich vorstellen, dass Vorrang oder Assoziativität oder Bewertung von links nach rechts Ihnen sagen, in welcher Reihenfolge die Dinge passieren, aber nicht. Sie mögen mir nicht glauben, aber bitte nehmen Sie mein Wort dafür, und ich sage es noch einmal: Vorrang und Assoziativität bestimmen nicht jeden Aspekt der Bewertungsreihenfolge eines Ausdrucks in C. Insbesondere, wenn es innerhalb eines Ausdrucks mehrere gibt Verschiedene Stellen, an denen wir versuchen, etwas wie
x
Vorrang und Assoziativität einen neuen Wert zuzuweisen , sagen uns nicht , welcher dieser Versuche zuerst oder zuletzt oder irgendetwas passiert.Wenn Sie also sicherstellen möchten, dass alle Ihre Programme genau definiert sind, welche Ausdrücke können Sie schreiben und welche nicht?
Diese Ausdrücke sind alle in Ordnung:
Diese Ausdrücke sind alle undefiniert:
Und die letzte Frage ist, wie können Sie feststellen, welche Ausdrücke gut definiert und welche nicht definiert sind?
Wie ich bereits sagte, sind die undefinierten Ausdrücke diejenigen, bei denen zu viel auf einmal passiert, bei denen Sie nicht sicher sein können, in welcher Reihenfolge die Dinge passieren und wo die Reihenfolge wichtig ist:
Als Beispiel für # 1 im Ausdruck
Es gibt drei Versuche, `x zu ändern.
Als Beispiel für # 2 im Ausdruck
Wir beide verwenden den Wert von
x
und ändern ihn.Das ist also die Antwort: Stellen Sie sicher, dass in jedem Ausdruck, den Sie schreiben, jede Variable höchstens einmal geändert wird. Wenn eine Variable geändert wird, versuchen Sie nicht, den Wert dieser Variablen auch an einer anderen Stelle zu verwenden.
quelle
Der Grund ist, dass das Programm undefiniertes Verhalten ausführt. Das Problem liegt in der Auswertungsreihenfolge, da gemäß C ++ 98-Standard keine Sequenzpunkte erforderlich sind (gemäß C ++ 11-Terminologie werden keine Operationen vor oder nach einer anderen sequenziert).
Wenn Sie sich jedoch an einen Compiler halten, wird das Verhalten dauerhaft sein, solange Sie keine Funktionsaufrufe oder Zeiger hinzufügen, was das Verhalten unübersichtlicher machen würde.
Also zuerst das GCC: Mit Nuwen MinGW 15 GCC 7.1 erhalten Sie:
}}
Wie funktioniert GCC? Es wertet Unterausdrücke in einer Reihenfolge von links nach rechts für die rechte Seite (RHS) aus und weist den Wert dann der linken Seite (LHS) zu. Genau so verhalten sich Java und C # und definieren ihre Standards. (Ja, die entsprechende Software in Java und C # hat Verhaltensweisen definiert.) Es bewertet jeden Unterausdruck einzeln in der RHS-Anweisung in einer Reihenfolge von links nach rechts. für jeden Unterausdruck: Das ++ c (Vorinkrement) wird zuerst ausgewertet, dann wird der Wert c für die Operation verwendet, dann das Nachinkrement c ++).
gemäß GCC C ++: Operatoren
Der äquivalente Code in definiertem Verhalten C ++, wie GCC versteht:
Dann gehen wir zu Visual Studio . Visual Studio 2015 erhalten Sie:
Wie funktioniert Visual Studio? Es wird ein anderer Ansatz gewählt. Es wertet alle Vorinkrement-Ausdrücke im ersten Durchgang aus, verwendet dann Variablenwerte in den Operationen im zweiten Durchgang, weist im dritten Durchgang von RHS zu LHS zu und wertet im letzten Durchgang alle aus Post-Inkrement-Ausdrücke in einem Durchgang.
Das Äquivalent in definiertem Verhalten C ++ wie Visual C ++ versteht also:
Wie in der Visual Studio-Dokumentation unter Vorrang und Reihenfolge der Auswertung angegeben :
quelle