Frage

#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?
}
War es hilfreich?

Lösung

C das Konzept des undefinierten Verhalten hat, das heißt einige Sprachkonstrukte syntaktisch gültig sind, aber Sie können das Verhalten nicht vorhersagen, wenn der Code ausgeführt wird.

Soweit ich weiß, ist der Standard nicht explizit sagen, Warum das Konzept des undefinierten Verhaltens existiert. In meinem Kopf, es ist einfach, weil die Sprache Designer da wollte einen gewissen Spielraum in der Semantik sein, statt dh zu verlangen, dass alle Implementierungen Integer-Überlauf in der exakt gleichen Art und Weise behandeln, die sehr wahrscheinlich schwerwiegende Performance Kosten auferlegen würde, ließen sie nur das Verhalten undefined so dass, wenn Sie Code schreiben, die integer-Überlauf verursacht, kann alles passieren.

Also, in diesem Sinne, warum sind diese „Probleme“? Die Sprache sagt deutlich, dass bestimmte Dinge führen href="http://en.wikipedia.org/wiki/Undefined_behavior" rel="noreferrer"> undefinierten Verhalten volatile deklariert wird, bedeutet das nicht beweisen oder nichts ändern. Es ist undefined ; Sie können nicht über das Verhalten folgern.

Ihre interessanteste schau Beispiel des mit

u = (u++);

ist ein Musterbeispiel von undefiniertem Verhalten (siehe Wikipedias Eintrag auf Sequenz zeigt ).

Andere Tipps

Just kompilieren und Ihre Codezeilen zerlegen, wenn Sie so geneigt ist zu wissen, wie genau es ist, Sie bekommen, was Sie bekommen.

Das ist, was ich auf meiner Maschine zu bekommen, zusammen mit dem, was ich denke, gehe auf:

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

(I ... nehme an, dass die 0x00000014 Anweisung eine Art von Compiler-Optimierung war?)

Ich denke, die relevanten Teile des C99-Standard sind 6,5-Ausdrücke, §2

  

Zwischen dem vorherigen und nächsten Sequenzpunkt ein Objekt seinen gespeicherten Wert haben soll   höchstens einmal durch die Auswertung eines Ausdrucks geändert. Weiterhin ist der vorherige Wert   wird nur zu bestimmen, um den Wert zu lesen gespeichert werden.

und 6.5.16 Zuweisungsoperator, §4:

  

Die Reihenfolge der Auswertung der Operanden ist nicht spezifiziert. Wenn versucht wird, zu modifizieren,   das Ergebnis eines Zuweisungsoperators oder nach dem nächsten Sequenz Punkt zuzugreifen, die   Verhalten ist nicht definiert.

Das Verhalten kann nicht wirklich erklärt werden, da sie sowohl ruft nicht spezifiziert Verhalten und undefiniertes Verhalten , so können wir keine allgemeinen Aussagen über diesen Code machen, obwohl, wenn Sie lesen Olve Maudal des Arbeit wie Tief C und Entwurf c99 Standard section6.5 Absatz 3 sagt ( Hervorhebung von mir ):

  

Die Gruppierung von Operatoren und Operanden wird durch die syntax.74 angegeben) Mit Ausnahme der angegebenen   später (für den Funktionsaufruf (), &&, ||?,:, und Komma Operatoren), die Reihenfolge der Auswertung von Unterausdrücken und die Reihenfolge, in der Nebenwirkungen stattfinden, sind beide nicht spezifiziert <. / p>

Wenn wir also eine Zeile wie diese:

i = i++ + ++i;

Wir wissen nicht, ob i++ oder ++i wird zuerst ausgewertet werden. Dies ist vor allem der Compiler bessere Möglichkeiten zur Optimierung zu geben.

Wir haben auch nicht definiertes Verhalten auch hier, da das Programm Variablen Modifizieren (i, u, etc ..) mehr als einmal zwischen Sequenz zeigt . Von Standard-Entwurf Abschnitt 6.5 Absatz 2 ( Hervorhebung von mir ):

  

Zwischen dem vorherigen und nächsten Sequenzpunkt ein Objekt hat seinen gespeicherten Wert hat   höchstens einmal durch die Auswertung eines Ausdrucks geändert. Des Weiteren der vorherige Wert   wird nur zu bestimmen, um den Wert zu lesen gespeichert werden .

