Was sind all die gängigen undefinierten Verhaltensweisen, über die ein C ++ - Programmierer Bescheid wissen sollte? [geschlossen]

201

Was sind all die gängigen undefinierten Verhaltensweisen, über die ein C ++ - Programmierer Bescheid wissen sollte?

Sagen Sie, wie:

a[i] = i++;

yesraaj
quelle
3
Bist du sicher. Das sieht gut definiert aus.
Martin York
17
6.2.2 Evaluierungsreihenfolge [expr.evaluation] in der Programmiersprache C ++ sagen dies. Ich habe keine andere Referenz
yesraaj
4
Er hat recht.
Ich habe mir
4
Ich würde mir vorstellen, dass der Comiler das i ++ vor oder nach der Berechnung des Speicherorts von v [i] ausführen lässt. Klar, ich werde immer dort zugewiesen. aber es könnte entweder nach v [i] oder v [i + 1] schreiben, abhängig von der Reihenfolge der Operationen.
Evan Teran
2
In der Programmiersprache C ++ heißt es lediglich: "Die Reihenfolge der Operationen von Unterausdrücken innerhalb eines Ausdrucks ist undefiniert. Insbesondere können Sie nicht davon ausgehen, dass der Ausdruck von links nach rechts ausgewertet wird."
Dancavallaro

Antworten:

233

Zeiger

  • Dereferenzieren eines NULLZeigers
  • Dereferenzieren eines Zeigers, der durch eine "neue" Zuordnung der Größe Null zurückgegeben wird
  • Verwenden von Zeigern auf Objekte, deren Lebensdauer abgelaufen ist (z. B. zugewiesene Objekte stapeln oder gelöschte Objekte)
  • Dereferenzieren eines Zeigers, der noch nicht definitiv initialisiert wurde
  • Durchführen einer Zeigerarithmetik, die ein Ergebnis außerhalb der Grenzen (entweder über oder unter) eines Arrays ergibt.
  • Dereferenzieren des Zeigers an einer Stelle jenseits des Endes eines Arrays.
  • Konvertieren von Zeigern in Objekte inkompatibler Typen
  • Verwenden memcpyzum Kopieren überlappender Puffer .

Puffer läuft über

  • Lesen oder Schreiben in ein Objekt oder Array mit einem negativen Versatz oder über die Größe dieses Objekts hinaus (Stapel- / Heap-Überlauf)

Ganzzahlige Überläufe

  • Ganzzahliger Überlauf mit Vorzeichen
  • Auswertung eines Ausdrucks, der nicht mathematisch definiert ist
  • Linksverschobene Werte um einen negativen Betrag (Rechtsverschiebungen um negative Beträge sind implementierungsdefiniert)
  • Verschieben von Werten um einen Betrag, der größer oder gleich der Anzahl der Bits in der Anzahl ist (z. B. int64_t i = 1; i <<= 72ist undefiniert)

Typen, Besetzung und Konst

  • Umwandlung eines numerischen Werts in einen Wert, der vom Zieltyp nicht dargestellt werden kann (entweder direkt oder über static_cast)
  • Eine automatische Variable , bevor es auf jeden Fall zugewiesen (zB wurde int i; i++; cout << i;)
  • Verwenden des Werts eines anderen Objekts als volatileodersig_atomic_t beim Empfang eines Signals
  • Versuch, ein Zeichenfolgenliteral oder ein anderes const-Objekt während seiner Lebensdauer zu ändern
  • Verketten eines schmalen mit einem breiten String-Literal während der Vorverarbeitung

Funktion und Vorlage

  • Kein Wert von einer Wertrückgabefunktion zurückgeben (direkt oder durch Abfließen von einem Try-Block)
  • Mehrere unterschiedliche Definitionen für dieselbe Entität (Klasse, Vorlage, Aufzählung, Inline-Funktion, statische Elementfunktion usw.)
  • Unendliche Rekursion bei der Instanziierung von Vorlagen
  • Aufrufen einer Funktion unter Verwendung verschiedener Parameter oder Verknüpfung mit den Parametern und Verknüpfungen, für die die Funktion definiert ist.

OOP

  • Kaskadierende Zerstörungen von Objekten mit statischer Speicherdauer
  • Das Ergebnis der Zuweisung zu teilweise überlappenden Objekten
  • Rekursives erneutes Eingeben einer Funktion während der Initialisierung ihrer statischen Objekte
  • Durch virtuelle Funktionsaufrufe werden reine virtuelle Funktionen eines Objekts von seinem Konstruktor oder Destruktor aufgerufen
  • Bezugnehmend auf nicht statische Elemente von Objekten, die nicht konstruiert oder bereits zerstört wurden

