In Kapitel 5 von K & R (The C Programming Language 2nd Edition) habe ich Folgendes gelesen:
Erstens können Zeiger unter bestimmten Umständen verglichen werden. Wenn
p
undq
Punkt an den Mitgliedern des gleichen Array, dann Beziehungen wie==
,!=
,<
,>=
etc. richtig funktionieren.
Dies scheint zu implizieren, dass nur Zeiger verglichen werden können, die auf dasselbe Array zeigen.
Allerdings, als ich diesen Code ausprobiert habe
char t = 't';
char *pt = &t;
char x = 'x';
char *px = &x;
printf("%d\n", pt > px);
1
wird auf den Bildschirm gedruckt.
Zunächst einmal, dachte ich , ich würde oder irgendeine Art oder Fehler nicht definiert werden, weil pt
und px
nicht auf das gleiche Array zeigen werden (zumindest in meinem Verständnis).
Liegt auch pt > px
daran, dass beide Zeiger auf Variablen zeigen, die auf dem Stapel gespeichert sind, und der Stapel nach unten wächst, sodass die Speicheradresse von t
größer als die von x
? Ist. Welches ist, warum pt > px
ist wahr?
Ich werde verwirrter, wenn Malloc eingeführt wird. Auch in K & R in Kapitel 8.7 ist Folgendes geschrieben:
Es gibt jedoch immer noch eine Annahme, dass Zeiger auf verschiedene von zurückgegebene Blöcke
sbrk
sinnvoll verglichen werden können. Dies wird durch den Standard nicht garantiert, der Zeigervergleiche nur innerhalb eines Arrays erlaubt. Somit ist diese Version vonmalloc
nur unter Maschinen portierbar, für die der allgemeine Zeigervergleich sinnvoll ist.
Ich hatte kein Problem damit, Zeiger, die auf den auf dem Heap angegebenen Speicherplatz verweisen, mit Zeigern zu vergleichen, die auf Stapelvariablen verweisen.
Der folgende Code hat beispielsweise beim 1
Drucken einwandfrei funktioniert :
char t = 't';
char *pt = &t;
char *px = malloc(10);
strcpy(px, pt);
printf("%d\n", pt > px);
Aufgrund meiner Experimente mit meinem Compiler denke ich, dass jeder Zeiger mit jedem anderen Zeiger verglichen werden kann, unabhängig davon, wohin er einzeln zeigt. Darüber hinaus denke ich, dass die Zeigerarithmetik zwischen zwei Zeigern in Ordnung ist, unabhängig davon, wo sie einzeln zeigen, da die Arithmetik nur die Speicheradressen verwendet, die die Zeiger speichern.
Trotzdem bin ich verwirrt von dem, was ich in K & R lese.
Der Grund, den ich frage, ist, dass mein prof. machte es tatsächlich zu einer Prüfungsfrage. Er gab den folgenden Code:
struct A { char *p0; char *p1; }; int main(int argc, char **argv) { char a = 0; char *b = "W"; char c[] = [ 'L', 'O', 'L', 0 ]; struct A p[3]; p[0].p0 = &a; p[1].p0 = b; p[2].p0 = c; for(int i = 0; i < 3; i++) { p[i].p1 = malloc(10); strcpy(p[i].p1, p[i].p0); } }
Was bewerten diese:
p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1
Die Antwort ist 0
, 1
und 0
.
(Mein Professor enthält den Haftungsausschluss für die Prüfung, dass die Fragen für eine 64-Bit-Programmierumgebung mit Ubuntu Linux 16.04 und 64-Bit-Version gelten.)
(Anmerkung des Herausgebers: Wenn SO mehr Tags zulässt, würde dieser letzte Teil x86-64 , Linux und möglicherweise Assembly rechtfertigen . Wenn der Punkt der Frage / Klasse spezifisch Details zur Implementierung des Betriebssystems auf niedriger Ebene und nicht portables C wäre.)
C
dem , was ist sicher inC
. Der Vergleich von zwei Zeigern mit demselben Typ kann immer durchgeführt werden (z. B. Überprüfung auf Gleichheit), jedoch mithilfe von Zeigerarithmetik und Vergleich>
und<
ist nur dann sicher, wenn er innerhalb eines bestimmten Arrays (oder Speicherblocks) verwendet wird.Antworten:
Gemäß der C11 - Standard werden die relationalen Operatoren
<
,<=
,>
, und>=
nur von Zeigern auf Elemente des gleichen Array oder struct Objekt verwendet werden kann. Dies ist in Abschnitt 6.5.8p5 beschrieben:Beachten Sie, dass Vergleiche, die diese Anforderung nicht erfüllen, undefiniertes Verhalten hervorrufen , was (unter anderem) bedeutet, dass Sie sich nicht darauf verlassen können, dass die Ergebnisse wiederholbar sind.
In Ihrem speziellen Fall schien die Operation sowohl für den Vergleich zwischen den Adressen zweier lokaler Variablen als auch zwischen der Adresse einer lokalen und einer dynamischen Adresse "zu funktionieren". Das Ergebnis könnte sich jedoch ändern, indem Sie eine scheinbar nicht verwandte Änderung an Ihrem Code vornehmen oder sogar denselben Code mit unterschiedlichen Optimierungseinstellungen kompilieren. Mit undefiniertem Verhalten, nur weil der Code könnte einen Fehler zum Absturz bringen oder erzeugen bedeutet nicht , es wird .
Beispielsweise verfügt ein x86-Prozessor, der im 8086-Realmodus ausgeführt wird, über ein segmentiertes Speichermodell, das ein 16-Bit-Segment und einen 16-Bit-Offset zum Erstellen einer 20-Bit-Adresse verwendet. In diesem Fall wird eine Adresse also nicht genau in eine Ganzzahl konvertiert.
Die Gleichheitsoperatoren
==
und!=
jedoch nicht über diese Einschränkung haben. Sie können zwischen zwei beliebigen Zeigern auf kompatible Typen oder NULL-Zeiger verwendet werden. Die Verwendung von==
oder!=
in beiden Beispielen würde also einen gültigen C-Code erzeugen.Selbst mit
==
und!=
könnten Sie jedoch einige unerwartete, aber immer noch genau definierte Ergebnisse erzielen. Siehe Kann ein Gleichheitsvergleich nicht verwandter Zeiger als wahr bewertet werden? Weitere Details hierzu.In Bezug auf die Prüfungsfrage Ihres Professors werden einige fehlerhafte Annahmen getroffen:
Wenn Sie diesen Code auf einer Architektur und / oder mit einem Compiler ausführen, der diese Annahmen nicht erfüllt, können Sie sehr unterschiedliche Ergebnisse erzielen.
Außerdem zeigen beide Beispiele beim Aufrufen auch ein undefiniertes Verhalten
strcpy
, da der rechte Operand (in einigen Fällen) auf ein einzelnes Zeichen und nicht auf eine nullterminierte Zeichenfolge verweist, was dazu führt, dass die Funktion über die Grenzen der angegebenen Variablen hinaus liest.quelle
<
zwischen demmalloc
Ergebnis und einer lokalen Variablen (automatischer Speicher, dh Stapel) führen würde, könnte er davon ausgehen, dass der Ausführungspfad niemals verwendet wird, und nur die gesamte Funktion zu einerud2
Anweisung kompilieren (was einen unzulässigen Wert auslöst) -Anweisung Ausnahme, die der Kernel behandelt, indem er ein SIGILL an den Prozess liefert). GCC / Clang tun dies in der Praxis für andere Arten von UB, z. B. das Abfallen vom Ende einer Nichtfunktionvoid
. godbolt.org ist momentan nicht verfügbar , aber versuchen Sie es mit Kopieren / Einfügenint foo(){int x=2;}
und beachten Sie das Fehlen einesret
malloc
, um mehr Speicher vom Betriebssystem abzurufen. Es besteht also kein Grund anzunehmen, dass Ihre lokalen Variablen (Threadstapel) über dermalloc
dynamischen Zuweisung liegen Lager.int x,y;
eine Implementierung gegeben ...Das Hauptproblem beim Vergleichen von Zeigern mit zwei unterschiedlichen Arrays desselben Typs besteht darin, dass die Arrays selbst nicht an einer bestimmten relativen Position platziert werden müssen - eines könnte vor und nach dem anderen enden.
Nein, das Ergebnis hängt von der Implementierung und anderen unvorhersehbaren Faktoren ab.
Es gibt nicht unbedingt einen Stapel . Wenn es existiert, muss es nicht nachwachsen. Es könnte erwachsen werden. Es könnte auf bizarre Weise nicht zusammenhängend sein.
Schauen wir uns die C-Spezifikation an , §6.5.8 auf Seite 85, in der relationale Operatoren (dh die von Ihnen verwendeten Vergleichsoperatoren) erläutert werden. Beachten Sie, dass dies nicht für direkte
!=
oder==
Vergleich gilt.Der letzte Satz ist wichtig. Während ich einige nicht verwandte Fälle reduziert habe, um Platz zu sparen, ist uns ein Fall wichtig: zwei Arrays, die nicht Teil desselben Struktur- / Aggregatobjekts 1 sind , und wir vergleichen Zeiger mit diesen beiden Arrays. Dies ist undefiniertes Verhalten .
Während Ihr Compiler gerade eine Art CMP-Maschinenanweisung (Vergleichsanweisung) eingefügt hat, die die Zeiger numerisch vergleicht, und Sie hier Glück hatten, ist UB ein ziemlich gefährliches Tier. Es kann buchstäblich alles passieren - Ihr Compiler könnte die gesamte Funktion einschließlich sichtbarer Nebenwirkungen optimieren. Es könnte Nasendämonen hervorbringen.
1 Zeiger auf zwei verschiedene Arrays, die Teil derselben Struktur sind, können verglichen werden, da dies unter die Klausel fällt, in der die beiden Arrays Teil desselben Aggregatobjekts (der Struktur) sind.
quelle
t
undx
in der gleichen Funktion definiert ist, gibt es null Grund , etwas darüber , wie ein Compiler Targeting x86-64 Einheimischer für diese Funktion in dem Stapelrahmen wird das Layout zu übernehmen. Der nach unten wachsende Stapel hat nichts mit der Deklarationsreihenfolge von Variablen in einer Funktion zu tun. Selbst in getrennten Funktionen könnten sich die Einheimischen der "Kind" -Funktion mit den Eltern vermischen, wenn sich eines in das andere einfügen könnte.void
) tun g ++ und clang ++ dies in der Praxis wirklich: godbolt.org/z/g5vesB sie Nehmen Sie an, dass der Ausführungspfad nicht genommen wird, weil er zu UB führt, und kompilieren Sie solche Basisblöcke zu einer unzulässigen Anweisung. Oder zu keinerlei Anweisungen, nur stillschweigend zu dem nächsten Asm durchzufallen, falls diese Funktion jemals aufgerufen wurde. (Aus irgendeinem Grundgcc
nicht nurg++
).Diese Fragen reduzieren sich auf:
Und die Antwort auf alle drei lautet "Implementierung definiert". Die Fragen Ihres Profis sind falsch. Sie haben es in traditionellem Unix-Layout basiert:
Einige moderne Einheiten (und alternative Systeme) entsprechen jedoch nicht diesen Traditionen. Es sei denn, sie haben der Frage "ab 1992" vorangestellt; Stellen Sie sicher, dass Sie bei der Bewertung eine -1 angeben.
quelle
arr[]
ein solches Objekt ist, der Standard einen Vergleich vorschreibt , derarr+32768
größer ist alsarr
selbst wenn ein Vergleich mit signierten Zeigern etwas anderes melden würde.Auf fast jeder fernmodernen Plattform haben Zeiger und Ganzzahlen eine isomorphe Ordnungsbeziehung, und Zeiger auf disjunkte Objekte werden nicht verschachtelt. Die meisten Compiler stellen diese Reihenfolge Programmierern zur Verfügung, wenn Optimierungen deaktiviert sind. Der Standard unterscheidet jedoch nicht zwischen Plattformen mit einer solchen Reihenfolge und solchen, die nicht erfordern , dass Implementierungen dem Programmierer eine solche Reihenfolge auch auf Plattformen offenlegen, die dies tun würden definiere es. Folglich führen einige Compiler-Writer verschiedene Arten von Optimierungen und "Optimierungen" durch, basierend auf der Annahme, dass Code niemals relationale Operatoren für Zeiger auf verschiedene Objekte vergleicht.
Gemäß der veröffentlichten Begründung beabsichtigten die Autoren des Standards, dass Implementierungen die Sprache erweitern, indem sie angeben, wie sie sich in Situationen verhalten, die der Standard als "undefiniertes Verhalten" charakterisiert (dh wenn der Standard keine Anforderungen stellt ), wenn dies nützlich und praktisch wäre Einige Compiler-Autoren gehen jedoch eher davon aus, dass Programme niemals versuchen werden, von etwas zu profitieren, das über die Standardmandate hinausgeht, als es Programmen zu ermöglichen, Verhaltensweisen, die die Plattformen unterstützen könnten, ohne zusätzliche Kosten sinnvoll auszunutzen.
Mir sind keine kommerziell entworfenen Compiler bekannt, die mit Zeigervergleichen etwas Seltsames anfangen, aber wenn Compiler für ihr Back-End auf das nichtkommerzielle LLVM umsteigen, verarbeiten sie zunehmend unsinnigen Code, dessen Verhalten zuvor angegeben wurde Compiler für ihre Plattformen. Ein solches Verhalten ist nicht auf relationale Operatoren beschränkt, sondern kann sogar die Gleichheit / Ungleichheit beeinflussen. Obwohl der Standard festlegt, dass ein Vergleich zwischen einem Zeiger auf ein Objekt und einem "gerade vergangenen" Zeiger auf ein unmittelbar vorhergehendes Objekt gleich ist, neigen gcc- und LLVM-basierte Compiler dazu, unsinnigen Code zu generieren, wenn Programme einen solchen ausführen Vergleiche.
Betrachten Sie als Beispiel für eine Situation, in der sich selbst ein Gleichheitsvergleich in gcc und clang unsinnig verhält, Folgendes:
Sowohl clang als auch gcc generieren Code, der immer 4 zurückgibt, selbst wenn
x
es sich um zehn Elemente handelt,y
unmittelbar darauf folgt undi
Null ist, was dazu führt, dass der Vergleich wahr ist undp[0]
mit dem Wert 1 geschrieben wird. Ich denke, was passiert, ist, dass ein Durchgang der Optimierung neu geschrieben wird die Funktion als ob*p = 1;
durch ersetzt worden wärex[10] = 1;
. Der letztere Code wäre äquivalent, wenn der Compiler*(x+10)
als äquivalent zu interpretiert würde*(y+i)
, aber leider erkennt eine nachgeschaltete Optimierungsstufe, dass ein Zugriff aufx[10]
nur definiert werden würde, wennx
mindestens 11 Elemente vorhanden wären, was es unmöglich machen würde, dass dieser Zugriff Auswirkungen haty
.Wenn Compiler dieses "Kreativ" mit Zeigergleichheitsszenario erhalten können, das vom Standard beschrieben wird, würde ich ihnen nicht vertrauen, dass sie nicht noch kreativer werden, wenn der Standard keine Anforderungen stellt.
quelle
Es ist ganz einfach: Der Vergleich von Zeigern ist nicht sinnvoll, da die Speicherorte für Objekte niemals in der Reihenfolge garantiert werden, in der Sie sie deklariert haben. Die Ausnahme sind Arrays. & array [0] ist niedriger als & array [1]. Darauf weist K & R hin. In der Praxis sind die Adressen der Strukturmitglieder auch in der Reihenfolge, in der Sie sie meiner Erfahrung nach deklarieren. Keine Garantie dafür .... Eine weitere Ausnahme ist, wenn Sie einen Zeiger für gleich vergleichen. Wenn ein Zeiger einem anderen entspricht, wissen Sie, dass er auf dasselbe Objekt zeigt. Was auch immer es ist. Schlechte Prüfungsfrage, wenn Sie mich fragen. Abhängig von Ubuntu Linux 16.04, 64-Bit-Version Programmierumgebung für eine Prüfungsfrage? Ja wirklich ?
quelle
arr[0]
,arr[1]
usw. getrennt. Sie deklarierenarr
als Ganzes, sodass die Reihenfolge der einzelnen Array-Elemente ein anderes Problem darstellt als in dieser Frage beschrieben.memcpy
einen zusammenhängenden Teil einer Struktur kopieren und alle darin enthaltenen Elemente und nichts anderes beeinflussen kann. Der Standard ist schlampig in Bezug auf die Terminologie, welche Arten von Zeigerarithmetik mit Strukturen odermalloc()
zugewiesenem Speicher durchgeführt werden können. Dasoffsetof
Makro wäre ziemlich nutzlos, wenn man nicht die gleiche Art von Zeigerarithmetik mit den Bytes einer Struktur wie mit a verwenden könntechar[]
, aber der Standard sagt nicht ausdrücklich, dass die Bytes einer Struktur sind (oder verwendet werden können als) ein Array-Objekt.Was für eine provokative Frage!
Selbst das flüchtige Scannen der Antworten und Kommentare in diesem Thread zeigt, wie emotional Ihre scheinbar einfache und unkomplizierte Abfrage ist.
Es sollte nicht überraschen.
Inarguably, Missverständnisse rund um das Konzept und die Verwendung von Zeigern stellen eine vorherrschende Ursache von schweren Fehlern in allgemein Programmierung.
Das Erkennen dieser Realität zeigt sich leicht in der Allgegenwart von Sprachen, die speziell dafür entwickelt wurden, die Herausforderungen, die Zeiger insgesamt mit sich bringen , anzugehen und vorzugsweise zu vermeiden . Denken Sie an C ++ und andere Ableitungen von C, Java und seinen Beziehungen, Python und anderen Skripten - lediglich als die bekannteren und am weitesten verbreiteten und mehr oder weniger geordneten Schweregrade bei der Behandlung des Problems.
Die Entwicklung eines tieferen Verständnisses der zugrunde liegenden Prinzipien muss daher für jeden Einzelnen relevant sein , der eine hervorragende Programmierung anstrebt - insbesondere auf Systemebene .
Ich stelle mir vor, genau das will Ihr Lehrer demonstrieren.
Und die Natur von C macht es zu einem bequemen Fahrzeug für diese Erkundung. Weniger klar als Assemblierung - obwohl vielleicht leichter verständlich - und dennoch weitaus expliziter als Sprachen, die auf einer tieferen Abstraktion der Ausführungsumgebung basieren.
C ist eine Sprache auf Systemebene , die die deterministische Übersetzung der Absicht des Programmierers in Anweisungen erleichtert , die Maschinen verstehen können . Obwohl es als hochrangig eingestuft ist, gehört es tatsächlich zu einer „mittleren“ Kategorie. Da es jedoch keine solche gibt, muss die Bezeichnung "System" ausreichen.
Diese Eigenschaft ist maßgeblich dafür verantwortlich, dass es eine bevorzugte Sprache für Gerätetreiber , Betriebssystemcode und eingebettete Implementierungen ist. Darüber hinaus eine zu Recht bevorzugte Alternative bei Anwendungen, bei denen eine optimale Effizienz von größter Bedeutung ist. wo das den Unterschied zwischen Überleben und Aussterben bedeutet und daher eine Notwendigkeit im Gegensatz zu einem Luxus ist. In solchen Fällen verliert der attraktive Komfort der Portabilität seinen Reiz, und die Entscheidung für die mangelhafte Leistung des kleinsten gemeinsamen Nenners wird zu einer undenkbar nachteiligen Option.
Was macht C - und einige seiner Derivate - ganz speziell ist, dass es erlaubt seinen Benutzern vollständige Kontrolle - wenn das ist , was sie sich wünschen - ohne Auferlegung der damit verbundenen Aufgaben auf sie , wenn sie es nicht tun. Dennoch bietet es nie mehr als die dünnste von Isolierungen aus der Maschine , weshalb die ordnungsgemäße Verwendung erfordert anspruchsvolles Verständnis des Begriffs des Zeigers .
Im Wesentlichen ist die Antwort auf Ihre Frage sehr einfach und befriedigend süß - zur Bestätigung Ihres Verdachts. Vorausgesetzt jedoch, man misst jedem Konzept in dieser Aussage die erforderliche Bedeutung bei :
Ersteres ist sowohl stets sichere und potentiell richtige , während die letzteren kann je nur sein richtige , wenn es wurde festgelegt als sicher . Überraschenderweise hängt die Feststellung der Gültigkeit des letzteren von einigen ab und verlangt dies .
Ein Teil der Verwirrung ergibt sich natürlich aus der Auswirkung der Rekursion, die dem Prinzip eines Zeigers innewohnt - und den Herausforderungen bei der Unterscheidung von Inhalten und Adressen.
Sie haben ganz richtig vermutet,
Und mehrere Mitwirkende haben bestätigt: Zeiger sind nur Zahlen. Manchmal etwas näher an komplexen Zahlen, aber immer noch nicht mehr als Zahlen.
Die amüsante Schärfe, in der diese Behauptung hier aufgenommen wurde, offenbart mehr über die menschliche Natur als über die Programmierung, bleibt jedoch bemerkenswert und ausführlich. Vielleicht machen wir das später ...
Wie ein Kommentar andeutet; All diese Verwirrung und Bestürzung ergibt sich aus der Notwendigkeit, zu unterscheiden, was gültig ist und was sicher ist , aber das ist eine übermäßige Vereinfachung. Wir müssen auch unterscheiden, was funktional und was zuverlässig ist , was praktisch ist und was richtig sein kann und noch weiter: was unter bestimmten Umständen richtig ist und was im allgemeineren Sinne richtig sein kann . Ganz zu schweigen von; der Unterschied zwischen Konformität und Anstand .
Zu diesem Zweck müssen wir zunächst genau wissen , was ein Zeiger ist .
Wie mehrere darauf hingewiesen haben: Der Begriff Zeiger ist lediglich ein spezieller Name für einen Index und somit nichts weiter als eine andere Zahl .
Dies sollte angesichts der Tatsache, dass alle modernen Mainstream-Computer Binärmaschinen sind , die notwendigerweise ausschließlich mit und auf Zahlen arbeiten, bereits selbstverständlich sein . Quantum Computing mag das ändern, aber das ist höchst unwahrscheinlich und nicht erwachsen geworden .
Wie Sie bereits bemerkt haben, sind Zeiger technisch gesehen genauere Adressen . Eine offensichtliche Einsicht, die natürlich die lohnende Analogie einführt, sie mit den „Adressen“ von Häusern oder Grundstücken auf einer Straße zu korrelieren.
In einem flachen Speichermodell: Der gesamte Systemspeicher ist in einer einzigen linearen Reihenfolge organisiert: Alle Häuser in der Stadt liegen auf derselben Straße, und jedes Haus wird allein durch seine Nummer eindeutig identifiziert. Herrlich einfach.
In segmentierten Schemata wird eine hierarchische Organisation von nummerierten Straßen über der von nummerierten Häusern eingeführt, so dass zusammengesetzte Adressen erforderlich sind.
Bringen Sie uns zu der weiteren Wendung , die das Rätsel in ein so faszinierend kompliziertes Gewirr verwandelt . Oben war es zweckmäßig, der Einfachheit und Klarheit halber vorzuschlagen, dass Zeiger Adressen sind . Das ist natürlich nicht richtig. Ein Zeiger ist keine Adresse; Ein Zeiger ist eine Referenz auf eine Adresse , er enthält eine Adresse . Wie der Umschlag trägt ein Hinweis auf das Haus. Wenn Sie darüber nachdenken, können Sie einen Blick darauf werfen, was mit dem im Konzept enthaltenen Vorschlag der Rekursion gemeint war. Immer noch; Wir haben nur so viele Wörter und sprechen über die Adressen von Verweisen auf Adressenund so blockiert bald die meisten Gehirne bei einer ungültigen Op-Code-Ausnahme . Und zum größten Teil wird die Absicht leicht aus dem Kontext gewonnen, also kehren wir auf die Straße zurück.
Postangestellte in unserer imaginären Stadt ähneln denen, die wir in der "realen" Welt finden. Es ist wahrscheinlich, dass niemand einen Schlaganfall erleidet, wenn Sie über eine ungültige Adresse sprechen oder sich erkundigen , aber jeder letzte wird zurückschrecken, wenn Sie ihn bitten , auf diese Informationen zu reagieren .
Angenommen, es gibt nur 20 Häuser in unserer einzigartigen Straße. Stellen Sie sich weiter vor, eine fehlgeleitete oder legasthene Seele habe einen sehr wichtigen Brief an Nummer 71 gerichtet. Jetzt können wir unseren Spediteur Frank fragen, ob es eine solche Adresse gibt, und er wird einfach und ruhig berichten: Nein . Wir können auch erwarten , dass er , wie weit außerhalb der Straße schätzen diese Stelle würde lügen , wenn es tat exist: etwa 2,5 - mal weiter als das Ende. Nichts davon wird ihn ärgern. Allerdings , wenn wir ihn fragen würden , zu liefern , diesen Brief, oder holen von diesem Ort ein Element, ist er wahrscheinlich ganz offen über seine sein Unmut und Ablehnung zu erfüllen.
Zeiger sind nur Adressen und Adressen sind nur Zahlen.
Überprüfen Sie die Ausgabe von Folgendem:
Rufen Sie so viele Zeiger auf, wie Sie möchten, gültig oder nicht. Bitte veröffentlichen Sie Ihre Ergebnisse, wenn dies auf Ihrer Plattform fehlschlägt oder Ihr (zeitgemäßer) Compiler sich beschwert.
Nun, da Zeiger sind nur Zahlen, ist es zwangsläufig gültig , sie zu vergleichen. In gewisser Hinsicht ist es genau das, was Ihr Lehrer demonstriert. Alle folgenden Aussagen sind absolut gültig - und richtig! - C und wird beim Kompilieren ohne Probleme ausgeführt , obwohl keiner der Zeiger initialisiert werden muss und die darin enthaltenen Werte möglicherweise undefiniert sind :
result
explizit und drucken es aus, um den Compiler zu zwingen, den ansonsten redundanten, toten Code zu berechnen.Natürlich ist das Programm schlecht geformt, wenn entweder a oder b zum Zeitpunkt des Tests undefiniert (sprich: nicht richtig initialisiert ) sind, aber das ist für diesen Teil unserer Diskussion völlig irrelevant . Diese Schnipsel, wie auch die folgenden Aussagen sind garantiert - von der ‚Standard‘ - kompilieren und laufen einwandfrei, trotz der IN -validity jeder Zeiger beteiligt.
Probleme treten nur auf, wenn ein ungültiger Zeiger dereferenziert wird . Wenn wir Frank bitten, an der ungültigen, nicht vorhandenen Adresse abzuholen oder zu liefern.
Bei einem beliebigen Zeiger:
Während diese Anweisung kompiliert und ausgeführt werden muss:
... wie muss das:
... die folgenden beiden werden im krassen Gegensatz dazu immer noch leicht kompiliert, schlagen jedoch bei der Ausführung fehl, es sei denn, der Zeiger ist gültig - womit wir hier lediglich meinen, dass er auf eine Adresse verweist, auf die der vorliegenden Anmeldung Zugriff gewährt wurde :
Wie subtil die Veränderung? Die Unterscheidung liegt in der Differenz zwischen dem Wert des Zeigers - das ist die Adresse - und dem Wert des Inhalts: des Hauses unter dieser Nummer. Es tritt kein Problem auf, bis der Zeiger dereferenziert wird . bis versucht wird, auf die Adresse zuzugreifen, mit der es verknüpft ist. Beim Versuch, das Paket über den Straßenabschnitt hinaus zu liefern oder abzuholen ...
Im weiteren Sinne gilt das gleiche Prinzip notwendigerweise für komplexere Beispiele, einschließlich der oben genannten Notwendigkeit , die erforderliche Gültigkeit festzustellen :
Relationaler Vergleich und Arithmetik bieten den gleichen Nutzen wie das Testen der Äquivalenz und sind im Prinzip gleichwertig. Allerdings , was die Ergebnisse dieser Berechnung würde bedeuten , ist eine andere Sache ganz - und genau das Problem behoben , indem die Notierungen Sie enthalten.
In C ist ein Array ein zusammenhängender Puffer, eine ununterbrochene lineare Reihe von Speicherstellen. Vergleich und Arithmetik, die auf Zeiger angewendet werden, deren Referenzorte innerhalb einer solchen singulären Reihe natürlich und offensichtlich sowohl in Bezug aufeinander als auch auf dieses 'Array' (das einfach durch die Basis identifiziert wird) von Bedeutung sind. Genau das Gleiche gilt für jeden Block, der durch
malloc
oder zugewiesen wirdsbrk
. Da diese Beziehungen implizit sind , kann der Compiler gültige Beziehungen zwischen ihnen herstellen und daher sicher sein, dass Berechnungen die erwarteten Antworten liefern.Eine ähnliche Gymnastik mit Zeigern durchzuführen, die auf bestimmte Blöcke oder Arrays verweisen, bietet keinen solchen inhärenten und offensichtlichen Nutzen. Dies gilt umso mehr, als jede Beziehung, die zu einem bestimmten Zeitpunkt besteht, durch eine nachfolgende Neuzuweisung ungültig werden kann, bei der sich diese höchstwahrscheinlich ändern oder sogar invertiert werden. In solchen Fällen kann der Compiler nicht die erforderlichen Informationen abrufen, um das Vertrauen herzustellen, das er in der vorherigen Situation hatte.
Sie als Programmierer können jedoch über solche Kenntnisse verfügen! Und in einigen Fällen sind sie verpflichtet, dies auszunutzen.
Es IST daher Umstände , unter denen selbst diese ganz ist VALID und vollkommen richtig.
In der Tat ist, dass genau das, was
malloc
selbst hat intern zu tun , wenn die Zeit zurückgewonnen Blöcke versuchen kommt Verschmelzung - auf der großen Mehrheit der Architekturen. Gleiches gilt für den Betriebssystem-Allokator, wie er dahinter stehtsbrk
. wenn offensichtlicher , häufig , auf unterschiedlicheren Einheiten, kritischer - und relevant auch auf Plattformen, auf denen diesmalloc
möglicherweise nicht der Fall ist. Und wie viele davon sind nicht in C geschrieben?Die Gültigkeit, Sicherheit und der Erfolg einer Handlung sind unweigerlich die Folge des Einsichtsniveaus, auf dem sie beruht und angewendet wird.
In den von Ihnen angebotenen Zitaten sprechen Kernighan und Ritchie ein eng verwandtes, aber dennoch getrenntes Problem an. Sie definieren die Einschränkungen der Sprache und erläutern, wie Sie die Funktionen des Compilers nutzen können, um Sie zu schützen, indem Sie zumindest potenziell fehlerhafte Konstrukte erkennen. Sie beschreiben die Längen der Mechanismus in der Lage ist - ausgelegt ist - zu gehen , um Sie in Ihrer Programmieraufgabe zu unterstützen. Der Compiler ist dein Diener, du bist der Meister. Ein weiser Meister ist jedoch einer, der mit den Fähigkeiten seiner verschiedenen Diener bestens vertraut ist.
In diesem Zusammenhang dient undefiniertes Verhalten dazu, auf eine potenzielle Gefahr und die Möglichkeit eines Schadens hinzuweisen. nicht das bevorstehende, irreversible Schicksal oder das Ende der Welt, wie wir sie kennen, zu implizieren. Es bedeutet einfach, dass wir - was den Compiler bedeutet - keine Vermutungen darüber anstellen können, was dieses Ding sein oder darstellen könnte und aus diesem Grund entscheiden wir uns, unsere Hände von der Sache zu waschen. Wir werden nicht für Missgeschicke verantwortlich gemacht, die sich aus der Nutzung oder dem Missbrauch dieser Einrichtung ergeben können .
Tatsächlich heißt es einfach: „Über diesen Punkt hinaus, Cowboy : Sie sind auf sich allein gestellt ..."
Ihr Professor möchte Ihnen die feineren Nuancen demonstrieren .
Beachten Sie, wie sorgfältig sie ihr Beispiel ausgearbeitet haben. und wie spröde es noch ist ist. Indem Sie die Adresse von
a
, inDer Compiler wird gezwungen, den tatsächlichen Speicher für die Variable zuzuweisen, anstatt ihn in ein Register zu stellen. Da es sich um eine automatische Variable handelt, hat der Programmierer jedoch keine Kontrolle darüber, wo diese zugewiesen ist, und kann daher keine gültigen Vermutungen darüber anstellen, was darauf folgen würde. Aus diesem Grund
a
muss der Wert auf Null gesetzt werden, damit der Code wie erwartet funktioniert.Nur diese Zeile ändern:
dazu:
bewirkt, dass das Verhalten des Programms undefiniert wird . Zumindest ist die erste Antwort jetzt 1; aber das Problem ist weitaus unheimlicher.
Jetzt lädt der Code zur Katastrophe ein.
Obwohl es immer noch vollkommen gültig ist und sogar dem Standard entspricht , ist es jetzt schlecht geformt und kann, obwohl es sicher kompiliert werden kann, aus verschiedenen Gründen fehlschlagen. Denn jetzt gibt es mehrere Probleme - keine von denen der Compiler ist die Lage , zu erkennen.
strcpy
beginnt an der Adresse vona
und geht darüber hinaus, um Byte für Byte zu verbrauchen und zu übertragen, bis eine Null auftritt.Der
p1
Zeiger wurde auf einen Block von genau 10 Bytes initialisiert .Wenn es
a
zufällig am Ende eines Blocks platziert wird und der Prozess keinen Zugriff auf das Folgende hat, löst der nächste Lesevorgang - von p0 [1] - einen Segfault aus. Dieses Szenario ist auf der x86-Architektur unwahrscheinlich , aber möglich.Wenn das Gebiet jenseits der Adresse
a
ist zugänglich, wird kein Lesefehler auftreten, aber das Programm noch nicht vor Unglück gerettet.Wenn ein Null - Byte geschieht innerhalb der zehn an der Adresse des Startens auftreten
a
, es kann noch überleben dannstrcpy
aufhören wird und zumindest werden wir nicht in eine Schreib Verletzung leiden.Wenn es nicht fehlerhaft ist , falsch zu lesen, aber in dieser Zeitspanne von 10 kein Null-Byte auftritt,
strcpy
wird fortgesetzt und versucht , über den durch zugewiesenen Block hinaus zu schreibenmalloc
.Wenn dieser Bereich nicht dem Prozess gehört, sollte der Segfault sofort ausgelöst werden.
Die noch katastrophal - und subtile --- Situation entsteht , wenn der folgende Block wird durch das Verfahren im Besitz, denn dann der Fehler nicht erkannt wird, kann kein Signal angehoben werden, und so kann es zu ‚Arbeit‘ ‚erscheint‘ noch , Während andere Daten, die Verwaltungsstrukturen Ihres Allokators oder sogar Code (in bestimmten Betriebsumgebungen) tatsächlich überschrieben werden .
Aus diesem Grund können zeigerbezogene Fehler so schwer zu verfolgen sein . Stellen Sie sich diese Zeilen vor, die tief in Tausenden von Zeilen kompliziert verwandten Codes vergraben sind, den jemand anderes geschrieben hat, und Sie werden angewiesen, sich damit zu beschäftigen.
Dennoch , das Programm muss noch kompilieren, denn es bleibt vollkommen gültig und Standard - konforme C.
Diese Art von Fehlern, kein Standard und kein Compiler können die Unvorsichtigen davor schützen. Ich stelle mir vor, genau das wollen sie dir beibringen.
Paranoide Menschen versuchen ständig, die Natur von C zu ändern , um diese problematischen Möglichkeiten zu beseitigen und uns so vor uns selbst zu retten. aber das ist unaufrichtig . Dies ist die Verantwortung, die wir übernehmen müssen , wenn wir uns dafür entscheiden, die Macht zu verfolgen und die Freiheit zu erlangen, die uns eine direktere und umfassendere Steuerung der Maschine bietet. Promotoren und Verfolger von Perfektion in der Leistung werden niemals weniger akzeptieren.
Die Portabilität und die Allgemeinheit, die sie darstellt, sind eine grundsätzlich getrennte Überlegung und alles , was der Standard ansprechen möchte:
Deshalb ist es völlig in Ordnung ist , es zu halten verschieden von der Definition und technischen Spezifikation der Sprache selbst. Im Gegensatz zuwas viele zu glauben scheinen Allgemeinheit ist gegensätzlich zu außergewöhnlichen und beispielhaft .
Schlussfolgern:
Wäre dies nicht wahr, wäre eine Programmierung, wie wir sie kennen - und lieben - nicht möglich gewesen.
quelle
3.4.3
Dies ist auch ein Abschnitt, den Sie sich ansehen sollten: Er definiert UB als Verhalten, "für das diese Internationale Norm keine Anforderungen stellt".C11 6.5.6/9
, wobei zu beachten ist, dass das Wort "soll" eine Anforderung anzeigt. L "Wenn zwei Zeiger subtrahiert werden, zeigen beide auf Elemente desselben Array-Objekts oder einen nach dem letzten Element des Array-Objekts ".Zeiger sind nur ganze Zahlen, wie alles andere in einem Computer. Sie können sie absolut mit vergleichen
<
und>
und Ergebnisse erzielen, ohne dass ein Programm abstürzt. Der Standard garantiert jedoch nicht, dass diese Ergebnisse außerhalb von Array-Vergleichen eine Bedeutung haben .In Ihrem Beispiel für stapelzugewiesene Variablen kann der Compiler diese Variablen Registern oder Stapelspeicheradressen zuweisen und in beliebiger Reihenfolge auswählen. Vergleiche wie
<
und werden>
daher nicht über Compiler oder Architekturen hinweg konsistent sein. Allerdings==
und!=
sind nicht so beschränkt, Zeiger Vergleich Gleichheit ist ein gültiger und nützlicher Betrieb.quelle
int x[10],y[10],*p;
, wenn Code ausgewertety[0]
, dann ausgewertetp>(x+5)
und geschrieben,*p
ohnep
in der Zwischenzeity[0]
(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')
stattdessen verwenden,isalpha()
weil bei welcher vernünftigen Implementierung diese Charaktere diskontinuierlich wären? Das Fazit ist, dass Sie, selbst wenn keine Implementierung, von der Sie wissen, ein Problem hat, so weit wie möglich nach dem Standard codieren sollten, wenn Sie Wert auf Portabilität legen. Ich schätze das Label "Standards Maven", danke dafür. Ich kann in meinen Lebenslauf