Warum verwenden diese Konstrukte undefiniertes Verhalten vor und nach dem Inkrementieren?

814
#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?
}
PiX
quelle
12
@Jarett, nein, brauchte nur ein paar Zeiger auf "Sequenzpunkte". Während der Arbeit habe ich einen Code mit i = i ++ gefunden. Ich dachte, "Dies ändert den Wert von i nicht". Ich habe getestet und mich gefragt, warum. Seitdem habe ich diese Anweisung entfernt und durch i ++ ersetzt.
PiX
198
Ich finde es interessant, dass jeder IMMER davon ausgeht, dass Fragen wie diese gestellt werden, weil der Fragesteller das betreffende Konstrukt verwenden möchte. Meine erste Annahme war, dass PiX weiß, dass diese schlecht sind, aber neugierig ist, warum sie sich so verhalten, wie sie auf dem Compiler verwendet werden, den er verwendet hat ... Und ja, was unWind gesagt hat ... es ist undefiniert, es kann alles tun. .. einschließlich JCF (Jump and Catch Fire)
Brian Postow
32
Ich bin neugierig: Warum scheinen Compiler nicht vor Konstrukten wie "u = u ++ + ++ u;" zu warnen? ob das Ergebnis undefiniert ist?
Lernen Sie OpenGL ES
5
(i++)immer noch zu 1, unabhängig von Klammern
Drew McGowen
2
Was auch i = (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 definiert i = (i++);, ist es immer noch schlechter Code. Schreiben Sie einfachi++;
Keith Thompson

Antworten:

566

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 volatilewird, beweist oder ändert dies nichts. Es ist undefiniert ; Sie können nicht über das Verhalten argumentieren.

Ihr interessantestes Beispiel, das mit

u = (u++);

ist ein Lehrbuchbeispiel für undefiniertes Verhalten (siehe Wikipedia-Eintrag zu Sequenzpunkten ).

entspannen
quelle
8
@PiX: Dinge sind aus einer Reihe von möglichen Gründen undefiniert. Dazu gehören: Es gibt kein klares "richtiges Ergebnis", unterschiedliche Maschinenarchitekturen würden unterschiedliche Ergebnisse stark begünstigen, die bestehende Praxis ist nicht konsistent oder geht über den Geltungsbereich des Standards hinaus (z. B. welche Dateinamen gültig sind).
Richard
Um alle zu verwirren, sind einige solcher Beispiele in C11 gut definiert, z i = ++i + 1;.
MM
2
Wenn man den Standard und die veröffentlichten Gründe liest, ist klar, warum das Konzept von UB existiert. Der Standard sollte niemals vollständig beschreiben, was eine C-Implementierung tun muss, um für einen bestimmten Zweck geeignet zu sein (siehe die Diskussion der Regel "Ein Programm"), sondern stützt sich auf das Urteilsvermögen der Implementierer und den Wunsch, nützliche Qualitätsimplementierungen zu erstellen. Eine Qualitätsimplementierung, die für die Programmierung von Systemen auf niedriger Ebene geeignet ist, muss das Verhalten von Aktionen definieren, die bei High-End-Anwendungen zum Knacken von Zahlen nicht erforderlich wären. Anstatt zu versuchen, den Standard zu komplizieren ...
Supercat
3
... indem sie extrem detailliert darlegten, welche Eckfälle definiert sind oder nicht, erkannten die Autoren des Standards, dass Implementierer schneller vorgehen sollten, um zu beurteilen, welche Verhaltensweisen von den Arten von Programmen benötigt werden, die sie unterstützen sollen . Hypermodernistische Compiler geben vor, dass bestimmte Maßnahmen UB implizieren sollten, dass kein Qualitätsprogramm sie benötigen sollte, aber der Standard und die Begründung widersprechen einer solchen vermeintlichen Absicht.
Supercat
1
@jrh: Ich habe diese Antwort geschrieben, bevor mir klar wurde, wie außer Kontrolle die hypermoderne Philosophie geraten war. Was mich ärgert, ist der Fortschritt von "Wir müssen dieses Verhalten nicht offiziell erkennen, da die Plattformen, auf denen es benötigt wird, es sowieso unterstützen können" zu "Wir können dieses Verhalten entfernen, ohne einen brauchbaren Ersatz bereitzustellen, da es nie erkannt wurde und somit kein Code." es zu brauchen war kaputt ". Viele Verhaltensweisen hätten längst zugunsten von in jeder Hinsicht besseren Ersetzungen verworfen werden müssen , aber dafür hätte man ihre Legitimität anerkennen müssen.
Supercat
78

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:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(Ich ... nehme an, dass der Befehl 0x00000014 eine Art Compileroptimierung war?)

badp
quelle
Wie bekomme ich den Maschinencode? Ich benutze Dev C ++ und habe in den Compiler-Einstellungen mit der Option 'Code Generation'
herumgespielt
5
@ronnieaka gcc evil.c -c -o evil.binund gdb evil.bindisassemble evil, oder was auch immer die Windows-Entsprechungen davon sind :)
Badp
21
Diese Antwort spricht die Frage von nicht wirklich an Why are these constructs undefined behavior?.
Shafik Yaghmour
9
Abgesehen davon wird es einfacher sein, zu Assembly (mit gcc -S evil.c) zu kompilieren , was alles ist, was hier benötigt wird. Das Zusammenbauen und dann das Zerlegen ist nur ein Umweg.
Kat
50
Wenn Sie sich aus irgendeinem Grund fragen, was ein bestimmtes Konstrukt tut - und insbesondere, wenn der Verdacht besteht, dass es sich um ein undefiniertes Verhalten handelt -, lautet der uralte Ratschlag: "Probieren Sie es einfach mit Ihrem Compiler aus und sehen Sie" möglicherweise ziemlich gefährlich. Unter diesen Umständen erfahren Sie heute bestenfalls, was es unter dieser Version Ihres Compilers tut . Sie werden nicht viel darüber lernen, was es garantiert tut. Im Allgemeinen führt "Probieren Sie es einfach mit Ihrem Compiler aus" zu nicht portierbaren Programmen, die nur mit Ihrem Compiler funktionieren.
Steve Summit
64

