917 Stimmen

Warum verwenden diese Konstrukte undefiniertes Verhalten vor und nach der Inkrementierung?

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

16 Stimmen

@Jarett, nein, ich brauchte nur ein paar Hinweise auf "Sequenzpunkte". Bei der Arbeit fand ich ein Stück Code mit i = i++, ich dachte "Das ändert den Wert von i nicht". Ich habe es getestet und mich gefragt, warum. Inzwischen habe ich diese Anweisung entfernt und durch i++ ersetzt;

232 Stimmen

Ich finde es interessant, dass jeder IMMER davon ausgeht, dass solche Fragen gestellt werden, weil der Fragesteller das fragliche Konstrukt BENUTZEN will. Meine erste Vermutung war, dass PiX weiß, dass diese Konstrukte schlecht sind, aber neugierig ist, warum sie sich so verhalten, egal welchen Compiler er/sie benutzt... Und ja, was unWind sagte... es ist undefiniert, es könnte alles tun... einschließlich JCF (Jump and Catch Fire)

41 Stimmen

Ich bin neugierig: Warum warnen Compiler anscheinend nicht vor Konstrukten wie "u = u++ + ++u;", wenn das Ergebnis undefiniert ist?

633voto

unwind Punkte 377331

In C gibt es das Konzept des undefinierten Verhaltens, d. h. einige Sprachkonstrukte sind syntaktisch gültig, aber man kann das Verhalten bei der Ausführung des Codes nicht vorhersagen.

Soweit ich weiß, sagt die Norm nicht ausdrücklich なぜ das Konzept des undefinierten Verhaltens existiert. Meiner Meinung nach liegt es einfach daran, dass die Sprachdesigner einen gewissen Spielraum in der Semantik haben wollten. Anstatt z.B. zu verlangen, dass alle Implementierungen Integer-Überläufe auf genau die gleiche Weise behandeln, was sehr wahrscheinlich zu ernsthaften Leistungseinbußen führen würde, haben sie das Verhalten einfach undefiniert gelassen, so dass alles passieren kann, wenn man Code schreibt, der einen Integer-Überlauf verursacht.

Warum sind das also "Probleme"? Die Sprache besagt eindeutig, dass bestimmte Dinge dazu führen undefiniertes Verhalten . Es gibt kein Problem, es ist kein "sollte" beteiligt. Wenn sich das undefinierte Verhalten ändert, wenn eine der beteiligten Variablen deklariert wird volatile Das beweist oder ändert nichts. Es ist undefiniert Sie können nicht auf das Verhalten schließen.

Ihr am interessantesten aussehendes Beispiel, dasjenige mit

u = (u++);

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

11 Stimmen

@PiX: Die Dinge sind aus einer Reihe von Gründen undefiniert. Dazu gehören: Es gibt kein eindeutiges "richtiges Ergebnis", verschiedene Maschinenarchitekturen würden unterschiedliche Ergebnisse begünstigen, die bestehende Praxis ist nicht konsistent oder liegt außerhalb des Anwendungsbereichs der Norm (z. B. welche Dateinamen gültig sind).

7 Stimmen

Nur um alle zu verwirren, sind einige dieser Beispiele jetzt in C11 genau definiert, z. B. i = ++i + 1; .

0 Stimmen

Ich habe die Frage bearbeitet, um die UB bei der Auswertung von Funktionsargumenten hinzuzufügen, da diese Frage oft als Duplikat dafür verwendet wird. (Das letzte Beispiel)

103voto

haccks Punkte 100455

Die meisten der hier zitierten Antworten stammen aus dem C-Standard und betonen, dass das Verhalten dieser Konstrukte nicht definiert ist. Zum Verständnis warum das Verhalten dieser Konstrukte undefiniert ist Wir wollen diese Begriffe zunächst im Zusammenhang mit der C11-Norm verstehen:

Sequenziert: (5.1.2.3)

Bei zwei beliebigen Bewertungen A y B wenn A sequenziert wird, bevor B dann die Ausführung von A geht der Ausführung von B .

