Was macht diese Verwendung von Zeigern unvorhersehbar?

108

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.

trungnt
quelle
2
Sind Sie sicher, dass Sie den Code des Professors korrekt wiedergegeben haben? Es ist zwar formal möglich zu argumentieren, dass dieses Programm "unvorhersehbares" Verhalten hervorrufen könnte, aber es macht keinen Sinn, dies zu tun. Und ich bezweifle, dass jeder Professor etwas so Esoterisches verwenden würde, um den Studenten "unvorhersehbar" zu veranschaulichen.
Am
1
@ Lightness-Rennen im Orbit: Compiler dürfen nach der Ausgabe der erforderlichen Diagnosemeldungen falsch geformten Code "akzeptieren". Die Sprachspezifikation definiert jedoch nicht das Verhalten des Codes. Das heißt, aufgrund des Fehlers bei der Initialisierung von shat das Programm, wenn es von einem Compiler akzeptiert wird, formal ein unvorhersehbares Verhalten.
Am
2
@TheParamagneticCroissant: Nein. Die Initialisierung ist in der heutigen Zeit schlecht geformt.
Leichtigkeitsrennen im Orbit
2
@ The Paramagnetic Croissant: Wie ich oben sagte, benötigt die Sprache keinen schlecht geformten Code, um "nicht kompiliert werden zu können". Compiler müssen lediglich eine Diagnose ausstellen. Danach dürfen sie weitermachen und den Code "erfolgreich" kompilieren. Das Verhalten eines solchen Codes wird jedoch nicht durch die Sprachspezifikation definiert.
Am
2
Ich würde gerne wissen, was die Antwort war, die Ihr Professor Ihnen gegeben hat.
Daniël W. Crompton

Antworten:

125

Das Verhalten des Programms ist nicht vorhanden, weil es schlecht geformt ist.

char* s = "My String";

Das ist illegal. Vor 2011 war es 12 Jahre lang veraltet.

Die richtige Zeile lautet:

const char* s = "My String";

Davon abgesehen ist das Programm in Ordnung. Ihr Professor sollte weniger Whisky trinken!

Leichtigkeitsrennen im Orbit
quelle
10
mit -pedantic tut es: main.cpp: 6: 16: Warnung: ISO C ++ verbietet die Konvertierung einer String-Konstante in 'char *' [-Wpedantic]
marcinj
17
@black: Nein, die Tatsache, dass die Konvertierung illegal ist, macht das Programm schlecht geformt. Es wurde in der Vergangenheit veraltet . Wir sind nicht mehr in der Vergangenheit.
Leichtigkeitsrennen im Orbit
17
(Was albern ist, weil das der Zweck der 12-jährigen Abwertung war)
Leichtigkeitsrennen im Orbit
17
@black: Das Verhalten eines schlecht geformten Programms ist nicht "perfekt definiert".
Leichtigkeitsrennen im Orbit
11
Unabhängig davon handelt es sich bei der Frage um C ++ und nicht um eine bestimmte Version von GCC.
Leichtigkeitsrennen im Orbit
81

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:

char * s = "My String";

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 der charFamilie der String-Literale war eine solche Initialisierung immer illegal. Beispielsweise:

const int arr[] = {1};
int *p = arr; // nope!

In Pre-C ++ 11 gab es jedoch für String-Literale eine Ausnahme in §4.2 / 2:

Ein String-Literal (2.13.4), das kein breites String-Literal ist, kann in einen Wert vom Typ " Zeiger auf Zeichen" konvertiert werden . [...]. In beiden Fällen ist das Ergebnis ein Zeiger auf das erste Element des Arrays. Diese Konvertierung wird nur berücksichtigt, wenn ein explizit geeigneter Zeigerzieltyp vorhanden ist, und nicht, wenn generell eine Konvertierung von einem l-Wert in einen r-Wert erforderlich ist. [Hinweis: Diese Konvertierung ist veraltet . Siehe Anhang D. ]

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:

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

Damit ist der Rest des Programms vollkommen in Ordnung, da Sie nie swieder berühren . Das Lesen eines erstellten constObjekts über einen Nichtzeiger constist vollkommen in Ordnung. Das Schreiben eines erstellten constObjekts über einen solchen Zeiger ist ein undefiniertes Verhalten:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

Da es an keiner sStelle 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 Sie saber, 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