Ich denke, die relevanten Teile des C99-Standards sind 6.5 Ausdrücke, §2

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.

und 6.5.16 Zuweisungsoperatoren, §4:

Die Reihenfolge der Auswertung der Operanden ist nicht spezifiziert. Wenn versucht wird, das Ergebnis eines Zuweisungsoperators zu ändern oder nach dem nächsten Sequenzpunkt darauf zuzugreifen, ist das Verhalten undefiniert.

Christoph
quelle
2
Würde das oben
Gesagte
1
@ Supercat, soweit ich weiß, i=i=5ist auch undefiniertes Verhalten
Dhein
2
@Zaibis: Die Begründung, die ich für die meisten Orte verwende, gilt, dass eine Plattform mit mehreren Prozessoren theoretisch so etwas wie A=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 wie C=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 Thread I=I=5;jedoch ...
Supercat
1
... und der Compiler bemerkte nicht, dass sich beide Schreibvorgänge an derselben Stelle befanden (wenn einer oder beide Werte Zeiger enthalten, die möglicherweise schwer zu bestimmen sind), konnte der generierte Code blockieren. Ich denke nicht, dass reale Implementierungen ein solches Sperren als Teil ihres normalen Verhaltens implementieren, aber es wäre nach dem Standard zulässig, und wenn Hardware solche Verhaltensweisen billig implementieren könnte, könnte es nützlich sein. Auf der heutigen Hardware wäre ein solches Verhalten viel zu teuer, um es standardmäßig zu implementieren, aber das bedeutet nicht, dass es immer so wäre.
Supercat
1
@supercat, aber würde die Sequenzpunktzugriffsregel von c99 allein nicht ausreichen, um sie als undefiniertes Verhalten zu deklarieren? Es spielt also keine Rolle, was die Hardware technisch implementieren könnte.
Dhein
55

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)

Bei zwei beliebigen Bewertungen Aund B, falls Azuvor sequenziert B, muss die Ausführung von der Ausführung von Avorausgehen B.

Nicht sequenziert:

Wenn Anicht vorher oder nachher sequenziert wird B, dann Aund Bsind nicht sequenziert.

Bewertungen können eines von zwei Dingen sein:

  • Wertberechnungen , die das Ergebnis eines Ausdrucks berechnen ; und
  • Nebenwirkungen , bei denen es sich um Modifikationen von Objekten handelt.