Nicht sequenziert:

Si A wird nicht vor oder nach B entonces A y B sind nicht sequenziert.

Es gibt zwei Arten von Evaluierungen:

  • Wertberechnungen die das Ergebnis eines Ausdrucks ausrechnen; und
  • Nebeneffekte die Modifikationen von Objekten sind.

Sequenzpunkt:

Das Vorhandensein eines Sequenzpunktes zwischen der Auswertung von Ausdrücken A y B impliziert, dass jeder Wertberechnung y Nebenwirkung verbunden mit A wird vor jeder Wertberechnung y Nebenwirkung verbunden mit B .

Nun zu der Frage, für die Ausdrücke wie

int i = 1;
i = i++;

Norm besagt das:

6.5 Ausdrücke:

Wenn ein Seiteneffekt auf ein skalares Objekt relativ zu einem anderen Objekt nicht sequenziert ist entweder eine andere Nebenwirkung auf dasselbe skalare Objekt oder eine Wertberechnung mit dem Wert desselben skalaren Objekts, das Verhalten ist undefiniert . [...]

Daher ruft der obige Ausdruck UB auf, weil zwei Seiteneffekte auf dasselbe Objekt i im Verhältnis zueinander nicht sequenziert ist. Das heißt, es ist nicht sequenziert, ob die Nebenwirkung durch die Zuordnung zu i erfolgt vor oder nach der Nebenwirkung durch ++ .
Je nachdem, ob die Zuweisung vor oder nach dem Inkrement erfolgt, werden unterschiedliche Ergebnisse erzielt, und das ist der Fall von undefiniertes Verhalten .

Benennen wir die i links von der Zuordnung sein il und auf der rechten Seite der Zuordnung (im Ausdruck i++ ) sein ir dann sieht der Ausdruck wie folgt aus

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 zu Postfix ++ Operator ist das:

nur weil die ++ nach der Variablen kommt, bedeutet nicht, dass die Erhöhung zu spät erfolgt . Das Inkrement kann so früh erfolgen, wie es der Compiler wünscht solange der Compiler sicherstellt, dass der ursprüngliche Wert verwendet wird .

Es bedeutet, dass der Ausdruck il = ir++ könnte entweder bewertet werden als

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

o

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 führt 1 y 2 die von der Reihenfolge der Nebeneffekte durch Zuweisung und ++ und ruft daher UB auf.

82voto

Christoph Punkte 157217

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

Zwischen dem vorherigen und dem nächsten Sequenzpunkt muss ein Objekt seinen gespeicherten Wert haben höchstens einmal durch die Auswertung eines Ausdrucks geändert werden. Außerdem 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 das Ergebnis eines Zuweisungsoperators zu verändern oder nach dem nächsten Sequenzpunkt darauf zuzugreifen, ist das ist das Verhalten undefiniert.

2 Stimmen

Würde dies bedeuten, dass "i=i=5;" ein undefiniertes Verhalten wäre?

2 Stimmen

@supercat so weit ich weiß i=i=5 ist auch undefiniertes Verhalten

3 Stimmen

@Zaibis: Die Begründung, die ich gerne für die meisten Stellen verwende, besagt, dass eine Mehrprozessor-Plattform theoretisch etwas implementieren könnte wie A=B=5; wie "Write-Lock A; Write-Lock B; Store 5 to A; store 5 to B; Unlock B; Unock A;", und eine Anweisung wie C=A+B; als "Leseschloss A; Leseschloss B; A+B berechnen; A und B entsperren; Schreibschloss C; Ergebnis speichern; C entsperren;". Das würde sicherstellen, dass, wenn ein Thread A=B=5; ein anderer hat C=A+B; würde der letztgenannte Thread entweder beide Schreibvorgänge als stattgefunden ansehen oder keinen. Möglicherweise eine nützliche Garantie. Wenn ein Thread I=I=5; jedoch ...

76voto

badp Punkte 11166