es zitiert die folgenden Codebeispiele als undefined:

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

In all diesen Beispielen der Code ein Objekt mehr als einmal in der gleichen Reihenfolge Punkt zu ändern versucht, die mit dem ; in jedem dieser Fälle endet:

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

Keine Angabe Verhalten in der Entwurf c99 Standard in Abschnitt 3.4.4 wie:

  

Verwendung eines nicht spezifizierten Wert oder ein anderes Verhalten, wenn diese Internationale Norm liefert   zwei oder mehr Möglichkeiten und erlegt keine weiteren Anforderungen an die in jedem gewählt wird   Beispiel

und nicht definiertes Verhalten ist im Abschnitt 3.4.3 wie folgt definiert:

  

Verhalten bei der Verwendung eines nonportable oder fehlerhaften Programm Konstrukt oder von fehlerhaften Daten,   für die diese Internationale Norm legt keine Anforderungen

und stellt fest, dass:

  

Mögliches undefiniertes Verhalten reicht von der Situation völlig mit unvorhersehbaren Ergebnissen zu ignorieren, während der Übersetzung oder Programmausführung in einer dokumentierten Weise charakteristisch für die Umwelt (mit oder ohne die Ausgabe einer Diagnosemeldung) zu verhalten, eine Übersetzung oder Ausführung beendet ( mit der Ausgabe einer Diagnosemeldung).

Die meisten Antworten hier aus C-Norm zitiert betont, dass das Verhalten dieser Konstrukte nicht definiert ist. Um zu verstehen, , warum das Verhalten dieser Konstrukte sind nicht definiert , lassen Sie uns diese Begriffe verstehen zuerst im Lichte der C11-Standard:

Sequenced: (5.1.2.3)

  

Bei zwei beliebigen Auswertungen A und B, wenn A vor B sequenziert wird, dann ist die Ausführung von A wird die Ausführung von B vorangestellt werden.

unsequenced:

  

Wenn A vor nicht sequenziert oder nach B, dann A und B sind unsequenced.

Auswertungen kann eine von zwei Dinge:

  • Wertberechnungen , die das Ergebnis eines Ausdrucks erarbeiten; und
  • Nebenwirkungen , die Änderungen der Objekte sind.

Sequence Punkt:

  

Das Vorhandensein eines Sequenzpunktes zwischen der Auswertung von Ausdrücken A und B bedeutet, dass jeder Wertberechnung und Nebeneffekt im Zusammenhang mit A sequenziert wird, bevor alle Wert Berechnung und Nebeneffekt mit B verbunden.

kommt nun auf die Frage, für die Begriffe wie

int i = 1;
i = i++;

Standard sagt, dass:

6.5 Ausdrücke:

  

Wenn eine Nebenwirkung auf ein skalares Objekt ist unsequenced relativ zu entweder eine andere Nebenwirkung auf dem gleichen skalaren Objekt oder einen Wert Berechnung den Wert des gleichen skalaren Verwendung Objekt, ist das Verhalten nicht definiert . [...]

Daher ruft der obige Ausdruck UB, weil zwei Nebenwirkungen auf das gleiche Objekt i zueinander unsequenced relativ. Das bedeutet, dass es nicht sequenziert wird, ob die Nebenwirkung durch Zuordnung zu i wird vor oder nach der Nebenwirkung von ++ erfolgen.
Je nachdem, ob Zuordnung erfolgte vor oder nach der Erhöhung, werden unterschiedliche Ergebnisse erzeugt werden, und das ist das eines des Falles von nicht definiertes Verhalten .

Läßt die i links Abtretungs umbenannt wird il und rechts die Zuweisung (im Ausdruck i++) ir wird, dann 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 Postfix ++ Operator ist, dass:

  

, nur weil der ++ kommt, nachdem die Variable bedeutet nicht, dass der Zuwachs spät passiert. Der Zuwachs kann so früh geschehen, wie der Compiler Favoriten solange der Compiler stellt sicher, dass der ursprüngliche Wert verwendet wird .

Es bedeutet der Ausdruck il = ir++ ausgewertet werden konnten entweder als

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  

was zu zwei unterschiedlichen Ergebnissen 1 und 2 die durch Zuweisung und ++ auf der Sequenz von Nebenwirkungen abhängt und daher ruft UB.