Sequenzpunkt:

Das Vorhandensein eines Sequenzpunktes zwischen der Auswertung von Ausdrücken Aund Bimpliziert , dass jede Wertberechnung und Nebenwirkung , die mit Avor jeder sequenziert Wertberechnung und Nebenwirkung zuzugeordnet B.

Kommen wir nun zu der Frage, für die Ausdrücke wie

int i = 1;
i = i++;

Standard sagt, dass:

6.5 Ausdrücke:

Wenn eine Nebenwirkung auf einem skalares Objekt unsequenced relativ ist entweder eine andere Nebenwirkung auf dem gleichen skalare Objekt oder eine Wertberechnung mit dem Wert des gleichen skalare Objekts, wird das Verhalten undefiniert . [...]

Daher ruft der obige Ausdruck UB auf, da zwei Nebenwirkungen auf dasselbe Objekt irelativ zueinander nicht sequenziert sind. Das heißt, es wird nicht sequenziert, ob die Nebenwirkung durch Zuweisung an ivor 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 ilinks von der Zuweisung be ilund rechts von der Zuweisung (im Ausdruck i++) be um ir, dann ist der Ausdruck wie

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Ein wichtiger Punkt in Bezug auf den Postfix- ++Operator ist:

Nur weil das ++nach der Variablen kommt, heißt das nicht, dass das Inkrement zu spät erfolgt . Das Inkrement kann so früh erfolgen, wie es der Compiler möchte , solange der Compiler sicherstellt, dass der ursprüngliche Wert verwendet wird .

Dies bedeutet, dass der Ausdruck il = ir++entweder als bewertet werden kann

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

oder

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

in zwei unterschiedlichen Ergebnissen resultierende 1und 2davon abhängt , welche auf der Sequenz der Nebenwirkungen durch Zuordnung und ++und daher ruft UB.

Haccks
quelle
52

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.5Absatz 3 sagt ( Hervorhebung von mir ):

Die Gruppierung von Operatoren und Operanden wird durch die Syntax angegeben.74) Außer wie später angegeben (für die Funktionsaufruf- (), &&, ||,?: Und Komma-Operatoren), die Reihenfolge der Auswertung von Unterausdrücken und die Reihenfolge in Welche Nebenwirkungen auftreten, ist nicht spezifiziert.

Wenn wir also eine Zeile wie diese haben:

i = i++ + ++i;

Wir wissen nicht, ob wir zuerst bewertet werden i++oder ++iwerden. 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 Standardabschnitts 6.5Absatz 2 ( Schwerpunkt Mine ):

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 .

Die folgenden Codebeispiele werden als undefiniert bezeichnet:

i = ++i + 1;
a[i++] = i; 

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 :

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

Nicht spezifiziertes Verhalten wird im Entwurf der c99-Norm im Abschnitt 3.4.4wie folgt definiert :

Verwendung eines nicht spezifizierten Wertes oder eines anderen Verhaltens, bei dem diese Internationale Norm zwei oder mehr Möglichkeiten bietet und keine weiteren Anforderungen stellt, die in irgendeinem Fall gewählt werden

und undefiniertes Verhalten wird im Abschnitt definiert 3.4.3als:

Verhalten bei Verwendung eines nicht portierbaren oder fehlerhaften Programmkonstrukts oder von fehlerhaften Daten, für die diese Internationale Norm keine Anforderungen stellt

und stellt fest, dass:

Mögliches undefiniertes Verhalten reicht vom vollständigen Ignorieren der Situation mit unvorhersehbaren Ergebnissen über das Verhalten während der Übersetzung oder Programmausführung in einer für die Umgebung charakteristischen dokumentierten Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis zum Beenden einer Übersetzung oder Ausführung (mit der Ausgabe) einer Diagnosemeldung).

Shafik Yaghmour
quelle
33

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:

i = i + 1

C hat natürlich eine praktische Abkürzung:

i++

Dies bedeutet, "addiere 1 zu i und ordne das Ergebnis wieder i zu". Wenn wir also ein Durcheinander der beiden konstruieren, schreiben wir

i = i++

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 .