Kompilieren und disassemblieren Sie einfach Ihre Code-Zeile, wenn Sie wissen wollen, wie Sie genau das bekommen, was Sie bekommen.

Dies ist die Meldung, die ich auf meinem Rechner erhalte, zusammen mit dem, was ich denke, was vor sich geht:

$ 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 die Anweisung 0x00000014 eine Art Compiler-Optimierung war?)

0 Stimmen

Wie erhalte ich den Maschinencode? Ich verwende Dev C++, und ich habe mit der Option "Code Generation" in den Compiler-Einstellungen herumgespielt, aber es gibt keine zusätzliche Dateiausgabe oder eine Konsolenausgabe

5 Stimmen

@ronnieaka gcc evil.c -c -o evil.bin y gdb evil.bin disassemble evil oder was auch immer die Windows-Entsprechungen davon sind :)

0 Stimmen

Ist -0x4(%ebp) = 4 am Ende?

66voto

Shafik Yaghmour Punkte 147749

Das Verhalten kann nicht wirklich erklärt werden, da es sowohl die nicht spezifiziertes Verhalten y undefiniertes Verhalten Daher können wir keine allgemeinen Vorhersagen über diesen Code machen, obwohl, wenn Sie lesen Olve Maudal's Arbeiten wie Tief C y Nicht spezifiziert und unbestimmt Manchmal kann man in sehr spezifischen Fällen mit einem bestimmten Compiler und einer bestimmten Umgebung gute Vermutungen anstellen, aber bitte tun Sie das nicht in der Nähe der Produktion.

Also weiter zu nicht spezifiziertes Verhalten , in Entwurf der C99-Norm Abschnitt 6.5 Absatz 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 Operatoren Funktionsaufruf (), &&, ||, ?: und Komma), die Reihenfolge der Auswertung von Unterausdrücken und die Reihenfolge, in der Nebeneffekte auftreten, sind beide nicht spezifiziert.

Wenn wir also eine Zeile wie diese haben:

i = i++ + ++i;

wir wissen nicht, ob i++ ou ++i wird zuerst ausgewertet. Dies dient hauptsächlich dazu, dem Compiler bessere Möglichkeiten zur Optimierung .

Wir haben auch undefiniertes Verhalten auch hier, da das Programm Variablen verändert( i , u usw..) mehr als einmal zwischen Reihenfolgepunkte . Aus dem Entwurf des Standardabschnitts 6.5 Absatz 2 ( Hervorhebung von mir ):

Zwischen dem vorherigen und dem nächsten Sequenzpunkt muss ein Objekt seinen gespeicherten Wert haben höchstens einmal geändert durch die Auswertung eines Ausdrucks. Außerdem, der vorherige Wert darf nur gelesen werden, um den zu speichernden Wert zu bestimmen .

werden die folgenden Codebeispiele als undefiniert aufgeführt:

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

In all diesen Beispielen versucht der Code, ein Objekt mehr als einmal im selben Sequenzpunkt zu ändern, was mit dem ; in jedem einzelnen dieser Fälle:

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

Nicht spezifiziertes Verhalten ist definiert in der Entwurf der C99-Norm im Abschnitt 3.4.4 als:

Verwendung eines nicht spezifizierten Wertes oder ein anderes Verhalten, für das diese Internationale Norm zwei oder mehr Möglichkeiten vorsieht zwei oder mehr Möglichkeiten vorsieht und keine weiteren Anforderungen daran stellt, welche davon in jedem Fall gewählt wird Beispiel

und undefiniertes Verhalten ist definiert in Abschnitt 3.4.3 als:

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

und stellt fest, dass:

Das mögliche undefinierte Verhalten reicht vom völligen Ignorieren der Situation mit unvorhersehbaren Ergebnissen über das Verhalten während der Übersetzung oder Programmausführung in einer dokumentierten, für die Umgebung charakteristischen Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis hin zum Abbruch einer Übersetzung oder Ausführung (mit Ausgabe einer Diagnosemeldung).

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X