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?

15voto

Steve Summit Punkte 37650

Ihre Frage lautete wahrscheinlich nicht: "Warum sind diese Konstrukte in C undefiniertes Verhalten?". Ihre Frage war wahrscheinlich: "Warum hat dieser Code (mit ++ ) nicht den Wert, den ich erwartet habe?", und jemand hat Ihre Frage als Duplikat markiert und Sie hierher geschickt.

この answer versucht, diese Frage zu beantworten: Warum hat Ihr Code nicht die erwartete Antwort gegeben, und wie können Sie lernen, Ausdrücke zu erkennen (und zu vermeiden), die nicht wie erwartet funktionieren.

Ich nehme an, Sie kennen die grundlegende Definition von C's ++ y -- Operatoren, und wie die Präfixform ++x unterscheidet sich von der Postfixform x++ . Aber diese Operatoren sind schwer zu begreifen. Um sicher zu gehen, dass Sie sie verstanden haben, haben Sie vielleicht ein kleines Testprogramm geschrieben, das etwas wie

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

Aber zu Ihrer Überraschung hat dieses Programm no zu verstehen - es gab eine seltsame, unerklärliche Ausgabe, die darauf hindeutete, dass vielleicht ++ macht etwas ganz anderes, ganz und gar nicht das, was Sie dachten.

Oder Sie haben es mit einem schwer verständlichen Ausdruck zu tun wie

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

Vielleicht hat Ihnen jemand den Code als Rätsel aufgegeben. Auch dieser Code ergibt keinen Sinn, vor allem wenn Sie ihn ausführen - und wenn Sie ihn mit zwei verschiedenen Compilern kompilieren und ausführen, erhalten Sie wahrscheinlich zwei verschiedene Antworten! Was ist da los? Welche Antwort ist richtig? (Und die Antwort ist, dass beide richtig sind, oder keine von beiden.)

Wie Sie bereits gehört haben, sind diese Ausdrücke undefiniert Das bedeutet, dass die Sprache C keine Garantie dafür gibt, was sie tun werden. Das ist ein seltsames und beunruhigendes Ergebnis, denn Sie dachten wahrscheinlich, dass jedes Programm, das Sie schreiben können, solange es kompiliert und ausgeführt wird, eine eindeutige, wohldefinierte Ausgabe erzeugt. Aber im Fall von undefiniertem Verhalten ist das nicht der Fall.

Was macht einen Ausdruck undefiniert? Sind Ausdrücke mit ++ y -- immer undefiniert? Natürlich nicht: Es handelt sich um nützliche Operatoren, und wenn man sie richtig einsetzt, sind sie vollkommen wohldefiniert.

Die Ausdrücke, um die es hier geht, sind dann undefiniert, wenn zu viel auf einmal passiert, wenn wir nicht sagen können, in welcher Reihenfolge die Dinge passieren werden, aber wenn die Reihenfolge für das Ergebnis wichtig ist.

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, ob man vor dem eigentlichen Aufruf printf errechnet der Compiler den Wert von x zuerst, oder x++ oder vielleicht ++x ? Aber es stellt sich heraus Wir wissen es nicht . Es gibt in C keine Regel, die besagt, dass die Argumente einer Funktion von links nach rechts, von rechts nach links oder in einer anderen Reihenfolge ausgewertet werden. Wir können also nicht sagen, ob der Compiler Folgendes tun wird x zuerst, dann ++x entonces x++ , oder x++ entonces ++x entonces x oder eine andere Reihenfolge. Aber die Reihenfolge spielt eine Rolle, denn je nachdem, welche Reihenfolge der Compiler verwendet, erhalten wir eine andere Zahlenreihe ausgedruckt.

Was ist mit diesem verrückten Ausdruck?

x = x++ + ++x;

Das Problem bei diesem Ausdruck ist, dass er drei verschiedene Versuche enthält, den Wert von x (1) die x++ Teil versucht zu nehmen x Wertes, addieren Sie 1 und speichern Sie den neuen Wert in x und geben den alten Wert zurück; (2) die ++x Teil versucht zu nehmen x Wertes, addieren Sie 1 und speichern Sie den neuen Wert in x und geben den neuen Wert zurück; und (3) die x = Teil versucht, die Summe der beiden anderen Teile wieder auf x . Welche dieser drei versuchten Zuweisungen wird "gewinnen"? Welcher der drei Werte wird tatsächlich den Endwert von x ? Auch hier gibt es, was vielleicht überrascht, keine Regel in C, die uns das sagt.