Eine andere Möglichkeit, dies zu beantworten, anstatt sich in undurchschaubaren Details von Sequenzpunkte verzetteln und nicht definiertes Verhalten, einfach zu fragen, was sie sollen das? Was war der Programmierer versuchen zu tun?

Das erste Fragment gefragt, i = i++ + ++i, ist ziemlich eindeutig verrückt in meinem Buch. Niemand würde es je in einem realen Programm schreiben, es ist nicht klar, was es tut, gibt es kein denkbaren Algorithmus jemand gewesen Code versucht haben könnte, die in dieser erfundenen Folge von Operationen zur Folge hätte. Und da es nicht auf der Hand liegt an dir und mir, was es tun sollte, es ist in Ordnung in meinem Buch, wenn der Compiler kann nicht herausfinden, was es angenommen hat, entweder zu tun.

Das zweite Fragment, i = i++, ist ein wenig leichter zu verstehen. Jemand klar ich versucht zu erhöhen, und weisen Sie das Ergebnis wieder i. Aber es gibt ein paar Möglichkeiten, dies zu Der einfachste Art und Weise in C tun 1 bis i hinzuzufügen und weisen das Ergebnis wieder zu i, ist das gleiche in fast jeden Programmiersprache:

i = i + 1

C, natürlich, hat eine praktische Abkürzung:

i++

Das bedeutet, "add 1 bis i, und weisen i das Ergebnis zurück". Wenn wir also ein Sammelsurium der beiden konstruieren, durch das Schreiben

i = i++

, was wir sagen wirklich „1 bis i hinzufügen und zuweisen i das Ergebnis zurück, und weisen i das Ergebnis zurück“. Ich verwirrte, so dass es stört mich nicht zu viel, wenn die Compiler verwechselt werden, auch.

Realistisch betrachtet, die einzige Zeit, diese verrückten Ausdrücke geschrieben werden, wenn die Menschen sie als künstliche Beispiele verwenden, wie ++ funktionieren soll. Und natürlich ist es wichtig, zu verstehen, wie ++ funktioniert. Aber eine praktische Regel für die Verwendung ++ ist: „Wenn es nicht offensichtlich ist, was ein Ausdruck Mittel ++ verwenden, tun Sie es nicht schreiben.“

Wir haben unzählige Stunden auf comp.lang.c diskutieren Ausdrücke wie diese und Warum sie sind nicht definiert zu verbringen. Zwei meiner mehr Antworten, die wirklich zu erklären versuchen, warum, sind im Web archiviert:

Siehe auch 3.8 und den Rest der Fragen Frage in Abschnitt 3 der C FAQ-Liste .

Oft wird diese Frage als Duplikat von Fragen zu Code wie

im Zusammenhang verknüpft
printf("%d %d\n", i, i++);

oder

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

oder ähnliche Varianten.

Dies ist zwar auch nicht definiertes Verhalten wie bereits erwähnt, gibt es feine Unterschiede, wenn printf() beteiligt ist, wenn sie auf eine Anweisung wie zu vergleichen:

x = i++ + i++;

In der folgenden Anweisung:

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

Reihenfolge der Auswertung der Argumente in printf() ist nicht spezifiziert . Das heißt, Ausdrücke i++ und ++i konnten in beliebiger Reihenfolge ausgewertet werden. C11 Standard hat einige relevante Beschreibungen hierzu:

Anhang J, nicht näher bezeichnet Verhaltensweisen

  

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

3.4.4, nicht näher bezeichnet Verhalten

  

Die Verwendung eines nicht spezifizierten Wert oder ein anderes Verhalten, wo diese   International Standard bietet zwei oder mehr Möglichkeiten und erlegt   keine weiteren Anforderungen, auf denen in jedem Fall gewählt werden.

     

Beispiel Ein Beispiel für nicht näher bezeichnete Verhalten ist die Reihenfolge, in der die   Argumente zu einer Funktion ausgewertet werden.

Das nicht spezifiziert Verhalten selbst ist kein Problem. Betrachten Sie folgendes Beispiel:

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