Quelldatei und Vorverarbeitung

  • Eine nicht leere Quelldatei, die nicht mit einem Zeilenumbruch oder einem Backslash endet (vor C ++ 11).
  • Ein Backslash gefolgt von einem Zeichen, das nicht Teil der angegebenen Escape-Codes in einer Zeichen- oder Zeichenfolgenkonstante ist (dies ist in C ++ 11 implementierungsdefiniert).
  • Überschreitung der Implementierungsgrenzen (Anzahl der verschachtelten Blöcke, Anzahl der Funktionen in einem Programm, verfügbarer Stapelspeicher ...)
  • Numerische Präprozessorwerte, die nicht durch a dargestellt werden können long int
  • Vorverarbeitungsanweisung auf der linken Seite einer funktionsähnlichen Makrodefinition
  • Dynamisches Generieren des definierten Tokens in einem #ifAusdruck

Zu klassifizieren

  • Aufruf von exit während der Zerstörung eines Programms mit statischer Speicherdauer
Diomidis Spinellis
quelle
Hm ... NaN (x / 0) und Infinity (0/0) wurden von der IEE 754 abgedeckt. Wenn C ++ später entworfen wurde, warum wird x / 0 als undefiniert aufgezeichnet?
new123456
Betreff: "Ein Backslash gefolgt von einem Zeichen, das nicht Teil der angegebenen Escape-Codes in einer Zeichen- oder Zeichenfolgenkonstante ist." Das ist UB in C89 (§3.1.3.4) und C ++ 03 (das C89 enthält), aber nicht in C99. C99 sagt, dass "das Ergebnis kein Token ist und eine Diagnose erforderlich ist" (§6.4.4.4). Vermutlich wird C ++ 0x (das C89 enthält) dasselbe sein.
Adam Rosenfield
1
Der C99-Standard enthält eine Liste undefinierter Verhaltensweisen in Anhang J.2. Es würde einige Arbeit erfordern, diese Liste an C ++ anzupassen. Sie müssten die Verweise auf die richtigen C ++ - Klauseln anstatt auf die C99-Klauseln ändern, irrelevante Elemente entfernen und auch prüfen, ob all diese Dinge in C ++ und C wirklich undefiniert sind. Aber es bietet einen Anfang.
Steve Jessop
1
@ new123456 - Nicht alle Gleitkommaeinheiten sind IEE754-kompatibel. Wenn C ++ die IEE754-Konformität erfordert, müssen Compiler den Fall, in dem die RHS Null ist, durch eine explizite Prüfung testen und behandeln. Indem das Verhalten undefiniert wird, kann der Compiler diesen Overhead vermeiden, indem er sagt: "Wenn Sie eine Nicht-IEE754-FPU verwenden, erhalten Sie kein IEEE754-FPU-Verhalten."
SecurityMatt
1
"Auswerten eines Ausdrucks, dessen Ergebnis nicht im Bereich der entsprechenden Typen liegt" .... Der Integer-Überlauf ist für nicht signierte Integraltypen gut definiert, nur für nicht signierte.
Nacitar Sevaht
31

Die 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.

Martin York
quelle
23
Die Reihenfolge ist nicht spezifiziert, nicht undefiniert.
Rob Kennedy
1
Ich hasse diesen :) Ich habe einen Arbeitstag verloren, als ich einen dieser Fälle aufgespürt habe ... trotzdem habe ich meine Lektion gelernt und bin zum Glück nicht wieder gefallen
Robert Gould
2
@Rob: Ich würde mit Ihnen über die Änderung der Bedeutung hier streiten, aber ich weiß, dass das Normungskomitee bei der genauen Definition dieser beiden Wörter sehr wählerisch ist. Also werde ich es einfach ändern :-)
Martin York
2
Ich hatte Glück in diesem Fall. Als ich auf dem College war, wurde ich davon gebissen und hatte einen Professor, der es sich einmal ansah und mir in etwa 5 Sekunden mein Problem erzählte. Keine Ahnung, wie viel Zeit ich sonst mit dem Debuggen verschwendet hätte.
Bill the Lizard
27

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.
Martin York
quelle
Was ist mit Sequenzpunkt gemeint?
Yesraaj
1
Ooh ... das ist böse, zumal ich genau die in Java empfohlene Struktur gesehen habe
Tom
Beachten Sie, dass einige Compiler das Verhalten in dieser Situation definieren. In VC ++ 2005+ werden beispielsweise, wenn a flüchtig ist, die erforderlichen Speicherbarier eingerichtet, um eine Neuordnung der Anweisungen zu verhindern, sodass die doppelt überprüfte Sperrung funktioniert.
Eclipse
Martin York: <i> // (c) wird garantiert nach (a) und (b) </ i> geschehen. Zugegeben, in diesem speziellen Beispiel wäre das einzige Szenario, in dem es von Bedeutung sein könnte, wenn 'i' eine flüchtige Variable wäre, die einem Hardwareregister zugeordnet ist, und ein [i] (alter Wert von 'i') mit einem Alias ​​versehen wäre, aber gibt es eines garantieren, dass das Inkrement vor einem Sequenzpunkt erfolgt?
Supercat
5

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.

