Ob in C oder C ++, ich denke, dass dieses illegale Programm, dessen Verhalten gemäß dem C- oder C ++ - Standard undefiniert ist, interessant ist:
#include <stdio.h>
int foo() {
int a;
const int b = a;
a = 555;
return b;
}
void bar() {
int x = 123;
int y = 456;
}
int main() {
bar();
const int n1 = foo();
const int n2 = foo();
const int n3 = foo();
printf("%d %d %d\n", n1, n2, n3);
return 0;
}
Ausgabe auf meinem Computer (nach Kompilierung ohne Optimierung):
123 555 555
Ich denke, dass dieses illegale Programm interessant ist, weil es die Stapelmechanik veranschaulicht, weil der Grund, warum man C oder C ++ (anstelle von beispielsweise Java) verwendet, darin besteht, nahe an der Hardware, nahe an der Stapelmechanik und dergleichen zu programmieren.
Wenn jedoch in StackOverflow der Code eines Fragestellers versehentlich aus dem nicht initialisierten Speicher gelesen wird, zitieren die am stärksten bewerteten Antworten ausnahmslos den C- oder C ++ - Standard (insbesondere C ++), sodass das Verhalten undefiniert ist. Dies gilt natürlich für den Standard - das Verhalten ist in der Tat undefiniert -, aber es ist merkwürdig, dass alternative Antworten, die aus Hardware- oder stapelmechanischer Sicht versuchen, zu untersuchen, warum ein bestimmtes undefiniertes Verhalten (wie das Ausgabe oben) könnte aufgetreten sein, sind selten und werden tendenziell ignoriert.
Ich erinnere mich sogar an eine Antwort, die darauf hinwies, dass undefiniertes Verhalten das Neuformatieren meiner Festplatte beinhalten könnte. Ich habe mir darüber jedoch keine allzu großen Sorgen gemacht, bevor ich das obige Programm ausgeführt habe.
Meine Frage lautet: Warum ist es wichtiger, den Lesern lediglich beizubringen, dass Verhalten in C oder C ++ undefiniert ist, als das undefinierte Verhalten zu verstehen? Ich meine, wenn der Leser das undefinierte Verhalten verstehen würde, würde er es dann nicht eher vermeiden?
Meine Ausbildung ist zufällig in Elektrotechnik und ich arbeite als Bauingenieur. Das letzte Mal, dass ich als Programmierer per se gearbeitet habe, war 1994, daher bin ich neugierig, die Perspektive von Anwendern mit konventionelleren, mehr zu verstehen Aktuelle Hintergründe der Softwareentwicklung.
quelle
Antworten:
Die Wertanalyse von Frama-C, ein statischer Analysator, dessen angebliches Ziel es ist, alle undefinierten Verhaltensweisen in einem C-Programm zu finden, betrachtet die Zuordnung
const int b = a;
als in Ordnung. Dies ist eine bewusste Entwurfsentscheidung, um zu ermöglichenmemcpy()
(normalerweise als Schleife überunsigned char
Elemente eines virtuellen Arrays implementiert , und dass der C-Standard möglicherweise eine erneute Implementierung als solche zulässt), einstruct
(das Polster und nicht initialisierte Elemente haben kann ) nach zu kopieren Ein weiterer.Die "Ausnahme" gilt nur für
lvalue = lvalue;
Zuweisungen ohne dazwischenliegende Konvertierung, dh für Zuweisungen, die einer Kopie eines Speicherabschnitts für einen Speicherort an einen anderen entsprechen.Ich (als einer der Autoren der Wertanalyse von Frama-C) habe dies mit Xavier Leroy zu einem Zeitpunkt besprochen, als er sich selbst über die Definition im verifizierten C-Compiler CompCert wunderte, sodass er möglicherweise dieselbe Definition verwendet hat. Es ist meiner Meinung nach sauberer als das, was der C-Standard mit unbestimmten Werten zu tun versucht, die
unsigned char
Fallenrepräsentationen sein können, und dem Typ , der garantiert keine Fallendarstellungen hat, aber sowohl CompCert als auch Frama-C nehmen relativ nicht exotische Ziele an. und vielleicht hat das Standardisierungskomitee versucht, Plattformen unterzubringen, auf denen das Lesen eines nicht initialisiertenint
Programms das Programm tatsächlich abbrechen kann.Wiederkehrende
b
oder vorbein1
,n2
odern3
bisprintf
in dem Ende zumindest undefiniertes Verhalten betrachtet werden, da eine nicht initialisierte Scheibe Speicherkopieren macht es nicht initialisiert. Mit einer alten Frama-C-Version:Und in einer alten Version von CompCert nach geringfügigen Änderungen, um das Programm für es akzeptabel zu machen:
quelle
Undefiniertes Verhalten bedeutet letztendlich, dass das Verhalten nicht deterministisch ist. Programmierer, die nicht wissen, dass sie nicht deterministischen Code schreiben, sind
nur schlechte,ignorante Programmierer. Diese Seite soll Programmierer besser (und weniger unwissend) machen.Es ist nicht unmöglich, angesichts nicht deterministischen Verhaltens ein korrektes Programm zu schreiben. Es ist jedoch eine spezialisierte Programmierumgebung und erfordert eine andere Art von Programmierdisziplin.
Selbst in Ihrem Beispiel können sich die Werte auf dem "Stapel" so ändern, dass Sie nicht die erwarteten Werte erhalten, wenn das Programm ein extern ausgelöstes Signal empfängt. Wenn die Maschine Trap-Werte hat, kann das Lesen von Zufallswerten außerdem dazu führen, dass etwas Seltsames passiert.
quelle
Weil das spezifische Verhalten möglicherweise nicht wiederholbar ist, selbst von Lauf zu Lauf ohne Neuerstellung.
Genau das zu verfolgen, was passiert ist, mag eine nützliche akademische Übung sein, um die Macken Ihrer speziellen Plattform besser zu verstehen, aber aus Codierungssicht ist die einzig relevante Lektion "Mach das nicht". Ein Ausdruck wie
a++ * a++
ist ein Codierungsfehler, Punkt. Das ist wirklich alles jemand muss wissen.quelle
"Undefiniertes Verhalten" ist die Abkürzung für "Dieses Verhalten ist nicht deterministisch. Es wird sich wahrscheinlich nicht nur auf verschiedenen Compilern oder Hardwareplattformen unterschiedlich verhalten, sondern auch auf verschiedenen Versionen desselben Compilers."
Die meisten Programmierer würden dies als unerwünschtes Merkmal betrachten, insbesondere da C und C ++ standardbasierte Sprachen sind . Das heißt, Sie verwenden sie teilweise, weil die Sprachspezifikation bestimmte Garantien für das Verhalten der Sprache gibt, wenn Sie einen standardkonformen Compiler verwenden.
Wie bei den meisten Dingen in der Programmierung müssen Sie die Vor- und Nachteile abwägen. Wenn der Vorteil einer Operation, die UB ist, die Schwierigkeit übersteigt, ein stabiles, plattformunabhängiges Verhalten zu erzielen, verwenden Sie auf jeden Fall das undefinierte Verhalten. Die meisten Programmierer werden denken, dass es sich die meiste Zeit nicht lohnt.
Das Mittel gegen undefiniertes Verhalten besteht darin, das Verhalten zu untersuchen, das Sie bei einer bestimmten Plattform und einem bestimmten Compiler tatsächlich erhalten. Diese Art von Prüfung wird wahrscheinlich nicht von einem erfahrenen Programmierer in einem Q & A-Umfeld für Sie untersucht.
quelle
(-1)<<1
dass C89 auf Plattformen, die nicht gepolstertes Zweierkomplement verwenden, als -2 definiert wird ...Wenn in der Dokumentation für einen bestimmten Compiler angegeben ist, was zu tun ist, wenn Code etwas tut, das vom Standard als "undefiniertes Verhalten" eingestuft wird, funktioniert Code, der sich auf dieses Verhalten stützt, beim Kompilieren mit diesem Compiler ordnungsgemäß , kann sich jedoch in beliebiger Weise verhalten, wenn Kompiliert mit einem anderen Compiler, dessen Dokumentation das Verhalten nicht spezifiziert.
Wenn in der Dokumentation für einen Compiler nicht angegeben ist, wie er mit einem bestimmten "undefinierten Verhalten" umgehen soll, sagt die Tatsache, dass das Verhalten eines Programms bestimmten Regeln zu entsprechen scheint, nichts darüber aus, wie sich ähnliche Programme verhalten. Jede Vielzahl von Faktoren kann dazu führen, dass ein Compiler Code ausgibt, der unerwartete Situationen unterschiedlich behandelt - manchmal auf scheinbar bizarre Weise.
Betrachten Sie beispielsweise einen Computer mit
int
einer 32-Bit-Ganzzahl:Wenn
size1
undsize2
Wären beide gleich 46341 (ihr Produkt ist 2147488281), könnte man erwarten, dass die Funktion 3 zurückgibt, aber ein Compiler könnte den ersten Test legitimerweise vollständig überspringen. Entweder wäre das Produkt klein genug, dass die Bedingung falsch wäre, oder die bevorstehende Multiplikation würde überlaufen und den Compiler von jeglicher Verpflichtung entbinden, etwas zu tun oder getan zu haben. Während ein solches Verhalten bizarr erscheinen mag, scheinen einige Compilerautoren sehr stolz auf die Fähigkeit ihrer Compiler zu sein, solche "unnötigen" Tests zu eliminieren. Einige Leute könnten erwarten, dass ein Überlauf bei der zweiten Multiplikation im schlimmsten Fall dazu führen würde, dass alle Bits dieses bestimmten Produkts willkürlich beschädigt werden. in der Tat jedochquelle
int
ein 32-Bit - Ganzzahl, dann Werte vom Typuint16_t
wird gefördert werden ,int
bevor irgendwelche Berechnungen mit ihnen. Eine Regel, die im Allgemeinen in Ordnung wäre, wenn Implementierungen signierte Arithmetik nur dann als anders als nicht signiert behandeln würden, wenn sie ein anderes definiertes Verhalten hätten.unsigned
und einen Wertebereich haben, der vollständig in den von passtint
, zu einem signierten Typ befördert werdenint
.