Steve Summit
quelle
1
Eine ziemlich unangenehme Gotcha im Hinblick auf undefiniertes Verhalten ist , dass , während es verwendet wird, sicher zu sein auf 99,9% der Compiler zu verwenden , *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 (die elseKlausel ist notwendig, um zuzulassen Der Compiler optimiert das, ifwas einige neuere Compiler benötigen würden.
Supercat
@supercat Ich glaube jetzt, dass jeder Compiler, der "intelligent" genug ist, um diese Art der Optimierung durchzuführen, auch klug genug sein muss, um assertAnweisungen zu betrachten, damit der Programmierer der fraglichen Zeile eine einfache Zeile voranstellen kann assert(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.)
Steve Summit
25

Oft wird diese Frage als Duplikat von Fragen verknüpft, die sich auf Code wie beziehen

printf("%d %d\n", i, i++);

oder

printf("%d %d\n", ++i, i++);

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:

x = i++ + i++;

In der folgenden Aussage:

printf("%d %d\n", ++i, i++);

Die Reihenfolge der Bewertung der Argumente in printf()ist nicht angegeben . Das heißt, Ausdrücke i++und ++ikönnen in beliebiger Reihenfolge ausgewertet werden. Der C11-Standard enthält einige relevante Beschreibungen dazu:

Anhang J, nicht näher bezeichnetes Verhalten

Die Reihenfolge, in der der Funktionsbezeichner, die Argumente und die Unterausdrücke innerhalb der Argumente in einem Funktionsaufruf (6.5.2.2) ausgewertet werden.

3.4.4, nicht spezifiziertes Verhalten

Verwendung eines nicht spezifizierten Wertes oder eines anderen Verhaltens, bei dem diese Internationale Norm zwei oder mehr Möglichkeiten bietet und keine weiteren Anforderungen stellt, die in irgendeinem Fall gewählt werden.

BEISPIEL Ein Beispiel für nicht angegebenes Verhalten ist die Reihenfolge, in der die Argumente für eine Funktion ausgewertet werden.

Das nicht spezifizierte Verhalten selbst ist KEIN Problem. Betrachten Sie dieses Beispiel:

printf("%d %d\n", ++x, y++);

Auch dies hat ein nicht spezifiziertes Verhalten, da die Reihenfolge der Bewertung von ++xund y++nicht spezifiziert ist. Aber es ist eine vollkommen legale und gültige Aussage. Es gibt keine undefinierten Verhalten in dieser Aussage. Weil die Änderungen ( ++xund y++) an verschiedenen Objekten vorgenommen werden.

Was macht die folgende Aussage

printf("%d %d\n", ++i, i++);

Als undefiniertes Verhalten gilt die Tatsache, dass diese beiden Ausdrücke dasselbe Objekt iohne 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:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

Der Kommaoperator wertet seine Operanden von links nach rechts aus und gibt nur den Wert des letzten Operanden aus. So in j = (++i, i++);, ++iSchritte iauf 6und i++Ausbeuten alter Wert von i( 6) , die zugeordnet ist j. Dann iwird 7aufgrund von Nachinkrement.

Wenn also das Komma im Funktionsaufruf ein Kommaoperator sein sollte, dann

printf("%d %d\n", ++i, i++);

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.

PP
quelle
Diese Sequenz 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?
Kavadias
@kavadias Diese printf-Anweisung beinhaltet aus demselben oben erläuterten Grund undefiniertes Verhalten. Sie schreiben bund cin 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-pointder auch diese gefunden werden können.
PP
23

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:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

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, iwü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 Zugriff iwä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 erkennen i, wenn an beiden Stellen dieselbe Variable verwendet wird, eine Routine jedoch Verweise auf zwei Zeiger pund qund und verwendet (*p)und (*q)im obigen Ausdruck (anstatt zu verwenden) akzeptiertizweimal) Der Compiler müsste den Deadlock nicht erkennen oder vermeiden, der auftreten würde, wenn die Adresse desselben Objekts für beide pund übergeben würde q.

Superkatze
quelle
16

Während die Syntax der Ausdrücke wie a = a++oder a++ + a++legal ist, ist das Verhalten dieser Konstrukte undefiniert, da ein Soll im C-Standard nicht eingehalten wird. C99 6.5p2 :

  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. [72] Außerdem darf der vorherige Wert nur gelesen werden, um den zu speichernden Wert zu bestimmen. [73]

