Wie undefiniert ist undefiniertes Verhalten?

Ich bin mir nicht ganz sicher, ob und inwieweit undefiniertes Verhalten ein Programm gefährden kann.

Sagen wir, ich habe diesen Code:

#include  int main() { int v = 0; scanf("%d", &v); if (v != 0) { int *p; *p = v; // Oops } return v; } 

Ist das Verhalten dieses Programms nur für die Fälle undefiniert, in denen v ungleich Null ist, oder ist es nicht definiert, selbst wenn v null ist?

Ich würde sagen, dass das Verhalten nicht definiert ist, nur wenn die Benutzer eine Zahl ungleich 0 einfügen. Wenn der betreffende Codeabschnitt nicht tatsächlich ausgeführt wird, sind die Bedingungen für UB nicht erfüllt (dh der nicht initialisierte pointers wird nicht erstellt weder dereferenziert).

Ein Hinweis darauf findet sich in 3.4.3 im Standard:

Verhalten bei der Verwendung eines nicht tragbaren oder errorshaften Programmkonstrukts oder errorshafter Daten, für die diese Internationale Norm keine Anforderungen stellt

Dies scheint zu implizieren, dass, wenn solche “errorshaften Daten” stattdessen korrekt wären, das Verhalten perfekt definiert wäre – was für unseren Fall ziemlich zutreffend scheint.


Zusätzliches Beispiel: Integer-Überlauf. Jedes Programm, das einen Zusatz mit vom Benutzer bereitgestellten Daten durchführt, ohne es umfassend zu prüfen, unterliegt einem solchen undefinierten Verhalten – aber ein Zusatz ist UB nur, wenn der Benutzer solche bestimmten Daten bereitstellt.

Da dies das Language-Lawyer- Tag hat, habe ich ein äußerst pingeliges Argument, dass das Verhalten des Programms unabhängig von Benutzereingaben undefiniert ist, aber nicht aus den zu erwartenden Gründen – obwohl es gut definiert sein kann (wenn v==0 ) abhängig von der Implementierung.

Das Programm definiert main als

 int main() { /* ... */ } 

C99 5.1.2.2.1 sagt, dass die Hauptfunktion entweder als definiert werden soll

 int main(void) { /* ... */ } 

oder wie

 int main(int argc, char *argv[]) { /* ... */ } 

oder gleichwertig; oder in einer anderen implementierungsdefinierten Weise.

int main() ist nicht äquivalent zu int main(void) . Ersteres besagt, dass main eine feste, aber nicht spezifizierte Anzahl und Art von Argumenten annimmt. Letzteres sagt, es braucht keine Argumente. Der Unterschied besteht darin, dass ein rekursiver Aufruf von main wie

 main(42); 

ist eine Constraint-Verletzung, wenn Sie int main(void) , aber nicht, wenn Sie int main() .

Zum Beispiel, diese beiden Programme:

 int main() { if (0) main(42); /* not a constraint violation */ } 

 int main(void) { if (0) main(42); /* constraint violation, requires a diagnostic */ } 

sind nicht gleichwertig.

Wenn die Implementierung dokumentiert, dass sie int main() als Erweiterung akzeptiert, gilt dies nicht für diese Implementierung .

Dies ist ein extrem nickpickender Punkt (über den nicht alle zustimmen) und lässt sich leicht vermeiden, indem man int main(void) deklariert (was Sie ohnehin tun sollten; alle functionen sollten Prototypen haben, nicht alte Deklarationen / Definitionen).

In der Praxis akzeptiert jeder Compiler, den ich gesehen habe, int main() ohne Beanstandung.

Um die Frage zu beantworten, die beabsichtigt war:

Sobald diese Änderung vorgenommen wurde, ist das Verhalten des Programms gut definiert, wenn v==0 , und ist nicht definiert, wenn v!=0 . Ja, die Definition des Programmverhaltens hängt von der Benutzereingabe ab. Da ist nichts besonders ungewöhnlich.

Lassen Sie mich argumentieren, warum ich denke, dass dies immer noch undefiniert ist.

Erstens sind die Responder, die das sagen, “meistens definiert” oder etwas, das aufgrund ihrer Erfahrung mit einigen Compilern einfach falsch ist. Eine kleine Modifikation Ihres Beispiels soll illustrieren:

 #include  int main() { int v; scanf("%d", &v); if (v != 0) { printf("Hello\n"); int *p; *p = v; // Oops } return v; } 

Was macht dieses Programm, wenn Sie “1” als Eingabe angeben? Wenn du antwortest “Es druckt Hallo und stürzt dann ab”, liegst du falsch. “Undefiniertes Verhalten” bedeutet nicht, dass das Verhalten einer bestimmten statement nicht definiert ist. Es bedeutet, dass das Verhalten des gesamten Programms nicht definiert ist. Der Compiler darf annehmen, dass Sie kein undefiniertes Verhalten haben. In diesem Fall kann er also annehmen, dass v nicht Null ist, und einfach keinen der Klammercodes ausgeben, einschließlich des printf .

Wenn Sie denken, dass dies unwahrscheinlich ist, denken Sie noch einmal darüber nach. GCC führt diese Analyse möglicherweise nicht genau durch, aber es führt sehr ähnliche aus. Mein liebstes Beispiel, das wirklich den Punkt für real veranschaulicht:

 int test(int x) { return x+1 > x; } 

