Was sind all die gängigen undefinierten Verhaltensweisen, über die ein C ++ - Programmierer Bescheid wissen sollte?
Sagen Sie, wie:
a[i] = i++;
Was sind all die gängigen undefinierten Verhaltensweisen, über die ein C ++ - Programmierer Bescheid wissen sollte?
Sagen Sie, wie:
a[i] = i++;
Antworten:
NULL
Zeigersmemcpy
zum Kopieren überlappender Puffer .int64_t i = 1; i <<= 72
ist undefiniert)int i; i++; cout << i;
)volatile
odersig_atomic_t
beim Empfang eines Signalslong int
#if
AusdruckDie Reihenfolge, in der Funktionsparameter ausgewertet werden, ist ein nicht angegebenes Verhalten . (Dadurch wird Ihr Programm nicht abstürzen, explodieren oder Pizza bestellen ... im Gegensatz zu undefiniertem Verhalten .)
Die einzige Voraussetzung ist, dass alle Parameter vollständig ausgewertet werden müssen, bevor die Funktion aufgerufen wird.
Dies:
// The simple obvious one.
callFunc(getA(),getB());
Kann gleichbedeutend sein mit:
int a = getA();
int b = getB();
callFunc(a,b);
Oder dieses:
int b = getB();
int a = getA();
callFunc(a,b);
Es kann entweder sein; Es liegt am Compiler. Das Ergebnis kann abhängig von den Nebenwirkungen von Bedeutung sein.
Dem Compiler steht es frei, die Bewertungsteile eines Ausdrucks neu zu ordnen (vorausgesetzt, die Bedeutung bleibt unverändert).
Aus der ursprünglichen Frage:
a[i] = i++;
// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)
// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:
int rhs = i++;
int lhs& = a[i];
lhs = rhs;
// or
int lhs& = a[i];
int rhs = i++;
lhs = rhs;
Double Checked Locking. Und ein leichter Fehler zu machen.
A* a = new A("plop");
// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'
// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.
// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
Lock lock(mutex);
if (a == null)
{
a = new A("Plop"); // (Point A).
}
}
a->doStuff();
// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
// Remember (c) has been done thus 'a' is not NULL.
// But the memory has not been initialized.
// Thread 2 now executes doStuff() on an uninitialized variable.
// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
Lock lock(mutex);
if (a == null)
{
A* tmp = new A("Plop"); // (Point A).
a = tmp;
}
}
a->doStuff();
// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.
Mein Favorit ist "Unendliche Rekursion bei der Instanziierung von Vorlagen", da ich glaube, dass dies die einzige ist, bei der das undefinierte Verhalten zur Kompilierungszeit auftritt.
Neben undefiniertem Verhalten gibt es auch das ebenso unangenehme implementierungsdefinierte Verhalten .
Undefiniertes Verhalten tritt auf, wenn ein Programm etwas tut, dessen Ergebnis nicht vom Standard angegeben wird.
Implementierungsdefiniertes Verhalten ist eine Aktion eines Programms, deren Ergebnis nicht durch den Standard definiert ist, die die Implementierung jedoch dokumentieren muss. Ein Beispiel ist "Multibyte-Zeichenliterale" aus der Frage "Stapelüberlauf". Gibt es einen C-Compiler, der dies nicht kompilieren kann? .
Implementierungsdefiniertes Verhalten beißt Sie nur, wenn Sie mit der Portierung beginnen (aber ein Upgrade auf eine neue Version des Compilers ist auch eine Portierung!)
Variablen dürfen in einem Ausdruck nur einmal aktualisiert werden (technisch einmal zwischen Sequenzpunkten).
int i =1;
i = ++i;
// Undefined. Assignment to 'i' twice in the same expression.
Ein grundlegendes Verständnis der verschiedenen Umweltgrenzen. Die vollständige Liste finden Sie in Abschnitt 5.2.4.1 der C-Spezifikation. Hier sind ein paar;
Ich war tatsächlich ein bisschen überrascht über das Limit von 1023 Fallbezeichnungen für eine switch-Anweisung. Ich kann davon ausgehen, dass diese für generierten Code / Lex / Parser ziemlich einfach überschritten werden.
Wenn diese Grenzwerte überschritten werden, haben Sie ein undefiniertes Verhalten (Abstürze, Sicherheitslücken usw.).
Richtig, ich weiß, dass dies aus der C-Spezifikation stammt, aber C ++ teilt diese grundlegenden Unterstützungen.
Verwenden memcpy
zum Kopieren zwischen überlappenden Speicherbereichen. Beispielsweise:
char a[256] = {};
memcpy(a, a, sizeof(a));
Das Verhalten ist gemäß dem C-Standard, der vom C ++ 03-Standard subsumiert wird, undefiniert.
Zusammenfassung
1 / #include void * memcpy (void * s1 einschränken, const void * s2 einschränken, size_t n);
Beschreibung
2 / Die Funktion memcpy kopiert n Zeichen von dem Objekt, auf das s2 zeigt, in das Objekt, auf das s1 zeigt. Wenn zwischen überlappenden Objekten kopiert wird, ist das Verhalten undefiniert. Rückgabe 3 Die memcpy-Funktion gibt den Wert von s1 zurück.
Zusammenfassung
1 #include void * memmove (void * s1, const void * s2, size_t n);
Beschreibung
2 Die Funktion memmove kopiert n Zeichen von dem Objekt, auf das s2 zeigt, in das Objekt, auf das s1 zeigt. Das Kopieren erfolgt so, als würden die n Zeichen des Objekts, auf das s2 zeigt, zuerst in ein temporäres Array von n Zeichen kopiert, das die Objekte, auf die s1 und s2 zeigen, nicht überlappt. Anschließend werden die n Zeichen des temporären Arrays kopiert das Objekt, auf das s1 zeigt. Kehrt zurück
3 Die memmove-Funktion gibt den Wert von s1 zurück.
Der einzige Typ, für den C ++ eine Größe garantiert, ist char
. Und die Größe ist 1. Die Größe aller anderen Typen ist plattformabhängig.