Mit Fußnote 73 wird dies weiter klargestellt

  1. In diesem Absatz werden undefinierte Anweisungsausdrücke wie z

    i = ++i + 1;
    a[i++] = i;

    während erlauben

    i = i + 1;
    a[i] = i;

Die verschiedenen Sequenzpunkte sind in Anhang C von C11 (und C99 ) aufgeführt:

  1. Die folgenden Sequenzpunkte sind in 5.1.2.3 beschrieben:

    • Zwischen den Auswertungen des Funktionsbezeichners und den tatsächlichen Argumenten in einem Funktionsaufruf und dem tatsächlichen Aufruf. (6.5.2.2).
    • Zwischen den Auswertungen des ersten und zweiten Operanden der folgenden Operatoren: logisches UND && (6.5.13); logisches ODER || (6.5.14); Komma, (6.5.17).
    • Zwischen den Auswertungen des ersten Operanden der Bedingung? : Operator und welcher der zweiten und dritten Operanden ausgewertet wird (6.5.15).
    • Das Ende eines vollständigen Deklarators: Deklaratoren (6.7.6);
    • Zwischen der Bewertung eines vollständigen Ausdrucks und dem nächsten zu bewertenden vollständigen Ausdruck. Das Folgende sind vollständige Ausdrücke: ein Initialisierer, der nicht Teil eines zusammengesetzten Literal ist (6.7.9); der Ausdruck in einer Ausdrucksanweisung (6.8.3); der steuernde Ausdruck einer Auswahlanweisung (if oder switch) (6.8.4); der steuernde Ausdruck einer while- oder do-Anweisung (6.8.5); jeder der (optionalen) Ausdrücke einer for-Anweisung (6.8.5.3); der (optionale) Ausdruck in einer return-Anweisung (6.8.6.4).
    • Unmittelbar bevor eine Bibliotheksfunktion zurückkehrt (7.1.4).
    • Nach den Aktionen, die jedem formatierten Konvertierungsspezifizierer für Eingabe- / Ausgabefunktionen zugeordnet sind (7.21.6, 7.29.2).
    • Unmittelbar vor und unmittelbar nach jedem Aufruf einer Vergleichsfunktion sowie zwischen jedem Aufruf einer Vergleichsfunktion und jeder Bewegung der Objekte, die als Argumente an diesen Aufruf übergeben wurden (7.22.5).

Der Wortlaut desselben Absatzes in C11 lautet:

  1. Wenn eine Nebenwirkung auf ein Skalarobjekt in Bezug auf eine andere Nebenwirkung auf dasselbe Skalarobjekt oder eine Wertberechnung unter Verwendung des Werts desselben Skalarobjekts nicht sequenziert wird, ist das Verhalten undefiniert. Wenn es mehrere zulässige Ordnungen der Unterausdrücke eines Ausdrucks gibt, ist das Verhalten undefiniert, wenn eine solche nicht sequenzierte Nebenwirkung in einer der Ordnungen auftritt.84)

Sie können solche Fehler in einem Programm erkennen, indem Sie beispielsweise eine neuere Version von GCC mit -Wallund 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:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function main’:
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

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

j = (i ++, ++ i);

ist gut definiert und erhöht sich ium eins, was den alten Wert ergibt. Verwerfen Sie diesen Wert. Beheben Sie dann beim Komma-Operator die Nebenwirkungen. und dann ium 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 ist

i += 2;
j = i;

Die ,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 Funktionsaufruf

int i = 0;
printf("%d %d\n", i++, ++i, i);

hat ein undefiniertes Verhalten, da zwischen den Auswertungen von i++und ++iin Funktionsargumenten kein Sequenzpunkt vorhanden ist und der Wert von idaher zweimal von beiden i++und ++izwischen dem vorherigen und dem nächsten Sequenzpunkt geändert wird.

Antti Haapala
quelle
14

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:

i = i++;
i = i++ + ++i;

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.

while(*src++ = *dst++);

Das Obige ist eine gängige Codierungspraxis beim Kopieren / Analysieren von Zeichenfolgen.

