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?

51voto

Steve Summit Punkte 37650

Eine andere Möglichkeit, diese Frage zu beantworten, anstatt sich in obskuren Details von Sequenzpunkten und undefiniertem Verhalten zu verzetteln, ist einfach zu fragen, was sollen sie bedeuten? Was wollte der Programmierer damit bezwecken?

Das erste Fragment fragte nach, i = i++ + ++i ist in meinen Augen ganz klar verrückt. Niemand würde es jemals in ein echtes Programm schreiben, es ist nicht offensichtlich, was es tut, es gibt keinen denkbaren Algorithmus, den jemand versucht haben könnte, zu kodieren, der zu dieser speziellen 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 meiner Meinung nach auch in Ordnung, wenn der Compiler nicht herausfinden kann, was es tun soll.

Das zweite Fragment, i = i++ ist etwas leichter zu verstehen. Jemand versucht eindeutig, i zu inkrementieren und das Ergebnis wieder i zuzuweisen. Aber es gibt mehrere Möglichkeiten, dies in C zu tun. Die einfachste Möglichkeit, 1 zu i zu addieren und das Ergebnis wieder i zuzuweisen, ist in fast jeder Programmiersprache gleich:

i = i + 1

C hat natürlich eine praktische Abkürzung:

i++

Das bedeutet: "Addiere 1 zu i, und weise das Ergebnis wieder i zu". Wenn wir also ein Mischmasch aus beidem konstruieren, indem wir schreiben

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, also stört es mich nicht allzu sehr, wenn der Compiler auch verwirrt ist.

Realistischerweise werden diese verrückten Ausdrücke nur dann geschrieben, wenn man sie als künstliche Beispiele dafür verwendet, wie ++ funktionieren soll. Und natürlich ist es wichtig, zu verstehen, wie ++ funktioniert. Eine praktische Regel für die Verwendung von ++ lautet jedoch: "Wenn es nicht offensichtlich ist, was ein Ausdruck mit ++ bedeutet, schreiben Sie ihn nicht."

Wir haben früher unzählige Stunden in comp.lang.c damit verbracht, Ausdrücke wie diese zu diskutieren und なぜ sie sind undefiniert. Zwei meiner längeren Antworten, die wirklich zu erklären versuchen, warum, sind im Internet archiviert:

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

1 Stimmen

Ein ziemlich unangenehmer Nachteil in Bezug auf Undefined Behavior ist, dass es zwar gebraucht auf 99,9 % der Compiler sicher zu verwenden *p=(*q)++; zu bedeuten if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE; ist dies nicht mehr der Fall. Hyper-modernes C würde so etwas wie die letztgenannte Formulierung erfordern (obwohl es keinen Standardweg gibt, um anzuzeigen, dass es dem Code egal ist, was in *p ), um den Grad an Effizienz zu erreichen, den die Compiler mit der erstgenannten Methode bieten (die else Klausel ist notwendig, damit der Compiler die Optimierung der if was bei einigen neueren Compilern erforderlich wäre).

0 Stimmen

@supercat Ich glaube jetzt, dass jeder Compiler, der "schlau" genug ist, um diese Art von Optimierung durchzuführen, auch schlau genug sein muss, um einen Blick auf assert Anweisungen, so dass der Programmierer der betreffenden Zeile eine einfache Anweisung vorangestellt werden kann assert(p != q) . (Natürlich würde die Teilnahme an diesem Kurs auch eine Neuformulierung der <assert.h> in Nicht-Debug-Versionen nicht vollständig zu löschen, sondern sie in etwas wie __builtin_assert_disabled() die der Compiler sehen kann und für die er dann keinen Code ausgibt).

0 Stimmen

Was wir wirklich sagen, ist "addiere 1 zu i, und weise das Ergebnis wieder i zu, und weise das Ergebnis wieder i zu". --- Ich glaube, es gibt einen " und weisen Sie das Ergebnis wieder i " zu viel.

33voto

P.P Punkte 111806

Oft wird diese Frage als Duplikat von Fragen zum Code verknüpft wie

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

o

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

oder ähnliche Varianten.

Dies ist zwar auch undefiniertes Verhalten wie bereits erwähnt, gibt es feine Unterschiede, wenn printf() bei einem Vergleich mit einer Aussage wie der folgenden:

x = i++ + i++;

In der folgenden Erklärung:

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

die Reihenfolge der Bewertung von Argumenten in printf() est nicht spezifiziert . Das heißt, Ausdrücke i++ y ++i können in beliebiger Reihenfolge ausgewertet werden. C11-Norm enthält einige einschlägige Beschreibungen dazu:

Anhang J, nicht spezifizierte Verhaltensweisen

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

3.4.4, nicht spezifiziertes Verhalten