Auch dies hat nicht spezifiziert Verhalten , da die Reihenfolge der Auswertung von ++x und y++ nicht spezifiziert ist. Aber es ist völlig legal und gültige Aussage. Es gibt nicht nicht definiertes Verhalten in dieser Aussage. Da die Modifikationen (++x und y++) sind getan verschieden Objekte.

Was macht die folgende Anweisung

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

als nicht definiertes Verhalten ist die Tatsache, dass diese beiden Ausdrücke ändern Sie die gleichen Objekt i ohne eine dazwischenliegende Sequenzpunkt .


Ein weiteres Detail ist, dass der Komma , die an der printf () -Aufruf ist ein Separator , nicht die Komma-Operator .

Dies ist eine wichtige Unterscheidung, weil die Komma-Operator einführt einen Sequenzpunkt zwischen der Bewertung ihrer Operanden, die die folgende Recht macht:

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 Komma-Operator wertet seine Operanden von links nach rechts und liefert nur den Wert des letzten Operanden. So in j = (++i, i++);, ++i i Schritten 6 und i++ Ausbeuten alten Wert von i (6), die j zugeordnet ist. Dann i aufgrund Nachinkrement 7 wird.

Also, wenn die Komma in dem Funktionsaufruf dann einen Komma-Operator sein sollte

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

wird kein Problem sein. Aber es ruft nicht definiertes Verhalten , weil die Komma ist hier ein Separator .


Für diejenigen, die neu sind nicht definiertes Verhalten würden profitieren von der Lektüre undefiniert, nicht näher bezeichnet und die Implementierung definierte Verhalten auch relevant ist

.

Obwohl es unwahrscheinlich ist, dass alle Compiler und Prozessoren würden so tatsächlich tun, wäre es legal sein, unter dem C-Standard, für den Compiler zu implementieren „i ++“ mit der Sequenz:

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

Während ich glaube nicht, dass alle Prozessoren unterstützen die Hardware zu ermöglichen, so etwas effizient durchgeführt werden, kann man sich leicht Situationen vorstellen, in denen ein solches Verhalten Multi-Threaded-Code würde einfacher (zB es würde garantieren, dass, wenn zwei Threads versuchen, führen gleichzeitig die obige Folge, würde i um zwei erhöht werden) und es ist nicht völlig ausgeschlossen, dass einige zukünftige Prozessor eine Funktion, so etwas bieten könnte.

Wenn die Compiler i++ schreiben waren, wie oben angegeben (legal unter der Norm) und waren die obigen Anweisungen in der gesamten Auswertung des Gesamtausdruckes einzustreuen (auch rechtlich), und wenn es nicht zu bemerken ist das passiert, dass ein die anderen Anweisungen für den Zugriff auf i passiert ist, wäre es möglich (und legal) für die Compiler eine Folge von Befehlen zu erzeugen, die Deadlock würde. Um sicher zu gehen, würde ein Compiler erkennt an Sicherheit grenzender Wahrscheinlichkeit das Problem in dem Fall, in dem die gleichen Variable i in beiden Orten verwendet wird, aber wenn eine Routine akzeptiert Verweise auf zwei Zeiger p und q und verwendet (*p) und (*q) in dem obigen Ausdruck (eher als die Verwendung von i zweimal) der Compiler nicht zu erkennen oder um den Stillstand zu vermeiden wäre erforderlich, dass, wenn das gleiche Objekt Adresse auftreten würde sowohl für geben wurde p und q.

Der C-Standard sagt, dass eine Variable nur höchstens einmal zwischen zwei Sequenzpunkten zugeordnet werden. Ein Semikolon ist beispielsweise ein Sequenzpunkt.
So jede Anweisung der Form:

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

und so verletzt auf diese Regel. Die Norm sagt auch, dass Verhalten ist nicht definiert und nicht nicht näher bezeichnet. Einige Compiler erkennen diese und einiges Ergebnis produzieren, aber dies ist nicht per Standard.

Allerdings sind zwei verschiedene Variablen können zwischen zwei Sequenzpunkte erhöht werden.

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

Das obige ist eine gemeinsame Codierung der Praxis während des Kopierens / Analyse-Strings.