Man könnte meinen, dass der Vorrang oder die Assoziativität oder die Auswertung von links nach rechts Aufschluss darüber gibt, in welcher Reihenfolge die Dinge geschehen, aber das ist nicht der Fall. Sie werden mir vielleicht nicht glauben, aber nehmen Sie mich beim Wort, und ich sage es noch einmal: Vorrang und Assoziativität bestimmen nicht jeden Aspekt der Auswertungsreihenfolge eines Ausdrucks in C. Insbesondere, wenn es innerhalb eines Ausdrucks mehrere verschiedene Stellen gibt, an denen wir versuchen, einen neuen Wert für etwas wie x , Vorrang und Assoziativität tun no Sie können uns nicht sagen, welcher dieser Versuche als erster oder als letzter oder sonst wie stattfindet.


Wenn Sie also sichergehen wollen, dass alle Ihre Programme wohldefiniert sind, 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 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 lautet: Wie kann man feststellen, welche Ausdrücke wohldefiniert und welche undefiniert sind?

Wie ich bereits sagte, sind die undefinierten Ausdrücke diejenigen, bei denen zu viel auf einmal passiert, bei denen man nicht sicher sein kann, in welcher Reihenfolge die Dinge passieren, und bei denen die Reihenfolge eine Rolle spielt:

  1. Wenn eine Variable an zwei oder mehr Stellen geändert (zugewiesen) wird, woher wissen Sie dann, 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 dann, ob der alte oder der neue Wert verwendet wird?

Ein Beispiel für #1 ist der Ausdruck

x = x++ + ++x;

gibt es drei Versuche zur Änderung x .

Als Beispiel für #2, in dem Ausdruck

y = x + x++;

verwenden wir beide den Wert von x und ändern Sie sie.

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


Noch eine Sache. Sie fragen sich vielleicht, wie man die undefinierten Ausdrücke "repariert", die ich zu Beginn dieser Antwort vorgestellt habe.

Im Fall von printf("%d %d %d\n", x, ++x, x++); Es ist ganz einfach - schreiben Sie es einfach als drei separate printf Anrufe:

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

Jetzt ist das Verhalten genau definiert, und Sie werden vernünftige Ergebnisse erhalten.

Im Fall von x = x++ + ++x Auf der anderen Seite gibt es keine Möglichkeit, es zu reparieren. Es gibt keine Möglichkeit, ihn so zu schreiben, dass er sich garantiert so verhält, wie Sie es erwarten - aber das ist in Ordnung, denn Sie würden niemals einen Ausdruck schreiben wie x = x++ + ++x in einem echten Programm sowieso.

11voto

TomOnTime Punkte 3818

Sur https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c Jemand fragte 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);

der 7 druckt... der OP hat erwartet, dass er 6 druckt.

El ++i Inkremente sind nicht garantiert, dass alle vor dem Rest der Berechnungen abgeschlossen sind. Tatsächlich werden verschiedene Compiler hier unterschiedliche Ergebnisse erzielen. In dem von Ihnen angeführten Beispiel werden die ersten 2 ++i ausgeführt, dann werden die Werte von k[] gelesen wurden, dann die letzte ++i entonces k[] .

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

Moderne Compiler optimieren dies sehr gut. Möglicherweise sogar besser als der Code, den Sie ursprünglich geschrieben haben (vorausgesetzt, er hat so funktioniert, wie Sie gehofft hatten).

6voto

alinsoar Punkte 14685

Eine gute Erklärung, was bei dieser Art von Berechnung passiert, finden Sie in dem Dokument n1188 de die ISO W14-Website .

Ich erkläre die Ideen.

Die wichtigste Regel 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. Außerdem darf der vorherige Wert nur gelesen werden, um den zu speichernden Wert zu bestimmen.

Die Sequenzpunkte in einem Ausdruck wie i=i++ sind vor i= und nach i++ .

In der oben zitierten Abhandlung wird erklärt, dass das Programm aus kleinen Kästchen besteht, wobei jedes Kästchen die Anweisungen zwischen 2 aufeinanderfolgenden Sequenzpunkten enthält. Die Sequenzpunkte sind in Anhang C der Norm definiert, im Fall von i=i++ Es gibt 2 Sequenzpunkte, die einen vollständigen Ausdruck abgrenzen. Ein solcher Ausdruck ist syntaktisch gleichwertig mit einem Eintrag von expression-statement in der Backus-Naur-Form der Grammatik (eine Grammatik ist in Anhang A der Norm enthalten).

