Was ist undefiniertes Verhalten in C und C ++? Was ist mit nicht spezifiziertem Verhalten und implementierungsdefiniertem Verhalten? Was ist der Unterschied zwischen ihnen?
530
Was ist undefiniertes Verhalten in C und C ++? Was ist mit nicht spezifiziertem Verhalten und implementierungsdefiniertem Verhalten? Was ist der Unterschied zwischen ihnen?
Antworten:
Undefiniertes Verhalten ist einer der Aspekte der C- und C ++ - Sprache, die für Programmierer aus anderen Sprachen überraschend sein können (andere Sprachen versuchen, es besser zu verbergen). Grundsätzlich ist es möglich, C ++ - Programme zu schreiben, die sich nicht vorhersehbar verhalten, obwohl viele C ++ - Compiler keine Fehler im Programm melden!
Schauen wir uns ein klassisches Beispiel an:
Die Variable
p
zeigt auf das Zeichenfolgenliteral"hello!\n"
, und die beiden folgenden Zuweisungen versuchen, dieses Zeichenfolgenliteral zu ändern. Was macht dieses Programm? Gemäß Abschnitt 2.14.5 Absatz 11 des C ++ - Standards wird undefiniertes Verhalten aufgerufen :Ich kann Leute schreien hören "Aber warte, ich kann das problemlos kompilieren und die Ausgabe erhalten
yellow
" oder "Was meinst du mit undefinierten String-Literalen, die im Nur-Lese-Speicher gespeichert sind, sodass der erste Zuweisungsversuch zu einem Core-Dump führt". Dies ist genau das Problem mit undefiniertem Verhalten. Grundsätzlich lässt der Standard zu, dass alles passiert, wenn Sie undefiniertes Verhalten aufrufen (sogar Nasendämonen). Wenn es ein "korrektes" Verhalten gemäß Ihrem mentalen Modell der Sprache gibt, ist dieses Modell einfach falsch; Der C ++ - Standard hat die einzige Abstimmung, Punkt.Andere Beispiele für undefiniertes Verhalten sind der Zugriff auf ein Array außerhalb seiner Grenzen, die Dereferenzierung des Nullzeigers , der Zugriff auf Objekte nach Ablauf ihrer Lebensdauer oder das Schreiben von angeblich cleveren Ausdrücken wie
i++ + ++i
.In Abschnitt 1.9 des C ++ - Standards werden auch die beiden weniger gefährlichen Brüder des undefinierten Verhaltens erwähnt, das nicht spezifizierte Verhalten und das implementierungsdefinierte Verhalten :
Im Einzelnen heißt es in Abschnitt 1.3.24:
Was können Sie tun, um undefiniertes Verhalten zu vermeiden? Grundsätzlich muss man gute C ++ - Bücher von Autoren lesen, die wissen, wovon sie sprechen. Schrauben Sie Internet-Tutorials. Bullschildt schrauben.
quelle
int f(){int a; return a;}
: Der Wert vona
kann sich zwischen Funktionsaufrufen ändern.Nun, dies ist im Grunde ein direktes Kopieren und Einfügen aus dem Standard
quelle
int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }
ein Compiler feststellen kann, dass alle Mittel zum Aufrufen der Funktion, mit der die Raketen nicht gestartet werden, undefiniertes Verhalten aufrufen, kann er den Aufruflaunch_missiles()
bedingungslos ausführen.Vielleicht könnte eine einfache Formulierung für das Verständnis einfacher sein als die strenge Definition der Standards.
Implementierungsdefiniertes Verhalten
Die Sprache sagt, dass wir Datentypen haben. Die Compiler-Anbieter geben an, welche Größen sie verwenden sollen, und stellen eine Dokumentation ihrer Aktivitäten bereit.
undefiniertes Verhalten
Sie machen etwas falsch. Zum Beispiel haben Sie einen sehr großen Wert in einem
int
, der nicht passtchar
. Wie setzen Sie diesen Wert einchar
? eigentlich gibt es keinen weg! Alles könnte passieren, aber das Vernünftigste wäre, das erste Byte dieses int zu nehmen und es einzufügenchar
. Es ist einfach falsch, dies zu tun, um das erste Byte zuzuweisen, aber genau das passiert unter der Haube.nicht näher bezeichnetes Verhalten
Welche Funktion dieser beiden wird zuerst ausgeführt?
Die Sprache gibt die Bewertung nicht an, von links nach rechts oder von rechts nach links! Ein nicht spezifiziertes Verhalten kann also zu einem undefinierten Verhalten führen oder auch nicht, aber Ihr Programm sollte auf keinen Fall ein nicht spezifiziertes Verhalten erzeugen.
@eSKay Ich denke, deine Frage ist es wert, die Antwort zu bearbeiten, um mehr zu klären :)
Der Unterschied zwischen implementierungsdefiniert und nicht spezifiziert besteht darin, dass der Compiler im ersten Fall ein Verhalten auswählen soll, im zweiten Fall jedoch nicht. Beispielsweise muss eine Implementierung nur eine Definition von haben
sizeof(int)
. Es kann also nicht gesagt werden, dass diessizeof(int)
4 für einen Teil des Programms und 8 für andere ist. Im Gegensatz zu nicht angegebenem Verhalten, bei dem der Compiler OK sagen kann, werde ich diese Argumente von links nach rechts und die Argumente der nächsten Funktion von rechts nach links auswerten. Es kann im selben Programm passieren, deshalb wird es als nicht spezifiziert bezeichnet . Tatsächlich hätte C ++ einfacher gemacht werden können, wenn einige der nicht spezifizierten Verhaltensweisen angegeben worden wären. Schauen Sie sich hier die Antwort von Dr. Stroustrup an :quelle
fun(fun1(), fun2());
ist das nicht das Verhalten"implementation defined"
? Der Compiler muss doch den einen oder anderen Kurs wählen?"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"
verstehe ich, dass diescan
passiert. Funktioniert das wirklich mit Compilern, die wir heutzutage verwenden?Aus dem offiziellen C-Begründungsdokument
quelle
Undefiniertes Verhalten vs. nicht spezifiziertes Verhalten enthält eine kurze Beschreibung.
Ihre letzte Zusammenfassung:
quelle
In der Vergangenheit stellten sowohl implementierungsdefiniertes Verhalten als auch undefiniertes Verhalten Situationen dar, in denen die Autoren des Standards erwarteten, dass Personen, die Qualitätsimplementierungen schreiben, mit Urteilsvermögen entscheiden würden, welche Verhaltensgarantien, falls vorhanden, für Programme in dem beabsichtigten Anwendungsfeld nützlich sind, das auf dem Programm ausgeführt wird beabsichtigte Ziele. Die Anforderungen von High-End-Code für das Knacken von Zahlen unterscheiden sich erheblich von denen von Low-Level-Systemcode, und sowohl UB als auch IDB bieten Compiler-Autoren Flexibilität, um diese unterschiedlichen Anforderungen zu erfüllen. Keine der beiden Kategorien schreibt vor, dass sich Implementierungen so verhalten, dass sie für einen bestimmten Zweck oder sogar für einen beliebigen Zweck nützlich sind. Qualitätsimplementierungen, die behaupten, für einen bestimmten Zweck geeignet zu sein, sollten sich jedoch in einer Weise verhalten, die diesem Zweck angemessen istob der Standard dies erfordert oder nicht .
Der einzige Unterschied zwischen implementierungsdefiniertem Verhalten und nicht definiertem Verhalten besteht darin, dass erstere erfordern, dass Implementierungen ein konsistentes Verhalten definieren und dokumentieren, selbst wenn nichts, was die Implementierung möglicherweise tun könnte, nützlich wäre . Die Trennlinie zwischen ihnen besteht nicht darin, ob es für Implementierungen im Allgemeinen nützlich wäre, Verhaltensweisen zu definieren (Compiler-Autoren sollten nützliche Verhaltensweisen definieren, wenn dies nach dem Standard erforderlich ist oder nicht), sondern ob es Implementierungen geben könnte, bei denen das Definieren eines Verhaltens gleichzeitig kostspielig wäre und nutzlos . Ein Urteil darüber, dass solche Implementierungen existieren könnten, impliziert in keiner Weise, Form oder Form ein Urteil über die Nützlichkeit der Unterstützung eines definierten Verhaltens auf anderen Plattformen.
Leider haben Compiler-Autoren seit Mitte der neunziger Jahre damit begonnen, das Fehlen von Verhaltensmandaten als ein Urteil zu interpretieren, dass Verhaltensgarantien die Kosten selbst in Anwendungsbereichen, in denen sie von entscheidender Bedeutung sind, und sogar auf Systemen, in denen sie praktisch nichts kosten, nicht wert sind. Anstatt UB als Aufforderung zu angemessenem Urteilsvermögen zu behandeln, haben Compiler-Autoren damit begonnen, dies als Entschuldigung dafür zu betrachten, dies nicht zu tun.
Beispiel: Geben Sie den folgenden Code ein:
Eine Zwei-Komplement-Implementierung müsste keinerlei Aufwand erfordern, um den Ausdruck
v << pow
als Zwei-Komplement-Verschiebung zu behandeln , ohne Rücksicht darauf, ob erv
positiv oder negativ ist.Die bevorzugte Philosophie einiger heutiger Compiler-Autoren würde jedoch darauf
v
hinweisen, dass es keinen Grund gibt, das Programm den negativen Bereich von abschneiden zu lassen , da es nur negativ sein kann, wenn sich das Programm auf undefiniertes Verhalten einlässtv
. Obwohl die Linksverschiebung negativer Werte früher von jedem einzelnen Compiler von Bedeutung unterstützt wurde und eine große Menge vorhandenen Codes auf diesem Verhalten beruht, würde die moderne Philosophie die Tatsache interpretieren, dass der Standard sagt, dass linksverschiebende negative Werte UB sind als Dies bedeutet, dass Compiler-Autoren dies ignorieren sollten.quelle
<<
UB auf negativen Zahlen steht, ist eine böse kleine Falle, und ich bin froh, daran erinnert zu werden!i+j>k
1 oder 0 ergibt, wenn die Addition überläuft, sofern er keine anderen Nebenwirkungen hat , kann ein Compiler möglicherweise einige massive Optimierungen vornehmen, die nicht möglich wären, wenn der Programmierer den Code als geschrieben hätte(int)((unsigned)i+j) > k
.C ++ Standard n3337 § 1.3.10 Implementierungsdefiniertes Verhalten
Manchmal schreibt C ++ Standard einigen Konstrukten kein bestimmtes Verhalten vor, sondern sagt stattdessen, dass ein bestimmtes, genau definiertes Verhalten von einer bestimmten Implementierung (Version der Bibliothek) ausgewählt und beschrieben werden muss. So kann der Benutzer immer noch genau wissen, wie sich das Programm verhält, obwohl Standard dies nicht beschreibt.
C ++ Standard n3337 § 1.3.24 undefiniertes Verhalten
Wenn das Programm auf ein Konstrukt stößt, das nicht gemäß C ++ Standard definiert ist, kann es tun, was es will (vielleicht eine E-Mail an mich senden oder eine E-Mail an Sie senden oder den Code vollständig ignorieren).
C ++ Standard n3337 § 1.3.25 nicht spezifiziertes Verhalten
C ++ Standard legt einigen Konstrukten kein bestimmtes Verhalten auf, sondern sagt stattdessen, dass ein bestimmtes, genau definiertes Verhalten durch eine bestimmte Implementierung (Version der Bibliothek) ausgewählt werden muss ( Bot nicht unbedingt beschrieben ). In dem Fall, in dem keine Beschreibung angegeben wurde, kann es für den Benutzer schwierig sein, genau zu wissen, wie sich das Programm verhält.
quelle
Implementierung definiert-
Nicht spezifiziert -
Nicht definiert-
quelle
uint32_t s;
Bewertung,1u<<s
wanns
33 ist, vielleicht 0 oder 2 ergeben würde, aber nichts anderes Verrücktes tun würde. Neuere Compiler, die auswerten,1u<<s
können jedoch dazu führen, dass ein Compiler feststellt, dasss
Code vor oder nach diesem Ausdruck, der nur dann relevant wäre, wenn er zuvor weniger als 32 gewesen sein musss
er 32 oder mehr gewesen wäre, weggelassen werden .