Während der Syntax der Ausdrücke wie a = a++ oder a++ + a++ legal ist, die Verhalten dieser Konstrukte ist undefined , weil ein < em> wird in C-Standard nicht befolgt wird. C99 6.5p2 :

  
      
  1. Zwischen dem vorherigen und nächsten Sequenzpunkt ein Objekt hat seinen gespeicherten Wert höchstens einmal durch die Auswertung eines Ausdrucks geändert hat. [72] Darüber hinaus wird der vorherige Wert nur gelesen werden, um den Wert zu bestimmen, gespeichert wird [73]
  2.   

Mit Fußnote 73 weiter zu, dass

  
      
  1. Dieser Absatz macht undefinierte Anweisung Ausdrücke wie

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

    während es

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

Die verschiedenen Sequenzpunkte sind in Anhang C der C11 (und C99 ):

  
      
  1. Im Folgenden werden die Sequenzpunkte beschrieben in 5.1.2.3:

         
        
    • Zwischen den Auswertungen der Funktion Bezeichner und tatsächlichen Argumente in einem Funktionsaufruf und dem eigentlichen Anruf. (6.5.2.2).
    •   
    • Zwischen den Bewertungen des ersten und zweiten Operanden der folgenden Operatoren: logische UND && (6.5.13); logisches ODER || (6.5.14); Komma, (6.5.17).
    •   
    • Zwischen den Auswertungen des ersten Operanden der bedingten? :. Betreiber und je nachdem, welche von den zweiten und dritten Operanden ausgewertet (6.5.15)
    •   
    • Das Ende einer vollständigen declarator: Deklaratoren (6.7.6);
    •   
    • Zwischen der Auswertung eines vollen Ausdrucks und dem nächsten vollen Ausdruck ausgewertet werden. Die folgenden sind voll Ausdrücke: einen Initialisierer, die nicht Teil einer Verbindung literal (6.7.9) ist; die Expression in einer Expressions Anweisung (6.8.3); die Steuerung der Expression einer Selektionsangabe (wenn oder Schalter) (6.8.4); die Steuerung der Expression eines während oder do Anweisung (6.8.5); jeder der (optional) Ausdrücken einer for-Anweisung (6.8.5.3); der (optional) Ausdruck in einer return-Anweisung (6.8.6.4).
    •   
    • unmittelbar vor einer Bibliotheksfunktion zurückkehrt (7.1.4).
    •   
    • Nachdem die Aktionen mit jedem formatierten Eingabe / Ausgabe-Funktion Konvertierungsspezifizierer zugeordnet (7.21.6, 7.29.2).
    •   
    • unmittelbar vor und unmittelbar nach jedem Anruf zu einer Vergleichsfunktion, und auch zwischen jedem Anruf zu einer Vergleichsfunktion und zu einer Bewegung der Objekte als Argumente für diesen Anruf übergeben (7.22.5).
    •   
  2.   

Der Wortlaut der gleichen Absatz rel="nofollow in C11 ist :

  
      
  1. Wenn eine Nebenwirkung auf eine skalare Objekt unsequenced relativ entweder eine andere Nebenwirkung auf demselben Skalar-Objekt oder ein Wertberechnung den Wert des gleichen Skalar-Objekt, das Verhalten ist nicht definiert. Wenn es mehrere zulässige Anordnungen der Teilausdrücke eines Ausdrucks sind, ist das Verhalten nicht definiert, wenn eine solche unsequenced Nebenwirkung in einer der orderings.84 auftritt)
  2.   

Sie können zum Beispiel unter Verwendung eine aktuelle Version von GCC mit -Wall und -Werror, solche Fehler in einem Programm erkennen und dann GCC wird geradezu verweigern Ihr Programm zu kompilieren. Im Folgenden ist die Ausgabe von gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20.161.005:

% 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 wichtigste Teil ist zu wissen, welche eine Sequenz Punkt ist - und was ist eine Sequenz Punkt und was nicht . Zum Beispiel der Komma-Operator ist eine Folge Punkt, so

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

ist gut definiert und wird i um eins zu erhöhen, den alten Wert ergibt, verwerfen Sie diesen Wert; dann bei Kommaoperator setzen sich die Nebenwirkungen; und dann erhöhen i nach dem anderen, und der resultierende Wert wird der Wert des Ausdrucks - das heißt das ist nur eine erfundene Art und Weise zu schreiben j = (i += 2) was noch ist wieder eine „kluge“ Art und Weise zu schreiben

i += 2;
j = i;

