Was ist undefiniertes Verhalten in C und C ++? Was ist mit nicht spezifiziertem Verhalten und implementierungsdefiniertem Verhalten? Was ist der Unterschied zwischen ihnen?
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:
#include <iostream>
int main()
{
char* p = "hello!\n"; // yes I know, deprecated conversion
p[0] = 'y';
p[5] = 'w';
std::cout << p;
}
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 :
Der Versuch, ein Zeichenfolgenliteral zu ändern, ist nicht definiert.
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 :
Die semantischen Beschreibungen in dieser Internationalen Norm definieren eine parametrisierte nichtdeterministische abstrakte Maschine.
Bestimmte Aspekte und Operationen der abstrakten Maschine werden in dieser Internationalen Norm als implementierungsdefiniert beschrieben (z. B.
sizeof(int)
). Diese bilden die Parameter der abstrakten Maschine. Jede Implementierung muss eine Dokumentation enthalten, in der ihre Merkmale und ihr Verhalten in dieser Hinsicht beschrieben werden.Bestimmte andere Aspekte und Operationen der abstrakten Maschine werden in dieser Internationalen Norm als nicht spezifiziert beschrieben (z. B. Reihenfolge der Bewertung von Argumenten für eine Funktion). Nach Möglichkeit definiert diese Internationale Norm eine Reihe zulässiger Verhaltensweisen. Diese definieren die nichtdeterministischen Aspekte der abstrakten Maschine.
Bestimmte andere Operationen werden in dieser Internationalen Norm als undefiniert beschrieben (z. B. der Effekt der Dereferenzierung des Nullzeigers). [ Hinweis : Diese Internationale Norm stellt keine Anforderungen an das Verhalten von Programmen, die undefiniertes Verhalten enthalten. - Endnote ]
Im Einzelnen heißt es in Abschnitt 1.3.24:
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 für die Umgebung charakteristischen dokumentierten Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis zum Beenden einer Übersetzung oder Ausführung (mit der Ausgabe) einer Diagnosemeldung).
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.
int f(){int a; return a;}
: Der Wert von a
kann sich zwischen Funktionsaufrufen ändern.
Nun, dies ist im Grunde ein direktes Kopieren und Einfügen aus dem Standard
3.4.1 1 Implementierungsdefiniertes Verhalten Nicht spezifiziertes Verhalten, bei dem jede Implementierung dokumentiert, wie die Auswahl getroffen wird
2 BEISPIEL Ein Beispiel für ein implementierungsdefiniertes Verhalten ist die Ausbreitung des höherwertigen Bits, wenn eine vorzeichenbehaftete Ganzzahl nach rechts verschoben wird.
3.4.3 1 undefiniertes Verhaltensverhalten bei Verwendung eines nicht portierbaren oder fehlerhaften Programmkonstrukts oder fehlerhafter Daten, für die diese Internationale Norm keine Anforderungen stellt
2 HINWEIS Ein mögliches undefiniertes 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 die Ausgabe einer Diagnosemeldung).
3 BEISPIEL Ein Beispiel für undefiniertes Verhalten ist das Verhalten beim Ganzzahlüberlauf.
3.4.4 1 nicht spezifiziertes Verhalten Verwendung eines nicht spezifizierten Wertes oder eines anderen Verhaltens, bei dem diese Internationale Norm zwei oder mehr Möglichkeiten bietet und keine weiteren Anforderungen stellt, die in irgendeinem Fall gewählt werden
2 BEISPIEL Ein Beispiel für nicht angegebenes Verhalten ist die Reihenfolge, in der die Argumente für eine Funktion ausgewertet werden.
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 Aufruf launch_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 ein char
? 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ügen char
. 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?
void fun(int n, int m);
int fun1()
{
cout << "fun1";
return 1;
}
int fun2()
{
cout << "fun2";
return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?
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 :)
zum
fun(fun1(), fun2());
ist das Verhalten nicht "Implementierung definiert"? Der Compiler muss doch den einen oder anderen Kurs wählen?
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 dies sizeof(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 :
Es wird behauptet, dass der Unterschied zwischen dem, was erzeugt werden kann, um dem Compiler diese Freiheit zu geben, und dem Erfordernis einer "gewöhnlichen Bewertung von links nach rechts" erheblich sein kann. Ich bin nicht überzeugt, aber da unzählige Compiler "da draußen" die Freiheit ausnutzen und einige Leute diese Freiheit leidenschaftlich verteidigen, wäre eine Änderung schwierig und könnte Jahrzehnte dauern, bis sie in die entfernten Ecken der C- und C ++ - Welt vordringt. Ich bin enttäuscht, dass nicht alle Compiler vor Code wie ++ i + i ++ warnen. Ebenso ist die Reihenfolge der Bewertung von Argumenten nicht spezifiziert.
IMO bleiben viel zu viele "Dinge" undefiniert, nicht spezifiziert, implementierungsdefiniert usw. Dies ist jedoch leicht zu sagen und sogar Beispiele zu nennen, aber schwer zu beheben. Es sollte auch beachtet werden, dass es nicht allzu schwierig ist, die meisten Probleme zu vermeiden und tragbaren Code zu erzeugen.
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 dies can
passiert. Funktioniert das wirklich mit Compilern, die wir heutzutage verwenden?
Aus dem offiziellen C-Begründungsdokument
Die Begriffe nicht spezifiziertes Verhalten, undefiniertes Verhalten und implementierungsdefiniertes Verhalten werden verwendet, um das Ergebnis des Schreibens von Programmen zu kategorisieren, deren Eigenschaften der Standard nicht vollständig beschreibt oder nicht vollständig beschreiben kann. Das Ziel dieser Kategorisierung besteht darin, eine bestimmte Vielfalt von Implementierungen zuzulassen, die es ermöglicht, dass die Qualität der Implementierung eine aktive Kraft auf dem Markt ist, sowie bestimmte beliebte Erweiterungen zuzulassen, ohne das Gütesiegel der Konformität mit dem Standard zu entfernen. Anhang F des Standards katalogisiert die Verhaltensweisen, die in eine dieser drei Kategorien fallen.
Nicht spezifiziertes Verhalten gibt dem Implementierer einen gewissen Spielraum bei der Übersetzung von Programmen. Dieser Spielraum reicht nicht so weit, dass das Programm nicht übersetzt werden kann.
Undefiniertes Verhalten gibt dem Implementierer die Lizenz, bestimmte Programmfehler, die schwer zu diagnostizieren sind, nicht abzufangen. Es werden auch Bereiche mit möglichen konformen Spracherweiterungen identifiziert: Der Implementierer kann die Sprache erweitern, indem er eine Definition des offiziell nicht definierten Verhaltens bereitstellt.
Durch die Implementierung definiertes Verhalten gibt einem Implementierer die Freiheit, den geeigneten Ansatz zu wählen, erfordert jedoch, dass diese Auswahl dem Benutzer erklärt wird. Als implementierungsdefiniert bezeichnete Verhaltensweisen sind im Allgemeinen solche, bei denen ein Benutzer basierend auf der Implementierungsdefinition aussagekräftige Codierungsentscheidungen treffen kann. Implementierer sollten dieses Kriterium berücksichtigen, wenn sie entscheiden, wie umfangreich eine Implementierungsdefinition sein soll. Wie bei nicht angegebenem Verhalten ist es keine angemessene Antwort, die Quelle, die das implementierungsdefinierte Verhalten enthält, einfach nicht zu übersetzen.
Undefiniertes Verhalten vs. nicht spezifiziertes Verhalten enthält eine kurze Beschreibung.
Ihre letzte Zusammenfassung:
Zusammenfassend lässt sich sagen, dass Sie sich über nicht angegebenes Verhalten normalerweise keine Sorgen machen sollten, es sei denn, Ihre Software muss portabel sein. Umgekehrt ist undefiniertes Verhalten immer unerwünscht und sollte niemals auftreten.
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:
int scaled_velocity(int v, unsigned char pow)
{
if (v > 250)
v = 250;
if (v < -250)
v = -250;
return v << pow;
}
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 er v
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ässt v
. 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.
<<
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
Verhalten für ein wohlgeformtes Programmkonstrukt und korrekte Daten, das von der Implementierung abhängt und das jede Implementierung dokumentiert
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
Verhalten, für das diese Internationale Norm keine Anforderungen stellt [Hinweis: Undefiniertes Verhalten kann erwartet werden, wenn diese Internationale Norm keine explizite Definition des Verhaltens enthält oder wenn ein Programm ein fehlerhaftes Konstrukt oder fehlerhafte Daten verwendet. 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 für die Umgebung charakteristischen dokumentierten Weise (mit oder ohne Ausgabe einer Diagnosemeldung) bis zum Beenden einer Übersetzung oder Ausführung (mit der Ausgabe) einer Diagnosemeldung). Viele fehlerhafte Programmkonstrukte erzeugen kein undefiniertes Verhalten. Sie müssen diagnostiziert werden. - Endnote]
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
Verhalten für ein wohlgeformtes Programmkonstrukt und korrekte Daten, das von der Implementierung abhängt [Hinweis: Die Implementierung ist nicht erforderlich, um zu dokumentieren, welches Verhalten auftritt. Der Bereich möglicher Verhaltensweisen wird normalerweise durch diese Internationale Norm beschrieben. - Endnote]
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.
Implementierung definiert-
Implementierer wünschen, sollten gut dokumentiert sein, Standard gibt Auswahlmöglichkeiten, aber sicher zu kompilieren
Nicht spezifiziert -
Entspricht der Implementierung, ist jedoch nicht dokumentiert
Nicht definiert-
Alles könnte passieren, kümmere dich darum.
uint32_t s;
Bewertung, 1u<<s
wann s
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, dass s
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 .