Ich lerne gerade Zeiger und mein Professor hat diesen Code als Beispiel bereitgestellt:
//We cannot predict the behavior of this program!
#include <iostream>
using namespace std;
int main()
{
char * s = "My String";
char s2[] = {'a', 'b', 'c', '\0'};
cout << s2 << endl;
return 0;
}
Er schrieb in den Kommentaren, dass wir das Verhalten des Programms nicht vorhersagen können. Was genau macht es jedoch unvorhersehbar? Ich sehe nichts falsch daran.
s
hat das Programm, wenn es von einem Compiler akzeptiert wird, formal ein unvorhersehbares Verhalten.Antworten:
Das Verhalten des Programms ist nicht vorhanden, weil es schlecht geformt ist.
Das ist illegal. Vor 2011 war es 12 Jahre lang veraltet.
Die richtige Zeile lautet:
Davon abgesehen ist das Programm in Ordnung. Ihr Professor sollte weniger Whisky trinken!
quelle
Die Antwort lautet: Es hängt davon ab, gegen welchen C ++ - Standard Sie kompilieren. Der gesamte Code ist über alle Standards hinweg perfekt formuliert ‡ mit Ausnahme dieser Zeile:
Jetzt hat das Zeichenfolgenliteral den Typ
const char[10]
und wir versuchen, einen nicht konstanten Zeiger darauf zu initialisieren. Für alle anderen Typen außer derchar
Familie der String-Literale war eine solche Initialisierung immer illegal. Beispielsweise:In Pre-C ++ 11 gab es jedoch für String-Literale eine Ausnahme in §4.2 / 2:
In C ++ 03 ist der Code also vollkommen in Ordnung (obwohl veraltet) und weist ein klares, vorhersehbares Verhalten auf.
In C ++ 11 existiert dieser Block nicht - es gibt keine solche Ausnahme für String-Literale, in die konvertiert wurde
char*
, und daher ist der Code genauso schlecht geformt wie derint*
Beispiel, das ich gerade bereitgestellt habe. Der Compiler ist verpflichtet, eine Diagnose zu stellen, und im Idealfall würden wir in solchen Fällen, bei denen es sich eindeutig um Verstöße gegen das System vom Typ C ++ handelt, erwarten, dass ein guter Compiler diesbezüglich nicht nur konform ist (z. B. durch Ausgabe einer Warnung), sondern auch fehlschlägt geradezu.Der Code sollte im Idealfall nicht kompiliert werden - aber sowohl auf gcc als auch auf clang (ich nehme an, da es wahrscheinlich viel Code gibt, der mit geringem Gewinn gebrochen werden würde, obwohl diese Art von Systemloch seit über einem Jahrzehnt veraltet ist). Der Code ist schlecht geformt, und daher ist es nicht sinnvoll, über das Verhalten des Codes nachzudenken. Aber angesichts dieses speziellen Falls und der Geschichte, in der er zuvor erlaubt war, halte ich es nicht für unangemessen, den resultierenden Code so zu interpretieren, als wäre er implizit
const_cast
, so etwas wie:Damit ist der Rest des Programms vollkommen in Ordnung, da Sie nie
s
wieder berühren . Das Lesen eines erstelltenconst
Objekts über einen Nichtzeigerconst
ist vollkommen in Ordnung. Das Schreiben eines erstelltenconst
Objekts über einen solchen Zeiger ist ein undefiniertes Verhalten:Da es an keiner
s
Stelle in Ihrem Code Änderungen gibt, ist das Programm in C ++ 03 in Ordnung, sollte in C ++ 11 nicht kompiliert werden können, tut es aber trotzdem - und da die Compiler dies zulassen, gibt es immer noch kein undefiniertes Verhalten darin † . Angesichts der Tatsache, dass die Compiler die C ++ 03-Regeln immer noch [falsch] interpretieren, sehe ich nichts, was zu "unvorhersehbarem" Verhalten führen würde. Schreiben Sies
aber, und alle Wetten sind aus. In C ++ 03 und C ++ 11.† Auch wenn per Definition schlecht geformter Code keine Erwartung eines angemessenen Verhaltens ergibt.
‡ Außer nicht, siehe Matt McNabbs Antwort
quelle
Andere Antworten haben ergeben, dass dieses Programm in C ++ 11 aufgrund der Zuweisung eines
const char
Arrays zu a schlecht ausgebildet istchar *
.Das Programm war jedoch auch vor C ++ 11 schlecht geformt.
Die
operator<<
Überlastungen sind in<ostream>
. Die Anforderungiostream
zum Einschließenostream
wurde in C ++ 11 hinzugefügt.In der Vergangenheit hatten
iostream
die meisten Implementierungenostream
ohnehin Folgendes enthalten , möglicherweise zur Vereinfachung der Implementierung oder um eine bessere Lebensqualität zu gewährleisten.Es wäre jedoch konform
iostream
, nur dieostream
Klasse zu definieren, ohne dieoperator<<
Überladungen zu definieren .quelle
Das einzig etwas Falsche an diesem Programm ist, dass Sie einem veränderlichen
char
Zeiger kein String-Literal zuweisen sollen , obwohl dies häufig als Compiler-Erweiterung akzeptiert wird.Ansonsten erscheint mir dieses Programm gut definiert:
cout << s2
), sind genau definiert.operator<<
achar*
(oder aconst char*
) ist.#include <iostream>
enthält<ostream>
, was wiederum definiertoperator<<(ostream&, const char*)
, so dass alles an Ort und Stelle zu sein scheint.quelle
Sie können das Verhalten des Compilers aus den oben genannten Gründen nicht vorhersagen. (Es sollte nicht kompiliert werden können, kann aber nicht.)
Wenn die Kompilierung erfolgreich ist, ist das Verhalten genau definiert. Sie können das Verhalten des Programms sicher vorhersagen.
Wenn es nicht kompiliert werden kann, gibt es kein Programm. In einer kompilierten Sprache ist das Programm die ausführbare Datei, nicht der Quellcode. Wenn Sie keine ausführbare Datei haben, haben Sie kein Programm und können nicht über das Verhalten von etwas sprechen, das nicht existiert.
Also würde ich sagen, dass die Aussage Ihres Profis falsch ist. Sie können das Verhalten des Compilers bei diesem Code nicht vorhersagen, dies unterscheidet sich jedoch vom Verhalten des Programms . Wenn er also Nissen pflücken will, sollte er besser sicherstellen, dass er Recht hat. Oder Sie haben ihn natürlich falsch zitiert und der Fehler liegt in Ihrer Übersetzung dessen, was er gesagt hat.
quelle
Wie andere angemerkt haben, ist der Code unter C ++ 11 unzulässig, obwohl er unter früheren Versionen gültig war. Folglich muss ein Compiler für C ++ 11 mindestens eine Diagnose ausgeben, aber das Verhalten des Compilers oder des Restes des Build-Systems ist darüber hinaus nicht spezifiziert. Nichts im Standard würde einem Compiler verbieten, als Reaktion auf einen Fehler abrupt zu beenden, und eine teilweise geschriebene Objektdatei hinterlassen, die ein Linker für gültig hält, was zu einer fehlerhaften ausführbaren Datei führt.
Obwohl ein guter Compiler vor dem Beenden immer sicherstellen sollte, dass eine von ihm erwartete Objektdatei entweder gültig, nicht vorhanden oder als ungültig erkennbar ist, fallen solche Probleme nicht in den Zuständigkeitsbereich des Standards. Zwar gab es in der Vergangenheit einige Plattformen (und möglicherweise auch noch), auf denen eine fehlgeschlagene Kompilierung zu legitim erscheinenden ausführbaren Dateien führen kann, die beim Laden auf willkürliche Weise abstürzen (und ich musste mit Systemen arbeiten, auf denen Verbindungsfehler häufig ein solches Verhalten aufwiesen). Ich würde nicht sagen, dass die Folgen von Syntaxfehlern im Allgemeinen unvorhersehbar sind. Auf einem guten System erzeugt ein versuchter Build im Allgemeinen entweder eine ausführbare Datei mit dem besten Aufwand eines Compilers bei der Codegenerierung oder erzeugt überhaupt keine ausführbare Datei. Einige Systeme hinterlassen nach einem fehlgeschlagenen Build die alte ausführbare Datei.
Meine persönliche Präferenz wäre, dass festplattenbasierte Systeme die Ausgabedatei umbenennen, um die seltenen Fälle zu berücksichtigen, in denen diese ausführbare Datei nützlich wäre, während die Verwirrung vermieden wird, die sich aus der irrtümlichen Annahme ergibt, dass neuer Code ausgeführt wird, und für die eingebettete Programmierung Systeme, mit denen ein Programmierer für jedes Projekt ein Programm angeben kann, das geladen werden soll, wenn eine gültige ausführbare Datei nicht unter dem normalen Namen verfügbar ist [idealerweise etwas, das sicher auf das Fehlen eines verwendbaren Programms hinweist]. Ein Tool-Set für eingebettete Systeme hätte im Allgemeinen keine Möglichkeit zu wissen, was ein solches Programm tun soll, aber in vielen Fällen hat jemand, der "echten" Code für ein System schreibt, Zugriff auf einen Hardwaretestcode, der leicht an das angepasst werden kann Zweck. Ich weiß jedoch nicht, dass ich das Umbenennungsverhalten gesehen habe
quelle