Allerdings ist die , in Funktionsargument Listen nicht ein Komma-Operator, und es gibt keinen Sequenzpunkt zwischen den Bewertungen verschiedenen Argumente; stattdessen sind ihre Bewertungen unsequenced bezüglich zueinander sind; so dass der Funktionsaufruf

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

hat nicht definiertes Verhalten weil zwischen den Bewertungen von i++ und ++i in Funktionsargumenten , und der Wert von i keinen Sequenzpunkt ist daher zweimal geändert, sowohl i++ und ++i, zwischen der vorherigen und der nächsten Sequenz Punkt.

https://stackoverflow.com/questions/29505280/incrementing-array-index- in-c fragte jemand eine Aussage wie etwa:

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

, die 7 druckt ... die OP erwartet, dass es 6 drucken.

Die ++i Schritte sind nicht alle abgeschlossen, bevor der Rest der Berechnungen garantiert. In der Tat werden verschiedene Compiler hier unterschiedliche Ergebnisse erhalten. Im Beispiel, das Sie zur Verfügung gestellt, der erste 2 ++i ausgeführt, dann wurden die Werte von k[] lesen, dann den letzten ++i dann k[].

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

Moderne Compiler wird diese sehr gut optimieren. In der Tat, vielleicht besser als der Code ursprünglich Sie schrieb (vorausgesetzt, hatte es die Art und Weise arbeitete man gehofft hatte).

Ihre Frage war wohl nicht: „Warum diese Konstrukte nicht definiertes Verhalten in C ist?“. Ihre Frage war wohl: „Warum dieser Code hat (mit ++) gib mir nicht den Wert erwartet?“, Und jemand markiert Ihre Frage als Duplikat und schickte Sie hier.

Diese Antwort versucht, diese Frage zu beantworten: Warum haben Sie den Code Ihnen nicht die Antwort, die Sie erwarten, und wie können Sie lernen zu erkennen (und zu vermeiden) Ausdrücke, die nicht wie erwartet funktionieren wird <. / p>

Ich nehme an, Sie haben die grundlegende Definition von C des ++ und -- Betreiber jetzt gehört, und wie das Präfix Form ++x unterscheidet sich von der Postfix-Form x++. Aber diese Operatoren sind schwer zu denken, so sicherstellen, dass Sie verstanden, vielleicht schreiben Sie ein winzig kleines Testprogramm mit so etwas wie

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

Aber zu Ihrer Überraschung, dieses Programm tat nicht Hilfe Sie verstehen - es gedruckt einig seltsamen, unerwarteten, unerklärlichen Ausgang was darauf hindeutet, dass vielleicht ++ tut etwas ganz anderes, nicht das, was Sie dachten, es getan hat.

Oder vielleicht an Sie suchen eine schwer zu verstehen Ausdruck wie

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

Vielleicht hat jemand Sie diesen Code als ein Puzzle. Dieser Code macht auch keinen Sinn, vor allem, wenn Sie es ausführen - und wenn Sie es unter zwei verschiedenen Compilern kompilieren und ausführen, sind Sie wahrscheinlich zwei verschiedene Antworten bekommen! Was ist damit? Welche Antwort ist richtig? (Und die Antwort ist, dass sie beide sind, oder keiner von ihnen sind).

Wie Sie jetzt gehört habe, sind alle diese Ausdrücke nicht definiert , was bedeutet, dass die C-Sprache keine Garantie über dem macht, was sie tun. Das ist ein seltsames und überraschendes Ergebnis, weil Sie wahrscheinlich gedacht, dass jedes Programm, das Sie schreiben können, solange es kompiliert und lief, würde eine einzigartige, gut definierte Ausgabe erzeugen. Aber im Fall von nicht definiertes Verhalten, das ist nicht so.

Was macht einen Ausdruck nicht definiert? Sind Ausdrücke ++ und -- denen immer nicht definiert? Natürlich nicht. Diese nützliche Operatoren sind, und wenn man sie richtig zu nutzen, sie sind sehr gut definierte

Für die Ausdrücke wir reden, was macht sie nicht definiert ist, wenn es zu viel auf einmal los, wenn wir nicht sicher sind, welche um die Dinge in passieren wird, aber wenn die Ordnung Angelegenheiten zu dem Ergebnis kommen wir.

