Betrachten Sie diesen Code:
int i = 1;
int x = ++i + ++i;
Wir haben einige Vermutungen, was ein Compiler für diesen Code tun könnte, vorausgesetzt, er wird kompiliert.
- beide
++i
kehren zurück2
, was zux=4
. - einer
++i
kehrt zurück2
und der andere kehrt zurück3
, was zux=5
. - beide
++i
kehren zurück3
, was zux=6
.
Der zweite scheint mir am wahrscheinlichsten. Einer der beiden ++
Operatoren wird mit ausgeführt i = 1
, der i
wird inkrementiert und das Ergebnis 2
zurückgegeben. Dann wird der zweite ++
Operator mit ausgeführt i = 2
, der i
wird inkrementiert und das Ergebnis 3
zurückgegeben. Dann 2
und 3
werden addiert, um zu geben 5
.
Ich habe diesen Code jedoch in Visual Studio ausgeführt und das Ergebnis war 6
. Ich versuche, Compiler besser zu verstehen, und ich frage mich, was möglicherweise zu einem Ergebnis führen könnte 6
. Ich vermute nur, dass der Code mit einer "eingebauten" Parallelität ausgeführt werden könnte. Die beiden ++
Operatoren wurden aufgerufen, wobei jeder inkrementiert wurde, i
bevor der andere zurückkehrte, und dann kehrten beide zurück 3
. Dies würde meinem Verständnis des Aufrufstapels widersprechen und müsste weg erklärt werden.
Welche (vernünftigen) Dinge könnte ein C++
Compiler tun, die zu einem Ergebnis 4
oder einem Ergebnis führen würden oder 6
?
Hinweis
Dieses Beispiel erschien als Beispiel für undefiniertes Verhalten in Bjarne Stroustrups Programmierung: Prinzipien und Praxis mit C ++ (C ++ 14).
Siehe Zimt Kommentar .
quelle
Antworten:
Der Compiler nimmt Ihren Code, teilt ihn in sehr einfache Anweisungen auf und kombiniert und ordnet sie dann so an, dass er sie für optimal hält.
Der Code
int i = 1; int x = ++i + ++i;
besteht aus folgenden Anweisungen:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 5. read i as tmp2 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 9. read i as tmp4 10. add tmp2 and tmp4, as tmp5 11. store tmp5 in x
Aber obwohl dies eine nummerierte Liste ist, wie ich sie geschrieben habe, gibt es hier nur wenige Ordnungsabhängigkeiten : 1-> 2-> 3-> 4-> 5-> 10-> 11 und 1-> 6-> 7- > 8-> 9-> 10-> 11 müssen in ihrer relativen Reihenfolge bleiben. Ansonsten kann der Compiler frei neu ordnen und möglicherweise Redundanz beseitigen.
Sie können die Liste beispielsweise folgendermaßen bestellen:
1. store 1 in i 2. read i as tmp1 6. read i as tmp3 3. add 1 to tmp1 7. add 1 to tmp3 4. store tmp1 in i 8. store tmp3 in i 5. read i as tmp2 9. read i as tmp4 10. add tmp2 and tmp4, as tmp5 11. store tmp5 in x
Warum kann der Compiler das tun? Weil es keine Sequenzierung zu den Nebenwirkungen des Inkrements gibt. Aber jetzt kann der Compiler vereinfachen: Zum Beispiel gibt es in 4 einen toten Speicher: Der Wert wird sofort überschrieben. Auch tmp2 und tmp4 sind wirklich dasselbe.
1. store 1 in i 2. read i as tmp1 6. read i as tmp3 3. add 1 to tmp1 7. add 1 to tmp3 8. store tmp3 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
Und jetzt ist alles, was mit tmp1 zu tun hat, toter Code: Er wird nie verwendet. Und das erneute Lesen von i kann auch beseitigt werden:
1. store 1 in i 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 10. add tmp3 and tmp3, as tmp5 11. store tmp5 in x
Schauen Sie, dieser Code ist viel kürzer. Der Optimierer freut sich. Der Programmierer ist nicht, weil ich nur einmal erhöht wurde. Hoppla.
Schauen wir uns stattdessen etwas anderes an, das der Compiler tun kann: Kehren wir zur Originalversion zurück.
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 5. read i as tmp2 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 9. read i as tmp4 10. add tmp2 and tmp4, as tmp5 11. store tmp5 in x
Der Compiler könnte es folgendermaßen neu anordnen:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 5. read i as tmp2 9. read i as tmp4 10. add tmp2 and tmp4, as tmp5 11. store tmp5 in x
und dann wieder bemerken, dass ich zweimal gelesen werde, also eliminiere einen von ihnen:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 6. read i as tmp3 7. add 1 to tmp3 8. store tmp3 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
Das ist schön, aber es kann noch weiter gehen: Es kann tmp1 wiederverwenden:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 6. read i as tmp1 7. add 1 to tmp1 8. store tmp1 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
Dann kann das erneute Lesen von i in 6 beseitigt werden:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 4. store tmp1 in i 7. add 1 to tmp1 8. store tmp1 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
Jetzt ist 4 ein toter Laden:
1. store 1 in i 2. read i as tmp1 3. add 1 to tmp1 7. add 1 to tmp1 8. store tmp1 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
und jetzt können 3 und 7 zu einer Anweisung zusammengeführt werden:
1. store 1 in i 2. read i as tmp1 3+7. add 2 to tmp1 8. store tmp1 in i 5. read i as tmp2 10. add tmp2 and tmp2, as tmp5 11. store tmp5 in x
Beseitigen Sie die letzte temporäre:
1. store 1 in i 2. read i as tmp1 3+7. add 2 to tmp1 8. store tmp1 in i 10. add tmp1 and tmp1, as tmp5 11. store tmp5 in x
Und jetzt erhalten Sie das Ergebnis, das Visual C ++ Ihnen liefert.
Beachten Sie, dass in beiden Optimierungspfaden die wichtigen Ordnungsabhängigkeiten beibehalten wurden, sofern die Anweisungen nicht entfernt wurden, um nichts zu tun.
quelle
Während dies UB ist (wie das OP impliziert), folgen hypothetische Möglichkeiten, wie ein Compiler die 3 Ergebnisse erhalten könnte. Alle drei würden das gleiche korrekte
x
Ergebnis liefern, wenn sie mit unterschiedlichenint i = 1, j = 1;
Variablen anstelle ein und derselben verwendet würdeni
.int i = 1; int i1 = i, i2 = i; // i1 = i2 = 1 ++i1; // i1 = 2 ++i2; // i2 = 2 int x = i1 + i2; // x = 4
int i = 1; int i1 = ++i; // i1 = 2 int i2 = ++i; // i2 = 3 int x = i1 + i2; // x = 5
int i = 1; int &i1 = i, &i2 = i; ++i1; // i = 2 ++i2; // i = 3 int x = i1 + i2; // x = 6
quelle
i
. Zu wissen, dass es nur einmal passieren kann, gibt es nur einmal aus. Bei Option 2 wird der Code buchstäblich in Maschinencode übersetzt, wie dies bei einem College-Compiler-Klassenprojekt der Fall sein könnte. Bei Option 3 ist es wie bei Option 1, es wurden jedoch zwei Kopien des Vorinkrements erstellt. Muss einen Vektor verwendet haben, keine Menge. :-)Ich werde Option 4
++i
wählen : Beide passieren gleichzeitig.Neuere Prozessoren bewegen sich in Richtung einiger interessanter Optimierungen, und die parallele Code-Auswertung, wo dies wie hier zulässig ist, ist eine weitere Möglichkeit, wie Compiler immer schnelleren Code erstellen. Ich sehe als praktische Implementierung Compiler, die sich in Richtung Parallelität bewegen.
Ich konnte leicht einen Race-Zustand erkennen, der nicht deterministisches Verhalten oder einen Busfehler aufgrund derselben Speicherkonflikte verursachte - alles erlaubt, da der Codierer gegen den C ++ - Vertrag verstieß - daher UB.
Es könnte , aber nicht darin zählen.
Verwenden
++i + ++i
oder erwarten Sie keine vernünftigen Ergebnisse.quelle
i
zwei Registern zuweisen , beide Register inkrementieren und beide zurückschreiben. Der Prozessor hat keine Möglichkeit, dies zu beheben. Das grundlegende Problem ist, dass weder C ++ noch moderne CPUs streng sequentiell sind. C ++ verfügt explizit über die Sequenzierung "Vorher passiert" und "Nachher passiert", um standardmäßig ein "zur gleichen Zeit" zu ermöglichen.4
oder5
- abhängig vom Ausführungsmodell / der Geschwindigkeit des CPU-Threads, also ist dies UB im Herzen.Ich denke, dass eine einfache und unkomplizierte Interpretation (ohne ein Angebot für Compiler-Optimierungen oder Multithreading) nur wäre:
i
i
i
+ Hinzufügeni
Bei
i
zweimaliger Inkrementierung beträgt der Wert 3, und wenn er addiert wird, beträgt die Summe 6.Betrachten Sie dies zur Überprüfung als C ++ - Funktion:
int dblInc () { int i = 1; int x = ++i + ++i; return x; }
Hier ist der Assembler-Code, den ich beim Kompilieren dieser Funktion mit einer alten Version des GNU C ++ - Compilers (win32, gcc Version 3.4.2 (mingw-special)) erhalte. Hier finden keine ausgefallenen Optimierungen oder Multithreading statt:
__Z6dblIncv: push ebp mov ebp, esp sub esp, 8 mov DWORD PTR [ebp-4], 1 lea eax, [ebp-4] inc DWORD PTR [eax] lea eax, [ebp-4] inc DWORD PTR [eax] mov eax, DWORD PTR [ebp-4] add eax, DWORD PTR [ebp-4] mov DWORD PTR [ebp-8], eax mov eax, DWORD PTR [ebp-8] leave ret
Beachten Sie, dass sich die lokale Variable
i
nur an einer einzigen Stelle auf dem Stapel befindet: Adresse[ebp-4]
. Dieser Speicherort wird zweimal inkrementiert (in der 5.-8. Zeile der Assembly-Funktion; einschließlich scheinbar redundanter Lasten dieser Adresse ineax
). In der 9. bis 10. Zeile wird dieser Wert dann geladeneax
und dann hinzugefügteax
(dh der Strom wird berechneti + i
). Dann wird es redundant auf den Stapel und zurück aufeax
den Rückgabewert kopiert (der offensichtlich 6 sein wird).Es kann von Interesse sein, sich den C ++ - Standard (hier einen alten: ISO / IEC 14882: 1998 (E)) anzusehen, der für Ausdrücke Abschnitt 5.4 besagt:
Mit der Fußnote:
An dieser Stelle werden zwei Beispiele für nicht angegebenes Verhalten angegeben, an denen beide den Inkrementoperator beteiligt sind (eines davon ist :)
i = ++i + 1
.Wenn man möchte, kann man: eine Ganzzahl-Wrapper-Klasse erstellen (wie eine Java-Ganzzahl); Überladungsfunktionen
operator+
undoperator++
solche, dass sie die Zwischenwertobjekte zurückgeben; und damit schreiben++iObj + ++iObj
und veranlassen, dass ein Objekt mit 5 zurückgegeben wird. (Der Kürze halber habe ich hier keinen vollständigen Code eingefügt.)Persönlich wäre ich fasziniert, wenn es ein Beispiel für einen bekannten Compiler gäbe, der die Arbeit anders als in der oben gezeigten Reihenfolge erledigt hat. Es scheint mir, dass die einfachste Implementierung darin besteht, nur zwei Assembler-Codes
inc
für den primitiven Typ auszuführen, bevor die Additionsoperation ausgeführt wird.quelle
Das Vernünftige, was ein Compiler tun kann, ist Common Subexpression Elimination. Dies ist bereits eine häufige Optimierung in Compilern: Wenn ein Unterausdruck wie
(x+1)
in einem größeren Ausdruck mehr als einmal vorkommt, muss er nur einmal berechnet werden. Zum Beispiel ina/(x+1) + b*(x+1)
demx+1
Unterausdruck kann einmal berechnet werden.Natürlich muss der Compiler wissen, welche Unterausdrücke auf diese Weise optimiert werden können. Zweimaliges
rand()
Anrufen sollte zwei Zufallszahlen ergeben. Nicht inline Funktionsaufrufe müssen daher von CSE ausgenommen sein. Wie Sie bemerken, gibt es keine Regel, die besagt, wie zwei Vorkommen von behandelti++
werden sollen, daher gibt es keinen Grund, sie von CSE auszunehmen.Das Ergebnis kann tatsächlich sein, dass
int x = ++i + ++i;
optimiert istint __cse = i++; int x = __cse << 1
. (CSE, gefolgt von wiederholter Festigkeitsreduzierung)quelle
In der Praxis rufen Sie undefiniertes Verhalten auf. Alles kann passieren, nicht nur Dinge , die man „vernünftig“ betrachten, und oft Dinge tun passieren , dass man nicht für angemessen halten. Alles ist per Definition "vernünftig".
Eine sehr vernünftige Kompilierung besteht darin, dass der Compiler feststellt, dass das Ausführen einer Anweisung ein undefiniertes Verhalten hervorruft. Daher kann die Anweisung nicht ausgeführt werden. Daher wird sie in eine Anweisung übersetzt, die Ihre Anwendung absichtlich zum Absturz bringt. Das ist sehr vernünftig.
Downvoter: GCC ist mit Ihnen nicht einverstanden.
quelle
Es gibt keine vernünftige Sache, die ein Compiler tun könnte, um ein Ergebnis von 6 zu erhalten, aber es ist möglich und legitim. Ein Ergebnis von 4 ist völlig vernünftig, und ich würde ein Ergebnis von 5 Grenzwerten für vernünftig halten. Alle von ihnen sind vollkommen legal.
Hey warte! Ist nicht klar, was passieren muss? Die Addition benötigt die Ergebnisse der beiden Inkremente, daher müssen diese natürlich zuerst erfolgen. Und wir gehen von links nach rechts, also ... argh! Wenn es nur so einfach wäre. Leider ist das nicht der Fall. Wir gehen nicht von links nach rechts, und das ist das Problem.
Das Einlesen des Speicherorts in zwei Register (oder das Initialisieren beider Register aus demselben Literal, um den Roundtrip zum Speicher zu optimieren) ist für den Compiler eine sehr vernünftige Sache. Dies hat effektiv den Effekt, dass es im Verborgenen zwei verschiedene Variablen mit jeweils einem Wert von 2 gibt, die schließlich zu einem Ergebnis von 4 addiert werden. Dies ist "vernünftig", weil es schnell und effizient ist und mit beiden übereinstimmt der Standard und mit dem Code.
In ähnlicher Weise könnte der Speicherort einmal gelesen (oder die Variable aus dem Literal initialisiert) und einmal inkrementiert werden, und eine Schattenkopie in einem anderen Register könnte danach inkrementiert werden, was dazu führen würde, dass 2 und 3 addiert würden. Dies ist, würde ich sagen, grenzwertig vernünftig, obwohl vollkommen legal. Ich halte es für grenzwertig vernünftig, weil es nicht das eine oder andere ist. Es ist weder der "vernünftige" optimierte Weg, noch ist es der "vernünftige" genau pedantische Weg. Es ist etwas in der Mitte.
Das zweimalige Inkrementieren des Speicherorts (was zu einem Wert von 3 führt) und das anschließende Hinzufügen dieses Werts zu einem Endergebnis von 6 ist legitim, aber nicht ganz sinnvoll, da Speicherrundfahrten nicht gerade effizient sind. Obwohl auf einem Prozessor mit guter Speicherweiterleitung, kann es genauso gut "vernünftig" sein, dies zu tun, da der Speicher größtenteils unsichtbar sein sollte ...
Da der Compiler "weiß", dass es sich um denselben Speicherort handelt, kann er sich auch für eine Erhöhung entscheiden den Wert zweimal innerhalb eines Registers und dann auch zu sich selbst hinzufügen. Jeder Ansatz würde das Ergebnis von 6 ergeben.
Der Compiler darf Ihnen nach dem Wortlaut des Standards ein solches Ergebnis liefern, obwohl ich persönlich 6 so ziemlich als "fuck you" -Memo der Abteilung für abscheuliche Dinge betrachten würde, da es eine ziemlich unerwartete Sache ist (legal oder nicht, Es ist eine gute Sache, immer die geringsten Überraschungen zu erleben!). Wenn man sieht, wie undefiniertes Verhalten involviert ist, kann man leider nicht wirklich über "unerwartet" streiten, wie?
Also, was ist der Code, den Sie dort haben, für den Compiler? Fragen wir clang, was uns zeigt, wenn wir nett fragen (mit aufrufen
-ast-dump -fsyntax-only
):ast.cpp:4:9: warning: multiple unsequenced modifications to 'i' [-Wunsequenced] int x = ++i + ++i; ^ ~~ (some lines omitted) `-CompoundStmt 0x2b3e628 <line:2:1, line:5:1> |-DeclStmt 0x2b3e4b8 <line:3:1, col:10> | `-VarDecl 0x2b3e430 <col:1, col:9> col:5 used i 'int' cinit | `-IntegerLiteral 0x2b3e498 <col:9> 'int' 1 `-DeclStmt 0x2b3e610 <line:4:1, col:18> `-VarDecl 0x2b3e4e8 <col:1, col:17> col:5 x 'int' cinit `-BinaryOperator 0x2b3e5f0 <col:9, col:17> 'int' '+' |-ImplicitCastExpr 0x2b3e5c0 <col:9, col:11> 'int' <LValueToRValue> | `-UnaryOperator 0x2b3e570 <col:9, col:11> 'int' lvalue prefix '++' | `-DeclRefExpr 0x2b3e550 <col:11> 'int' lvalue Var 0x2b3e430 'i' 'int' `-ImplicitCastExpr 0x2b3e5d8 <col:15, col:17> 'int' <LValueToRValue> `-UnaryOperator 0x2b3e5a8 <col:15, col:17> 'int' lvalue prefix '++' `-DeclRefExpr 0x2b3e588 <col:17> 'int' lvalue Var 0x2b3e430 'i' 'int'
Wie Sie sehen können, wird an zwei Stellen dasselbe
lvalue Var 0x2b3e430
Präfix++
angewendet, und diese beiden befinden sich unter demselben Knoten in der Baumstruktur. Dies ist zufällig ein sehr spezieller Operator (+), der nichts Besonderes über Sequenzierung oder dergleichen sagt. Warum ist das wichtig? Nun, lesen Sie weiter.Beachten Sie die Warnung: "Mehrere nicht sequenzierte Änderungen an 'i'" . Oh oh, das hört sich nicht gut an. Was heißt das? [basic.exec] informiert uns über Nebenwirkungen und Sequenzierung und (Absatz 10) darüber, dass Auswertungen von Operanden einzelner Operatoren und von Unterausdrücken einzelner Ausdrücke standardmäßig nicht sequenziert werden, sofern nicht ausdrücklich anders angegeben . Nun, verdammt, das ist der Fall bei
operator+
- nichts wird anders gesagt, also ...Aber kümmern wir uns um vorher sequenzierte, unbestimmt sequenzierte oder nicht sequenzierte? Wer will das schon wissen?
Aus demselben Absatz geht auch hervor, dass sich nicht sequenzierte Auswertungen möglicherweise überschneiden und dass das Verhalten undefiniert ist, wenn sie sich auf denselben Speicherort beziehen (das ist der Fall!) Und dass einer möglicherweise nicht gleichzeitig ausgeführt wird. Hier wird es wirklich hässlich, denn das bedeutet, dass Sie nichts wissen und keine Garantie dafür haben, "vernünftig" zu sein. Das Unvernünftige ist eigentlich vollkommen zulässig und "vernünftig".
quelle
Es gibt eine Regel :
Somit ist sogar x = 100 ein mögliches gültiges Ergebnis.
Für mich ist das logischste Ergebnis im Beispiel 6, weil wir den Wert von i zweimal erhöhen und sie ihn sich selbst hinzufügen. Es ist schwierig, vor den Berechnungswerten von beiden Seiten von "+" eine Addition durchzuführen.
Compiler-Entwickler können jedoch jede andere Logik implementieren.
quelle
Es sieht so aus, als ob ++ i einen l-Wert zurückgibt, aber i ++ einen r-Wert zurückgibt.
Dieser Code ist also in Ordnung:
int i = 1; ++i = 10; cout << i << endl;
Dieser ist nicht:
int i = 1; i++ = 10; cout << i << endl;
Die beiden obigen Anweisungen stimmen mit VisualC ++, GCC7.1.1, CLang und Embarcadero überein.
Aus diesem Grund ähnelt Ihr Code in VisualC ++ und GCC7.1.1 dem folgenden
int i = 1; ... do something there for instance: ++i; ++i; ... int x = i + i;
Bei der Demontage wird zuerst i inkrementiert und i neu geschrieben. Wenn Sie versuchen, es hinzuzufügen, tun Sie dasselbe, erhöhen i und schreiben es neu. Dann fügt ich i zu i hinzu. Ich habe festgestellt, dass CLang und Embarcadero unterschiedlich handeln. Es stimmt also nicht mit der ersten Anweisung überein. Nach dem ersten ++ i speichert es das Ergebnis in einem r-Wert und fügt es dann dem zweiten i ++ hinzu.
quelle
Ich persönlich hätte nie erwartet, dass ein Compiler in Ihrem Beispiel 6 ausgibt. Es gibt bereits gute und detaillierte Antworten auf Ihre Frage. Ich werde eine kurze Version versuchen.
Grundsätzlich
++i
handelt es sich in diesem Zusammenhang um einen zweistufigen Prozess:i
i
Im Zusammenhang mit
++i + ++i
den beiden Seiten kann der Zusatz in beliebiger Reihenfolge nach dem Standard bewertet werden. Dies bedeutet, dass die beiden Inkremente als unabhängig betrachtet werden. Es gibt auch keine Abhängigkeit zwischen den beiden Begriffen. Das Inkrementieren und Lesen voni
kann daher verschachtelt sein. Dies gibt die mögliche Reihenfolge:i
für den linken Operandeni
für den richtigen Operandeni
den linken Operanden zurücki
den richtigen Operanden zurückNun, da ich darüber nachdenke, ist 6 nach dem Standard am sinnvollsten. Für ein Ergebnis von 4 benötigen wir eine CPU, die zuerst
i
unabhängig liest , dann den Wert erhöht und an dieselbe Stelle zurückschreibt. im Grunde eine Rennbedingung. Für einen Wert von 5 benötigen wir einen Compiler, der Provisorien einführt.Der Standard besagt jedoch, dass
++i
die Variable vor der Rückgabe inkrementiert wird, dh bevor die aktuelle Codezeile tatsächlich ausgeführt wird. Der Summenoperator+
mussi + i
nach dem Anwenden der Inkremente summieren . Ich würde sagen, dass C ++ an den Variablen arbeiten muss und nicht an einer Wertesemantik. Daher ist 6 für mich jetzt am sinnvollsten, da es auf der Semantik der Sprache und nicht auf dem Ausführungsmodell von CPUs beruht.quelle
#include <stdio.h> void a1(void) { int i = 1; int x = ++i; printf("i=%d\n",i); printf("x=%d\n",x); x = x + ++i; // Here printf("i=%d\n",i); printf("x=%d\n",x); } void b2(void) { int i = 1; int x = ++i; printf("i=%d\n",i); printf("x=%d\n",x); x = i + ++i; // Here printf("i=%d\n",i); printf("x=%d\n",x); } void main(void) { a1(); // b2(); }
quelle
Nun, es hängt vom Design des Compilers ab. Daher hängt die Antwort davon ab, wie der Compiler die Anweisungen dekodiert. Die Verwendung von zwei verschiedenen Variablen ++ x und ++ y zum Erstellen einer Logik wäre die bessere Wahl. Hinweis: Die Ausgabe hängt von der Version der neuesten Sprachversion in ms Visual Studio ab, sofern diese aktualisiert wurde. Wenn sich die Regeln geändert haben, wird auch die Ausgabe geändert
quelle
Versuche dies
int i = 1; int i1 = i, i2 = i; // i1 = i2 = 1 ++i1; // i1 = 2 ++i2; // i2 = 2 int x = i1 + i2; // x = 4
quelle
In der Praxis rufen Sie undefiniertes Verhalten auf. Alles kann passieren, nicht nur Dinge , die man „vernünftig“ betrachten, und oft Dinge tun passieren , dass man nicht für angemessen halten. Alles ist per Definition "vernünftig".
Eine sehr vernünftige Kompilierung besteht darin, dass der Compiler feststellt, dass das Ausführen einer Anweisung ein undefiniertes Verhalten hervorruft. Daher kann die Anweisung niemals ausgeführt werden. Daher wird sie in eine Anweisung übersetzt, die Ihre Anwendung absichtlich zum Absturz bringt. Das ist sehr vernünftig. Schließlich weiß der Compiler, dass dieser Absturz niemals passieren kann.
quelle