Die Reihenfolge der Anweisungen in einer Schachtel hat also keine klare Ordnung.

i=i++

kann interpretiert werden als

tmp = i
i=i+1
i = tmp

oder als

tmp = i
i = tmp
i=i+1

weil alle diese Formen den Code zu interpretieren i=i++ gültig sind und weil beide unterschiedliche Antworten erzeugen, ist das Verhalten undefiniert.

Ein Sequenzpunkt kann also durch den Anfang und das Ende jedes Kästchens, aus dem das Programm besteht, gesehen werden [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 man manchmal das Ergebnis ändern.

EDIT:

Eine weitere gute Quelle zur Erläuterung solcher Unklarheiten sind die Einträge aus c-faq Website (auch veröffentlicht als Buch ), nämlich ici y ici y ici .

0 Stimmen

Wie fügt diese Antwort den bestehenden Antworten etwas Neues hinzu? Auch die Erklärungen für i=i++ ist sehr ähnlich zu diese Antwort .

0 Stimmen

@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 Seite der ISO 9899 gelernt habe open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf

0 Stimmen

@haccks diese Antwort ist in Ordnung, abgesehen davon, dass sie eine Kopie deiner Antwort ist, aber ich würde stattdessen fragen, was alle anderen Antworten hier machen und warum sie so viel Ansehen haben, während sie den Hauptpunkt der Frage verfehlen, nämlich die Details der UB in Beispielen zu erklären.

3voto

Mohamed El-Nakeep Punkte 6360

Der Grund dafür ist, dass das Programm ein undefiniertes Verhalten zeigt. Das Problem liegt in der Auswertungsreihenfolge, da es keine Sequenzpunkte gibt, die nach dem C++98-Standard erforderlich sind (nach der C++11-Terminologie wird keine Operation vor oder nach einer anderen angeordnet).

Wenn Sie sich jedoch an einen Compiler halten, werden Sie feststellen, dass das Verhalten beständig ist, solange Sie keine Funktionsaufrufe oder Zeiger hinzufügen, was das Verhalten noch unübersichtlicher machen würde.

Verwendung von Nuwen MinGW 15 GCC 7.1 werden Sie erhalten:

 #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 der GCC? Er wertet Unterausdrücke in der Reihenfolge von links nach rechts für die rechte Seite (RHS) aus und weist den Wert dann der linken Seite (LHS) zu. Dies ist genau die Art und Weise, wie Java und C# sich verhalten und ihre Standards definieren. (Ja, die entsprechende Software in Java und C# hat definierte Verhaltensweisen). Sie wertet jeden Unterausdruck in der RHS-Anweisung einzeln in der Reihenfolge von links nach rechts aus; für jeden Unterausdruck wird zuerst ++c (Vorinkrement) ausgewertet, dann wird der Wert c für die Operation verwendet, dann das Nachinkrement c++).

laut GCC C++: Operatoren

In GCC C++ steuert der Vorrang der Operatoren die Reihenfolge in die Reihenfolge, in der die einzelnen Operatoren ausgewertet werden

den äquivalenten Code in C++ mit definiertem Verhalten, wie GCC ihn 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 
}

Visual Studio verfolgt einen anderen Ansatz: Es wertet alle Pre-Increment-Ausdrücke im ersten Durchgang aus, verwendet dann die Variablenwerte in den Operationen im zweiten Durchgang, weist im dritten Durchgang von RHS nach LHS zu und wertet dann im letzten Durchgang alle Post-Increment-Ausdrücke in einem Durchgang aus.

Also das Äquivalent in definiertem Verhalten C++, wie es 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 
}

wie in der Visual Studio-Dokumentation unter Vorrang und Reihenfolge der Bewertung :

Wenn mehrere Operatoren zusammen auftreten, haben sie den gleichen Vorrang und werden entsprechend ihrer Assoziativität ausgewertet. Die in der Tabelle aufgeführten Operatoren werden in den Abschnitten ab Postfix-Operatoren beschrieben.

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

1 Stimmen

Die Frage bezieht sich auch auf c jetzt, nicht C++

1 Stimmen

Wenn Sie sich jedoch an einen Compiler halten, werden Sie feststellen, dass das Verhalten beständig ist. Nun, nein, nicht unbedingt. Wenn Sie z. B. Optimierungsflags ändern, kann es leicht passieren, dass der Compiler Code ausgibt, der das undefinierte Verhalten anders aussehen lässt. Auch wenn Sie scheinbar zusammenhangslose Änderungen an benachbartem Code vornehmen.

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