Versuchen Sie, ein kleines Testprogramm zu INT_MAX , um INT_MAX , INT_MAX+1 und test(INT_MAX) . ( INT_MAX Sie sicher, dass die Optimierung aktiviert ist.) Eine typische Implementierung könnte INT_MAX als 2147483647, INT_MAX+1 als -2147483648 und test(INT_MAX) als 1 test(INT_MAX) .

Tatsächlich kompiliert GCC diese function, um eine Konstante 1 zurückzugeben. Warum? Da der Ganzzahlüberlauf ein nicht definiertes Verhalten ist, kann der Compiler davon ausgehen, dass Sie dies nicht tun. Daher kann x nicht gleich INT_MAX , daher ist x+1 größer als x , daher kann diese function bedingungslos 1 zurückgeben.

Undefiniertes Verhalten kann und führt zu Variablen, die nicht mit sich selbst übereinstimmen, negativen Zahlen, die größere Zahlen als positive Zahlen (siehe oben) vergleichen, und anderen bizarren Verhaltensweisen. Je schlauer der Compiler, desto bizarrer ist das Verhalten.

OK, ich gebe zu, ich kann nicht Kapitel und Vers des Standards zitieren, um die genaue Frage zu beantworten, die Sie gestellt haben. Aber Leute, die sagen “Ja ja, aber im wirklichen Leben Dereferenzierung von NULL gibt nur einen seg-Fehler” sind mehr falsch, als sie sich vorstellen können, und sie werden mehr falsch mit jeder Compiler-Generation.

Und wenn der Code im wirklichen Leben tot ist, sollten Sie ihn entfernen. Wenn es nicht tot ist, dürfen Sie kein undefiniertes Verhalten aufrufen. Das ist meine Antwort auf deine Frage.

Wenn v 0 ist, wird Ihre zufällige pointerszuweisung nie ausgeführt, und die function gibt Null zurück, so dass es kein undefiniertes Verhalten ist

Wenn Sie Variablen (insbesondere explizite pointers) deklarieren, wird ein Stück Speicher zugewiesen (normalerweise ein Int). Dieser Speicherfrieden wird dem System als free markiert, aber der alte Wert, der dort gespeichert wird, wird nicht gelöscht (dies hängt davon ab, dass die Speicherzuweisung vom Compiler implementiert wird, es könnte den Platz mit Nullen füllen), so dass Ihr int *p ein hat Zufallswert (Junk), den es als integer interpretieren muss. Das Ergebnis ist der Ort im Speicher, an dem p auf (p’s pointer) zeigt. Wenn Sie versuchen, dereference (aka. Zugriff auf diesen Teil des Speichers), wird es (fast jedes Mal) von einem anderen process / Programm belegt, so dass der Speichermanager Zugriffsschutzprobleme verursacht, wenn Sie andere Speicher ändern / ändern .

In diesem Beispiel führt jeder andere Wert zu einem nicht definierten Verhalten, weil niemand weiß, worauf *p in diesem Moment zeigt.

Ich hoffe, diese Erklärung ist hilfreich.

Edit: Ah, tut mir leid, wieder ein paar Antworten vor mir 🙂

Es ist einfach. Wenn ein Stück Code nicht ausgeführt wird, hat es kein Verhalten !!!, ob definiert oder nicht .

Wenn die Eingabe 0 ist, wird der Code innerhalb von if nicht ausgeführt. Es hängt also vom Rest des Programms ab, ob das Verhalten definiert ist (in diesem Fall ist es definiert).

Wenn die Eingabe nicht 0 ist, führen Sie Code aus, von dem wir alle wissen, dass er nicht definiert ist.

Ich würde sagen, es macht das ganze Programm undefiniert.

Der Schlüssel zu undefiniertem Verhalten ist, dass es nicht definiert ist . Der Compiler kann tun, was er will, wenn er diese statement sieht. Jetzt wird jeder Compiler wie erwartet damit umgehen, aber er hat immer noch das Recht zu tun, was er will – einschließlich der Änderung von Teilen, die nichts damit zu tun haben.

Zum Beispiel kann ein Compiler dem Programm die Meldung “Dieses Programm kann gefährlich sein” hinzufügen, wenn es ein undefiniertes Verhalten feststellt. Dies würde die Ausgabe ändern, unabhängig davon, ob v 0 ist oder nicht.

Ihr Programm ist ziemlich gut definiert. Wenn v == 0 ist, gibt es Null zurück. Wenn v! = 0 ist, spritzt es über einen zufälligen Punkt im Speicher.

p ist ein pointers, sein Anfangswert könnte alles sein, da Sie ihn nicht initialisieren. Der tatsächliche Wert hängt vom Betriebssystem ab (einige Null Speicher bevor Sie es Ihrem process geben, einige nicht), Ihr Compiler, Ihre Hardware und was war im Speicher, bevor Sie Ihr Programm ausgeführt haben.

Die pointerszuweisung schreibt nur in einen zufälligen Speicherort. Es könnte erfolgreich sein, es könnte andere Daten korrumpieren oder es könnte einen Fehler verursachen – es hängt von allen oben genannten Faktoren ab.

Soweit C geht, ist es ziemlich gut definiert, dass nicht-initialisierte Variablen keinen bekannten Wert haben und Ihr Programm (obwohl es kompilieren könnte) nicht korrekt ist.