Barry
quelle
Ich denke, hier bedeutet "unvorhersehbar" vom Professor, dass man den Standard nicht verwenden kann, um vorherzusagen, was ein Compiler mit schlecht geformtem Code machen wird (über die Ausgabe einer Diagnose hinaus). Ja, es könnte so behandelt werden, wie C ++ 03 sagt, dass es behandelt werden sollte, und (unter dem Risiko des Irrtums "No True Scotsman") der gesunde Menschenverstand erlaubt es uns, mit einiger Sicherheit vorherzusagen, dass dies das einzige ist, was ein vernünftiger Compiler-Autor ist wird jemals wählen, ob der Code überhaupt kompiliert wird. Andererseits könnte es so verstanden werden, dass das String-Literal umgekehrt wird, bevor es in non-const umgewandelt wird. Standard C ++ ist das egal.
Steve Jessop
2
@SteveJessop Ich kaufe diese Interpretation nicht. Dies ist weder ein undefiniertes Verhalten noch die Kategorie des schlecht geformten Codes, die der Standard als keine Diagnose erforderlich kennzeichnet. Es handelt sich um eine einfache Systemverletzung, die sehr vorhersehbar sein sollte (kompiliert und erledigt normale Dinge unter C ++ 03, kompiliert nicht unter C ++ 11). Sie können Compiler-Bugs (oder künstlerische Lizenzen) nicht wirklich verwenden, um darauf hinzuweisen, dass Code unvorhersehbar ist - andernfalls wäre der gesamte Code tautologisch unvorhersehbar.
Barry
Ich spreche nicht über Compiler-Fehler, ich spreche darüber, ob der Standard das Verhalten (falls vorhanden) des Codes definiert oder nicht. Ich vermute, dass der Professor dasselbe tut, und "unvorhersehbar" ist nur eine mit Schinkenfäusten versehene Art zu sagen, dass der aktuelle Standard das Verhalten nicht definiert. Jedenfalls scheint mir das wahrscheinlicher, als dass der Professor fälschlicherweise glaubt, dass dies ein wohlgeformtes Programm mit undefiniertem Verhalten ist.
Steve Jessop
1
Nein, tut es nicht. Der Standard definiert nicht das Verhalten von schlecht geformten Programmen.
Steve Jessop
1
@supercat: Es ist ein fairer Punkt, aber ich glaube nicht, dass es der Hauptgrund ist. Ich denke, der Hauptgrund, warum der Standard das Verhalten von schlecht geformten Programmen nicht spezifiziert, ist, dass Compiler Erweiterungen der Sprache unterstützen können, indem sie eine Syntax hinzufügen, die nicht gut geformt ist (wie es Objective C tut). Es ist nur ein Bonus, der Implementierung zu erlauben, nach einer fehlgeschlagenen Kompilierung einen totalen Horlick aus dem Aufräumen zu machen :-)
Steve Jessop
20

Andere Antworten haben ergeben, dass dieses Programm in C ++ 11 aufgrund der Zuweisung eines const charArrays zu a schlecht ausgebildet ist char *.

Das Programm war jedoch auch vor C ++ 11 schlecht geformt.

Die operator<<Überlastungen sind in <ostream>. Die Anforderung iostreamzum Einschließen ostreamwurde in C ++ 11 hinzugefügt.

In der Vergangenheit hatten iostreamdie meisten Implementierungen ostreamohnehin Folgendes enthalten , möglicherweise zur Vereinfachung der Implementierung oder um eine bessere Lebensqualität zu gewährleisten.

Es wäre jedoch konform iostream, nur die ostreamKlasse zu definieren, ohne die operator<<Überladungen zu definieren .

MM
quelle
13

Das einzig etwas Falsche an diesem Programm ist, dass Sie einem veränderlichen charZeiger kein String-Literal zuweisen sollen , obwohl dies häufig als Compiler-Erweiterung akzeptiert wird.

Ansonsten erscheint mir dieses Programm gut definiert:

  • Die Regeln, die vorschreiben, wie Zeichenarrays zu Zeichenzeigern werden, wenn sie als Parameter übergeben werden (z. B. mit cout << s2), sind genau definiert.
  • Das Array ist nullterminiert, was eine Bedingung für operator<<a char*(oder a const char*) ist.
  • #include <iostream>enthält <ostream>, was wiederum definiert operator<<(ostream&, const char*), so dass alles an Ort und Stelle zu sein scheint.
zneak
quelle
12

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.

Graham
quelle
10

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

Superkatze
quelle