Verwendung eines nicht spezifizierten Wertes oder ein anderes Verhalten, bei dem dieser Internationale Norm zwei oder mehr Möglichkeiten vorsieht und keine weiteren Anforderungen an die jeweils zu wählende Variante stellt.

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

El nicht näher bezeichnetes Verhalten selbst ist KEIN Thema. Betrachten Sie dieses Beispiel:

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

Auch dies hat nicht näher bezeichnetes Verhalten weil die Reihenfolge der Auswertung von ++x y y++ ist nicht spezifiziert. Aber es ist eine völlig legale und gültige Aussage. Es gibt keine undefiniertes Verhalten in dieser Anweisung. Da die Änderungen ( ++x y y++ ) werden durchgeführt, um deutlich Objekte.

Was besagt die folgende Aussage

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

als undefiniertes Verhalten ist die Tatsache, dass diese beiden Ausdrücke die dieselbe Objekt i ohne ein dazwischenliegendes Sequenzpunkt .


Ein weiteres Detail ist, dass die Komma die an dem printf()-Aufruf beteiligt ist, ist eine Abscheider , nicht die Komma-Operator .

Dies ist eine wichtige Unterscheidung, da die Komma-Operator führt eine Sequenzpunkt zwischen der Auswertung ihrer Operanden, so dass das Folgende legal 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 Komma-Operator wertet seine Operanden von links nach rechts aus und gibt nur den Wert des letzten Operanden wieder. Also in j = (++i, i++); , ++i Inkremente i a 6 y i++ ergibt den alten Wert von i ( 6 ), der zugewiesen ist j . Dann i wird 7 aufgrund der Nacherhöhung.

Wenn also die Komma im Funktionsaufruf ein Komma-Operator sein sollte, dann

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

wird kein Problem sein. Aber es ruft auf undefiniertes Verhalten weil die Komma hier ist ein Abscheider .


Für diejenigen, die neu sind bei undefiniertes Verhalten von der Lektüre profitieren würden 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: Unbestimmtes, nicht spezifiziertes und implementierungsdefiniertes Verhalten ist ebenfalls relevant.

0 Stimmen

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 zeigen (Auswertung von Argumenten von rechts nach links in gcc v7.3.0; Ergebnis "a=110 b=40 c=60"). Liegt es daran, dass die Zuweisungen als 'Full-Statements' betrachtet werden und somit einen Sequenzpunkt einführen? Sollte das nicht zu einer Argument-/Anweisungsauswertung von links nach rechts führen? Oder handelt es sich nur um eine Manifestation eines undefinierten Verhaltens?

1 Stimmen

@kavadias Die printf-Anweisung hat ein undefiniertes Verhalten, und zwar aus demselben Grund wie oben beschrieben. Sie schreiben b y c im 3. bzw. 4. Argument und Lesen im 2. Aber es gibt keine Reihenfolge zwischen diesen Ausdrücken (2., 3. & 4. Argument). gcc/clang hat eine Option -Wsequence-point die auch bei der Suche nach diesen helfen können.

23voto

supercat Punkte 72939

Obwohl es unwahrscheinlich ist, dass Compiler und Prozessoren dies tatsächlich tun würden, wäre es nach dem C-Standard legal, dass der Compiler "i++" mit der Sequenz 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

Während ich nicht glaube, dass irgendein Prozessor die Hardware unterstützt, um so etwas effizient zu ermöglichen, kann man sich leicht Situationen vorstellen, in denen ein solches Verhalten Multi-Thread-Code einfacher machen würde (z.B. würde es garantieren, dass wenn zwei Threads versuchen, die obige Sequenz gleichzeitig auszuführen, i um zwei inkrementiert wird), und es ist nicht völlig undenkbar, dass ein zukünftiger Prozessor eine solche Funktion bereitstellen könnte.

Wenn der Compiler Folgendes schreiben würde i++ wie oben angegeben (nach dem Standard legal) und würde die obigen Anweisungen in die Auswertung des gesamten Ausdrucks einstreuen (ebenfalls legal), und wenn es nicht zufällig bemerken würde, dass eine der anderen Anweisungen zufällig auf i wäre es möglich (und legal), dass der Compiler eine Befehlsfolge erzeugt, die zu einer Blockierung führt. Sicherlich würde ein Compiler das Problem mit ziemlicher Sicherheit erkennen, wenn dieselbe Variable i wird an beiden Stellen verwendet, aber wenn eine Routine Verweise auf zwei Zeiger akzeptiert p y q und verwendet (*p) y (*q) in dem obigen Ausdruck (statt der Verwendung von i zweimal), wäre der Compiler nicht verpflichtet, die Blockierung zu erkennen oder zu vermeiden, die auftreten würde, wenn die Adresse desselben Objekts in beiden Fällen übergeben würde p y q .

19voto