Gehen wir zurück zu den beiden Beispiele, die ich in dieser Antwort benutzt habe. Als ich schrieb

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

ist die Frage, vor dem Aufruf printf, wird der Compiler den Wert von x berechnet ersten oder x++, oder vielleicht ++x? Aber es stellt sich heraus, wir wissen nicht . Es gibt keine Regel in C, die besagen, dass die Argumente für eine Funktion von links nach rechts ausgewertet erhalten, oder von rechts nach links oder in einer anderen Reihenfolge. So können wir nicht sagen, ob der Compiler x zuerst tun, dann ++x, dann x++ oder x++ dann ++x dann x oder eine andere Reihenfolge. Aber die Reihenfolge eindeutig von Bedeutung, weil je nachdem, welche die Compiler-Anwendungen bestellen, werden wir deutlich unterschiedliche Ergebnisse von printf gedruckt werden.

Was ist mit diesem verrückten Ausdruck?

x = x++ + ++x;

Das Problem mit diesem Ausdruck ist, dass es drei verschiedene Versuche enthält den Wert von x zu ändern: (1) der x++ Teil versucht 1 bis x hinzuzufügen, speichern Sie den neuen Wert in x, und gibt den alten Wert von x; (2) Der Teil ++x versucht 1 bis x hinzuzufügen, speichert den neuen Wert in x, und gibt den neuen Wert von x; und (3) die x = Teil versucht, die Summe der anderen beiden zurück zu x zuzuordnen. Welche dieser drei versuchten Zuordnungen „gewinnen“? Welche der drei Werte erhalten tatsächlich assigned x? Wieder und vielleicht überraschend, gibt es in C keine Regel, uns zu sagen.

Sie können diesen Vorrang oder Assoziativität oder von links nach rechts vorstellen Auswertung erfahren Sie, was um Dinge geschehen in, aber sie tun es nicht. Sie können mir nicht glauben, aber bitte mein Wort nehmen, und ich sage es noch einmal: Vorrang und Assoziativität bestimmen nicht jeden Aspekt der Auswertungsreihenfolge eines Ausdrucks in C. Insbesondere wenn innerhalb eines Ausdrucks es mehrere sind verschiedene Orte, an denen wir versuchen, einen neuen Wert zu so etwas wie x, Vorrang und Assoziativität zuweisen tun nicht uns sagen, welche dieser versuche geschieht erste oder letzte, oder irgendetwas.


So mit allem, Hintergrund und Einführung aus dem Weg, wenn Sie sicherstellen wollen, dass alle Ihre Programme sind gut definiert, welche Ausdrücke können Sie schreiben, und welche können Sie nicht schreiben?

Diese Ausdrücke sind alle in Ordnung:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

Diese Ausdrücke werden alle undefined:

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 Sie sagen können, welche Ausdrücke wohldefiniert, und die Ausdrücke sind nicht definiert?

Wie ich bereits sagte, die undefinierten Ausdrücke sind die, wo es auch ist viel auf einmal gehen, wo man kann nicht sicher sein, was um Dinge passieren in und wo die Reihenfolge wichtig:

  1. Wenn es eine Variable, ist geändert zu werden (zugewiesen) in zwei oder mehr verschiedenen Orten, wie Sie wissen, welche Modifikation geschieht zuerst?
  2. Wenn es eine Variable, die an einem Ort verändert wird immer, und seinen Wert an einem anderen Ort benutzt zu haben, wie Sie wissen, ob es den alten Wert oder den neuen Wert verwendet?

Als Beispiel # 1, in dem Ausdruck

x = x++ + ++x;