Nikhil Vidhani
quelle
Natürlich gilt es nicht für verschiedene Variablen innerhalb eines Ausdrucks. Es wäre ein totaler Designfehler, wenn es so wäre! Alles, was Sie im zweiten Beispiel brauchen, ist, dass beide zwischen dem Ende der Anweisung und dem nächsten Anfang inkrementiert werden, und das ist garantiert, gerade weil das Konzept der Sequenzpunkte im Mittelpunkt all dessen steht.
underscore_d
11

In /programming/29505280/incrementing-array-index-in-c fragte jemand nach einer Aussage wie:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

welches 7 druckt ... das OP erwartete, dass es 6 druckt.

Es ++iist 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 ++iausgeführt, dann werden die Werte k[]gelesen wurden, dann ist die letzte ++idann k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

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).

TomOnTime
quelle
5

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.

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.

Die Sequenzpunkte in einem Ausdruck wie i=i++sind vorher i=und nachher i++.

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 Eintrag expression-statementin 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.

i=i++

kann interpretiert werden als

tmp = i
i=i+1
i = tmp

oder als

tmp = i
i = tmp
i=i+1

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 .

Alinsoar
quelle
Wie hat diese Antwort die vorhandenen Antworten neu ergänzt? Auch die Erklärungen für i=i++sind dieser Antwort sehr ähnlich .
Haccks
@haccks Ich habe die anderen Antworten nicht gelesen. Ich wollte in meiner eigenen Sprache erklären, was ich aus dem erwähnten Dokument von der offiziellen Website von ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
gelernt
5

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 ++xsich das Präfixformular vom Postfixformular unterscheidet x++. Aber diese Operatoren sind schwer zu überlegen. Um sicherzugehen, dass Sie verstanden haben, haben Sie vielleicht ein kleines Testprogramm geschrieben, das so etwas beinhaltet

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

Zu 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

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

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

printf("%d %d %d\n", x, ++x, x++);

Die Frage ist, printfberechnet der Compiler vor dem Aufruf den Wert von xfirst oder x++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 wird xzuerst, dann ++x, dann x++, oder x++dann ++xdann xoder eine andere Reihenfolge. Die Reihenfolge ist jedoch eindeutig von Bedeutung, da je nach der vom Compiler verwendeten Reihenfolge eindeutig unterschiedliche Ergebnisse gedruckt werden printf.

Was ist mit diesem verrückten Ausdruck?

x = x++ + ++x;

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 speichern xund den alten Wert von zurückzugeben x. (2) der ++xTeil versucht, 1 zu x hinzuzufügen, den neuen Wert in zu speichern xund den neuen Wert von zurückzugeben x; und (3) der x =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 zugewiesen x? 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 xVorrang 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:

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 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:

  1. Wenn es eine Variable gibt, die an zwei oder mehr verschiedenen Stellen geändert (zugewiesen) wird, woher wissen Sie, welche Änderung zuerst erfolgt?
  2. Wenn es eine Variable gibt, die an einer Stelle geändert wird und deren Wert an einer anderen Stelle verwendet wird, woher wissen Sie, ob sie den alten oder den neuen Wert verwendet?

Als Beispiel für # 1 im Ausdruck

x = x++ + ++x;

Es gibt drei Versuche, `x zu ändern.

Als Beispiel für # 2 im Ausdruck

y = x + x++;

Wir beide verwenden den Wert von xund ä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.

Steve Summit
quelle
3

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:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }}

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

In GCC C ++ steuert die Priorität der Operatoren die Reihenfolge, in der die einzelnen Operatoren ausgewertet werden

Der äquivalente Code in definiertem Verhalten C ++, wie GCC versteht:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Dann gehen wir zu Visual Studio . Visual Studio 2015 erhalten Sie:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

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:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

Wie in der Visual Studio-Dokumentation unter Vorrang und Reihenfolge der Auswertung angegeben :

Wenn mehrere Operatoren zusammen auftreten, haben sie die gleiche Priorität und werden entsprechend ihrer Assoziativität bewertet. Die Operatoren in der Tabelle werden in den Abschnitten beschrieben, die mit Postfix-Operatoren beginnen.

Mohamed El-Nakib
quelle
1
Ich habe die Frage bearbeitet, um die UB bei der Bewertung von Funktionsargumenten hinzuzufügen, da diese Frage häufig als Duplikat dafür verwendet wird. (Das letzte Beispiel)
Antti Haapala
1
Auch die Frage ist über c jetzt, nicht C ++
Antti Haapala