C ++ 98 und C ++ 03
Diese Antwort gilt für ältere Versionen des C ++ - Standards. Die Versionen C ++ 11 und C ++ 14 des Standards enthalten formal keine 'Sequenzpunkte'. Operationen werden stattdessen "vorher sequenziert" oder "nicht sequenziert" oder "unbestimmt sequenziert". Der Nettoeffekt ist im Wesentlichen der gleiche, aber die Terminologie ist unterschiedlich.
Haftungsausschluss : Okay. Diese Antwort ist etwas lang. Also haben Sie Geduld beim Lesen. Wenn Sie diese Dinge bereits kennen, macht Sie das erneute Lesen nicht verrückt.
Voraussetzungen : Grundkenntnisse in C ++ Standard
Was sind Sequenzpunkte?
Der Standard sagt
An bestimmten festgelegten Punkten in der Ausführungssequenz, die als Sequenzpunkte bezeichnet werden , müssen alle Nebenwirkungen früherer Bewertungen vollständig sein und es dürfen keine Nebenwirkungen nachfolgender Bewertungen aufgetreten sein. (§1.9 / 7)
Nebenwirkungen? Was sind Nebenwirkungen?
Die Auswertung eines Ausdrucks erzeugt etwas, und wenn sich zusätzlich der Zustand der Ausführungsumgebung ändert, wird gesagt, dass der Ausdruck (seine Auswertung) einige Nebenwirkungen hat.
Zum Beispiel:
int x = y++; //where y is also an int
Zusätzlich zur Initialisierungsoperation wird der Wert von y
aufgrund der Nebenwirkung des ++
Bedieners geändert .
So weit, ist es gut. Weiter zu Sequenzpunkten. Eine vom Autor von comp.lang.c angegebene alternative Definition von Seq-Punkten Steve Summit
:
Der Sequenzpunkt ist ein Zeitpunkt, zu dem sich der Staub abgesetzt hat und alle bisher beobachteten Nebenwirkungen garantiert vollständig sind.
Was sind die im C ++ Standard aufgeführten allgemeinen Sequenzpunkte?
Jene sind:
am Ende der Auswertung des vollständigen Ausdrucks ( §1.9/16
) (Ein vollständiger Ausdruck ist ein Ausdruck, der kein Unterausdruck eines anderen Ausdrucks ist.) 1
Beispiel:
int a = 5; // ; is a sequence point here
bei der Auswertung jedes der folgenden Ausdrücke nach der Auswertung des ersten Ausdrucks ( §1.9/18
) 2
a && b (§5.14)
a || b (§5.15)
a ? b : c (§5.16)
a , b (§5.18)
(hier ist a, b ein Kommaoperator; in func(a,a++)
,
ist kein Kommaoperator, es ist lediglich ein Trennzeichen zwischen den Argumenten a
und a++
. Daher ist das Verhalten in diesem Fall undefiniert (wenn a
es als primitiver Typ betrachtet wird))
bei einem Funktionsaufruf (unabhängig davon, ob die Funktion inline ist oder nicht) nach der Auswertung aller Funktionsargumente (falls vorhanden), die vor der Ausführung von Ausdrücken oder Anweisungen im Funktionskörper ( §1.9/17
) erfolgt.
1: Hinweis: Die Auswertung eines vollständigen Ausdrucks kann die Auswertung von Unterausdrücken umfassen, die nicht lexikalisch Teil des vollständigen Ausdrucks sind. Beispielsweise wird davon ausgegangen, dass Unterausdrücke, die an der Auswertung von Standardargumentausdrücken (8.3.6) beteiligt sind, in dem Ausdruck erstellt werden, der die Funktion aufruft, und nicht in dem Ausdruck, der das Standardargument definiert
2: Die angegebenen Operatoren sind die integrierten Operatoren, wie in Abschnitt 5 beschrieben. Wenn einer dieser Operatoren in einem gültigen Kontext überladen ist (Abschnitt 13) und somit eine benutzerdefinierte Operatorfunktion bezeichnet, bezeichnet der Ausdruck einen Funktionsaufruf und Die Operanden bilden eine Argumentliste ohne einen impliziten Sequenzpunkt zwischen ihnen.
Was ist undefiniertes Verhalten?
Der Standard definiert undefiniertes Verhalten in Abschnitt §1.3.12
als
Verhalten, wie es bei Verwendung eines fehlerhaften Programmkonstrukts oder fehlerhafter Daten auftreten kann, für die diese Internationale Norm keine Anforderungen stellt 3 .
Undefiniertes Verhalten kann auch erwartet werden, wenn in dieser Internationalen Norm die Beschreibung einer expliziten Definition des Verhaltens weggelassen wird.
3: Das zulässige undefinierte Verhalten reicht vom vollständigen 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 zum Beenden einer Übersetzung oder Ausführung (mit der Ausgabe einer Diagnosemeldung).
Kurz gesagt, undefiniertes Verhalten bedeutet, dass alles passieren kann, von Dämonen, die aus Ihrer Nase fliegen, bis zu Ihrer schwangeren Freundin.
Welche Beziehung besteht zwischen undefiniertem Verhalten und Sequenzpunkten?
Bevor ich darauf eingehe, müssen Sie den Unterschied zwischen undefiniertem Verhalten, nicht spezifiziertem Verhalten und implementierungsdefiniertem Verhalten kennen .
Das müssen Sie auch wissen the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified
.
Zum Beispiel:
int x = 5, y = 6;
int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.
Ein weiteres Beispiel hier .
Jetzt §5/4
sagt der Standard in
- 1) Zwischen dem vorherigen und dem nächsten Sequenzpunkt muss der gespeicherte Wert eines Skalarobjekts höchstens einmal durch Auswertung eines Ausdrucks geändert werden.
Was bedeutet das?
Informell bedeutet dies, dass zwischen zwei Sequenzpunkten eine Variable nicht mehr als einmal geändert werden darf. In einer Ausdrucksanweisung steht das next sequence point
normalerweise am abschließenden Semikolon und das previous sequence point
am Ende der vorherigen Anweisung. Ein Ausdruck kann auch ein Zwischenprodukt enthalten sequence points
.
Aus dem obigen Satz rufen die folgenden Ausdrücke undefiniertes Verhalten hervor:
i++ * ++i; // UB, i is modified more than once btw two SPs
i = ++i; // UB, same as above
++i = 2; // UB, same as above
i = ++i + 1; // UB, same as above
++++++i; // UB, parsed as (++(++(++i)))
i = (i, ++i, ++i); // UB, there's no SP between `++i` (right most) and assignment to `i` (`i` is modified more than once btw two SPs)
Aber die folgenden Ausdrücke sind in Ordnung:
i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i); // well defined
int j = i;
j = (++i, i++, j*i); // well defined
- 2) Auf den vorherigen Wert darf nur zugegriffen werden, um den zu speichernden Wert zu bestimmen.
Was bedeutet das? Wenn ein Objekt innerhalb eines vollständigen Ausdrucks beschrieben wird, müssen alle Zugriffe innerhalb desselben Ausdrucks direkt an der Berechnung des zu schreibenden Werts beteiligt sein .
Zum Beispiel sind bei i = i + 1
allen Zugriffen von i
(in LHS und in RHS) direkt an der Berechnung des zu schreibenden Wertes beteiligt. Also ist es gut.
Diese Regel beschränkt rechtliche Ausdrücke effektiv auf diejenigen, bei denen die Zugriffe nachweislich der Änderung vorausgehen.
Beispiel 1:
std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2
Beispiel 2:
a[i] = i++ // or a[++i] = i or a[i++] = ++i etc
wird nicht zugelassen, weil einer der Zugriffe von i
(der in a[i]
) nichts mit dem Wert zu tun hat, der in i gespeichert wird (was in in geschieht i++
), und daher gibt es keine gute Möglichkeit zu definieren - weder für unser Verständnis noch für das Compiler - Gibt an, ob der Zugriff vor oder nach dem Speichern des inkrementierten Werts erfolgen soll. Das Verhalten ist also undefiniert.
Beispiel 3:
int x = i + i++ ;// Similar to above
Folgen Sie der Antwort für C ++ 11 hier .
*p++ = 4
ist nicht undefiniertes Verhalten.*p++
wird interpretiert als*(p++)
.p++
gibtp
(eine Kopie) zurück und der Wert wird an der vorherigen Adresse gespeichert. Warum sollte das UB aufrufen? Es ist vollkommen in Ordnung.