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 :
- Das Untersuchen, Vergleichen und Manipulieren von Zeigern ist immer und notwendigerweise gültig, während die aus dem Ergebnis abgeleiteten Schlussfolgerungen von der Gültigkeit der enthaltenen Werte abhängen und daher nicht gültig sein müssen.
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,
Ich werde zu dem Gedanken gebracht, 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.
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 .
- Sie haben das Konzept fest im Griff und mögen diese Illustrationen wie einige andere als herablassend simpel empfinden, aber das hier erkennbare Maß an Verwirrung erfordert eine solche Einfachheit bei der Klärung.
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.
- Einige Implementierungen sind noch komplizierter, und die Gesamtheit der verschiedenen "Straßen" muss sich nicht zu einer zusammenhängenden Sequenz summieren, aber nichts davon ändert etwas am Basiswert.
- Wir sind notwendigerweise in der Lage, jede solche hierarchische Verknüpfung wieder in eine flache Organisation zu zerlegen. Je komplexer die Organisation ist, desto mehr Reifen müssen wir durchspringen, um dies zu tun, aber es muss möglich sein. Dies gilt in der Tat auch für den "Real-Modus" auf x86.
- Ansonsten ist die Zuordnung von Links zu anderen Adressen wäre nicht bijektiv , als zuverlässige Ausführung - auf Systemebene - Anforderungen , dass es muss sein.
- Mehrere Adressen dürfen nicht einzelnen Speicherorten zugeordnet werden
- Singularadressen dürfen niemals mehreren Speicherorten zugeordnet werden.
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:
void foo( void *p ) {
printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}
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 :
- Wir berechnen nur aus Gründen der Klarheit
result
explizit und drucken es aus, um den Compiler zu zwingen, den ansonsten redundanten, toten Code zu berechnen.
void foo( size_t *a, size_t *b ) {
size_t result;
result = (size_t)a;
printf(“%zu\n”, result);
result = a == b;
printf(“%zu\n”, result);
result = a < b;
printf(“%zu\n”, result);
result = a - b;
printf(“%zu\n”, result);
}
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:
int *p;
Während diese Anweisung kompiliert und ausgeführt werden muss:
printf(“%p”, p);
... wie muss das:
size_t foo( int *p ) { return (size_t)p; }
... 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 :
printf(“%p”, *p);
size_t foo( int *p ) { return *p; }
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 :
int* validate( int *p, int *head, int *tail ) {
return p >= head && p <= tail ? p : NULL;
}
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 wird sbrk
. 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 steht sbrk
. wenn offensichtlicher , häufig , auf unterschiedlicheren Einheiten, kritischer - und relevant auch auf Plattformen, auf denen dies malloc
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
, in
p[0].p0 = &a;
Der 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:
char a = 0;
dazu:
char a = 1; // or ANY other value than 0
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 von a
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 dann strcpy
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:
Dieses Dokument legt die Form und stellt die Interpretation von Programmen in der Programmiersprache C. Sein ausgedrückt Zweck ist auf Portabilität zu fördern , Zuverlässigkeit, Wartbarkeit und effiziente Ausführung von C - Sprachprogramme auf einer Vielzahl von Rechensystemen .
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:
- Das Untersuchen und Manipulieren von Zeigern selbst ist ausnahmslos gültig und oft fruchtbar . Die Interpretation der Ergebnisse kann sinnvoll sein oder auch nicht, aber Unglück wird niemals eingeladen, bis der Zeiger dereferenziert wird . bis versucht wird, auf die mit verknüpfte Adresse zuzugreifen .
Wäre dies nicht wahr, wäre eine Programmierung, wie wir sie kennen - und lieben - nicht möglich gewesen.
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.