Es gibt drei Versuche, `x zu ändern.

Als Beispiel # 2, in dem Ausdruck

y = x + x++;

Wir verwenden beide den Wert von x, und ändern Sie es.

Das ist also die Antwort: Stellen Sie sicher, dass Sie in jedem Ausdruck schreiben, jede Variable höchstens einmal geändert wird, und wenn eine Variable geändert wird, können Sie versuchen Sie nicht auch woanders den Wert dieser Variablen zu verwenden.

Eine gute Erklärung, was bei dieser Art der Berechnung passiert, finden Sie im Dokument n1188 aus die 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 darf der gespeicherte Wert eines Objekts höchstens einmal durch die Auswertung eines Ausdrucks geändert werden.Darüber hinaus darf der vorherige Wert nur gelesen werden, um den zu speichernden Wert zu bestimmen.

Die Sequenz zeigt in einem Ausdruck wie i=i++ sind vorher i= und danach i++.

In der Arbeit, die ich oben zitiert habe, wird erklärt, dass man sich das Programm so vorstellen kann, dass es aus kleinen Kästchen besteht, wobei jedes Kästchen die Anweisungen zwischen zwei aufeinanderfolgenden Sequenzpunkten enthält.Die Sequenzpunkte sind im Anhang C der Norm definiert i=i++ Es gibt zwei Sequenzpunkte, die einen vollständigen Ausdruck begrenzen.Ein solcher Ausdruck ist syntaktisch äquivalent mit einem Eintrag von expression-statement in der Backus-Naur-Form der Grammatik (eine Grammatik ist in Anhang A des Standards enthalten).

Die Reihenfolge der Anweisungen in einer Box hat also keine klare Reihenfolge.

i=i++

kann interpretiert werden als

tmp = i
i=i+1
i = tmp

oder als

tmp = i
i = tmp
i=i+1

Weil beide Formen den Code interpretieren i=i++ gültig sind und da beide unterschiedliche Antworten generieren, ist das Verhalten undefiniert.

Am Anfang und am Ende jedes Kästchens, aus dem das Programm besteht, ist ein Sequenzpunkt zu erkennen [die Kästchen sind atomare Einheiten in C], und innerhalb eines Kästchens ist die Reihenfolge der Anweisungen nicht in allen Fällen definiert.Wenn man diese Reihenfolge ändert, kann sich manchmal das Ergebnis ändern.

BEARBEITEN:

Eine weitere gute Quelle zur Erklärung solcher Unklarheiten sind die Einträge aus c-faq Website (auch veröffentlicht als Buch), nämlich Hier Und Hier Und Hier .

Der Grund dafür ist, dass das Programm nicht definiertes Verhalten ausgeführt wird. Das Problem liegt in der Auswertungsreihenfolge, weil es keine Sequenzpunkte erforderlich ist gemäß 98 C ++ Standard (ohne Operationen vor oder nach der anderen sequenzierten gemäß C ++ 11-Terminologie).

Wenn Sie jedoch an einen Compiler halten, werden Sie das Verhalten nachhaltig, so lange finden, da Sie Funktionsaufrufe oder Zeiger fügen, die das Verhalten mehr unordentlich machen würde.

  • Also zuerst die 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 arbeiten? es auswertet Unterausdrücke in einer von links nach rechts, damit der rechten Seite (RHS), wird der Wert auf der linken Seite zuordnet (LHS). Das ist genau wie Java und C # verhalten und ihre Standards definieren. (Ja, die äquivalente Software in Java und C # hat Verhalten definiert). Es bewertet jeden Unterausdruck nacheinander in der RHS-Anweisung in einer Reihenfolge von links nach rechts; für jeden Unter expression: die ++ c (pre-increment) zuerst wird der Wert c für den Betrieb verwendet wird, bewertet, dann das Post-Inkrement c ++)

.

entsprechend GCC C ++: Operatoren

  

In GCC C ++, die Priorität der Operatoren bestimmt die Reihenfolge, in   welche die einzelnen Betreiber ausgewertet

der entsprechende Code in definiertem Verhalten C ++ als 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 href="https://www.visualstudio.com/" rel="nofollow noreferrer"> Visual Studio

#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 Visual Studio arbeiten, es eine anderen Ansatz nimmt, wertet sie alle prä-Schritte Ausdrücke in ersten Durchlauf verwendet dann Variablen Werte in den Operationen in zweitem Durchlauf von RHS zu LHS weist es in dritten Durchgang, dann letzten Pass wertet alle Nachinkrement Ausdrücke in einem Durchgang.

So die äquivalent in definiertem Verhalten C ++ als Visual C ++ versteht:

#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 
}

als Visual Studio Dokumentation wird unter Präzedenz und Reihenfolge der Bewertung :

  

Wenn mehrere Operatoren zusammen erscheinen, haben sie gleich Vorrang und werden entsprechend ihrer Assoziativität bewertet. Die Betreiber in der Tabelle sind in den Abschnitten beginnend mit Postfix-Operatoren beschrieben.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top