Während die Syntax der Ausdrücke wie a = a++ ou a++ + a++ legal ist, ist die Verhalten dieser Konstrukte ist undefiniert weil ein soll in der C-Norm nicht befolgt wird. C99 6.5p2 :

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

Mit Fußnote 73 weitere Klarstellung, dass

  1. Dieser Absatz macht undefinierte Anweisungsausdrücke wie

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

    und ermöglicht

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

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

  1. Es folgen die in 5.1.2.3 beschriebenen Sequenzpunkte:

    • Zwischen den Bewertungen des Funktionsbezeichners und der eigentlichen Argumente in einem Funktionsaufruf und dem eigentlichen Aufruf. (6.5.2.2).
    • Zwischen den Bewertungen 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 des bedingten ? : Operators und der Auswertung des zweiten und dritten Operanden (6.5.15).
    • Das Ende eines vollständigen Deklarators: Deklaratoren (6.7.6);
    • Zwischen der Auswertung eines vollständigen Ausdrucks und dem nächsten vollständigen Ausdruck, der ausgewertet werden soll. Vollständige Ausdrücke sind: ein Initialisierer, der nicht Teil eines zusammengesetzten Literals ist (6.7.9); der Ausdruck in einer expression-Anweisung (6.8.3); der steuernde Ausdruck einer selection-Anweisung (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 mit jedem Konvertierungsspezifizierer für formatierte Eingabe-/Ausgabefunktionen verbunden 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 als Argumente an diesen Aufruf übergebenen Objekte (7.22.5).

Der Wortlaut desselben Absatz in C11 ist:

  1. Wenn ein Seiteneffekt auf ein skalares Objekt in Bezug auf einen anderen Seiteneffekt auf dasselbe skalare Objekt oder eine Wertberechnung, die den Wert desselben skalaren Objekts verwendet, nicht in der richtigen Reihenfolge ist, ist das Verhalten undefiniert. Wenn es mehrere zulässige Reihenfolgen der Unterausdrücke eines Ausdrucks gibt, ist das Verhalten undefiniert, wenn ein solcher unsequenzierter Seiteneffekt in einer der Reihenfolgen auftritt.84)

Sie können solche Fehler in einem Programm erkennen, indem Sie zum Beispiel eine aktuelle Version von GCC mit -Wall y -Werror und dann weigert sich GCC schlichtweg, Ihr Programm zu kompilieren. 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

Es ist wichtig zu wissen was ein Sequenzpunkt ist - und Was ist einen Sequenzpunkt und was ist nicht . Zum Beispiel die Komma-Operator ist ein Sequenzpunkt, also

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

ist wohldefiniert und wird inkrementiert i um eins, was den alten Wert ergibt, diesen Wert verwerfen; dann beim Komma-Operator die Nebeneffekte regeln; und dann inkrementieren i um eins, und der sich daraus ergebende Wert wird zum Wert des Ausdrucks - d.h. dies ist nur eine erfundene Schreibweise für j = (i += 2) was wieder einmal eine "clevere" Art ist, zu schreiben

i += 2;
j = i;

Allerdings ist die , in Funktionsargumentlisten ist no ein Komma-Operator, und es gibt keinen Sequenzpunkt zwischen den Auswertungen verschiedener Argumente; stattdessen sind ihre Auswertungen in Bezug zueinander ungeordnet; daher ist der Funktionsaufruf

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

hat undefiniertes Verhalten denn gibt es keinen Sequenzpunkt zwischen den Bewertungen von i++ y ++i in Funktionsargumenten und der Wert von i wird daher zweimal geändert, sowohl durch i++ y ++i zwischen dem vorherigen und dem nächsten Sequenzpunkt.

15voto

Nikhil Vidhani Punkte 689

Der C-Standard besagt, dass eine Variable zwischen zwei Sequenzpunkten nur maximal einmal zugewiesen werden darf. Ein Semikolon ist zum Beispiel ein Sequenzpunkt.
Also jede Aussage der Form:

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

und so weiter verstoßen gegen diese Regel. Der Standard sagt auch, dass das Verhalten undefiniert und nicht unspezifiziert ist. Einige Compiler erkennen dies und erzeugen ein bestimmtes Ergebnis, aber das ist nicht der Standard.

Zwischen zwei Sequenzpunkten können jedoch zwei verschiedene Variablen inkrementiert werden.

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

Dies ist eine gängige Kodierungspraxis beim Kopieren/Analysieren von Zeichenketten.

0 Stimmen

Natürlich gilt dies nicht für verschiedene Variablen innerhalb eines Ausdrucks. Es wäre ein totaler Konstruktionsfehler, wenn es so wäre! Alles, was Sie im zweiten Beispiel brauchen, ist, dass beide zwischen dem Ende der Anweisung und dem Beginn der nächsten inkrementiert werden, und das ist garantiert, gerade wegen des Konzepts der Sequenzpunkte, das im Zentrum all dessen steht.

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