Daniel Earwicker
quelle
Ich habe das schon einmal gemacht, aber ich sehe nicht, wie undefiniert es ist. Es ist ziemlich offensichtlich, dass Sie im Nachhinein eine unendliche Rekursion machen.
Robert Gould
Das Problem ist, dass der Compiler Ihren Code nicht untersuchen und genau entscheiden kann, ob er unter einer unendlichen Rekursion leidet oder nicht. Es ist ein Beispiel für das Problem des Anhaltens. Siehe: stackoverflow.com/questions/235984/…
Daniel Earwicker
Ja, es ist definitiv ein Problem
Robert Gould
Mein System stürzte ab, weil der Austausch durch zu wenig Speicher verursacht wurde.
Johannes Schaub - Litb
2
Präprozessorkonstanten, die nicht in ein int passen, sind auch Kompilierungszeiten.
Joshua
5

Zuweisen einer Konstante nach dem Abisolieren constmit const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined
yesraaj
quelle
5

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

Constantin
quelle
4

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.
Martin York
quelle
Infact mindestens einmal zwischen zwei Sequenzpunkten.
Prasoon Saurav
2
@Prasoon: Ich denke du meintest: höchstens einmal zwischen zwei Sequenzpunkten. :-)
Nawaz
3

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;

  • 127 Parameter in einer Funktionsdefinition
  • 127 Argumente in einem Funktionsaufruf
  • 127 Parameter in einer Makrodefinition
  • 127 Argumente in einem Makroaufruf
  • 4095 Zeichen in einer logischen Quellzeile
  • 4095 Zeichen in einem Zeichenfolgenliteral oder einem breiten Zeichenfolgenliteral (nach Verkettung)
  • 65535 Bytes in einem Objekt (nur in einer gehosteten Umgebung)
  • 15Nesting Levels für # eingeschlossene Dateien
  • 1023 Fallbezeichnungen für eine switch-Anweisung (ausgenommen solche für verschachtelte switch-Anweisungen)

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.

RandomNickName42
quelle
9
Wenn Sie diese Grenzen erreichen, haben Sie mehr Probleme als undefiniertes Verhalten.
new123456
Sie könnten EINFACH 65535 Bytes in einem Objekt überschreiten, z. B. einem STD :: vector
Demi
2

Verwenden memcpyzum 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.

7.21.2.1 Die memcpy-Funktion

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.

7.21.2.2 Die memmove-Funktion

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.

John Dibling
quelle
2

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.

JaredPar
quelle
Ist das nicht was für <cstdint> ist? Es definiert Typen wie uint16_6 usw.
Jasper Bekkers
Ja, aber die Größe der meisten Typen, sagen wir lang, ist nicht genau definiert.
JaredPar
Außerdem ist cstdint noch nicht Teil des aktuellen c ++ - Standards. Eine aktuelle tragbare Lösung finden Sie unter boost / stdint.hpp.
Evan Teran
Das ist kein undefiniertes Verhalten. Der Standard besagt, dass eine konforme Plattform die Größen definiert und nicht der Standard, der sie definiert.
Daniel Earwicker
1
@JaredPar: Es ist ein komplexer Beitrag mit vielen Gesprächsthemen, also habe ich alles hier zusammengefasst . Die Quintessenz lautet: "5. Um -2147483647 und +2147483647 binär darzustellen, benötigen Sie 32 Bit."
John Dibling
2

Objekte auf Namespace-Ebene in verschiedenen Kompilierungseinheiten sollten für die Initialisierung niemals voneinander abhängig sein, da ihre Initialisierungsreihenfolge undefiniert ist.

yesraaj
quelle