Warum sind Zeiger für viele neue und sogar alte College-Studenten in C oder C ++ so ein Hauptverwirrungsfaktor? Gibt es Werkzeuge oder Denkprozesse, mit denen Sie verstehen können, wie Zeiger auf der Ebene von Variablen, Funktionen und darüber hinaus funktionieren?
Was sind einige gute Praktiken, die getan werden können, um jemanden auf das Niveau von "Ah-hah, ich habe es" zu bringen, ohne dass er im Gesamtkonzept festsitzt? Grundsätzlich bohren Sie ähnliche Szenarien.
Antworten:
Zeiger ist ein Konzept, das für viele zunächst verwirrend sein kann, insbesondere wenn es darum geht, Zeigerwerte zu kopieren und dennoch auf denselben Speicherblock zu verweisen.
Ich habe festgestellt, dass die beste Analogie darin besteht, den Zeiger als ein Stück Papier mit einer Hausadresse und dem Speicherblock, auf den er verweist, als das eigentliche Haus zu betrachten. Alle Arten von Operationen können somit leicht erklärt werden.
Ich habe unten einen Delphi-Code und gegebenenfalls einige Kommentare hinzugefügt. Ich habe mich für Delphi entschieden, da meine andere Hauptprogrammiersprache, C #, Dinge wie Speicherlecks nicht auf die gleiche Weise aufweist.
Wenn Sie nur das allgemeine Konzept der Zeiger lernen möchten, sollten Sie die Teile mit der Bezeichnung "Speicherlayout" in der folgenden Erläuterung ignorieren. Sie sollen Beispiele dafür geben, wie der Speicher nach Operationen aussehen könnte, sind jedoch von geringerer Natur. Um jedoch genau zu erklären, wie Pufferüberläufe wirklich funktionieren, war es wichtig, dass ich diese Diagramme hinzufügte.
Haftungsausschluss: Diese Erklärung und die beispielhaften Speicherlayouts sind in jeder Hinsicht erheblich vereinfacht. Es gibt mehr Overhead und viel mehr Details, die Sie wissen müssen, wenn Sie mit Speicher auf niedriger Ebene umgehen müssen. Für die Erklärung von Speicher und Zeigern ist es jedoch genau genug.
Nehmen wir an, die unten verwendete THouse-Klasse sieht folgendermaßen aus:
Wenn Sie das Hausobjekt initialisieren, wird der dem Konstruktor gegebene Name in das private Feld FName kopiert. Es gibt einen Grund, warum es als Array mit fester Größe definiert ist.
Im Speicher wird mit der Hauszuweisung ein gewisser Aufwand verbunden sein. Ich werde dies im Folgenden folgendermaßen veranschaulichen:
Der "tttt" -Bereich ist Overhead, normalerweise gibt es mehr davon für verschiedene Arten von Laufzeiten und Sprachen, wie 8 oder 12 Bytes. Es ist unbedingt erforderlich, dass die in diesem Bereich gespeicherten Werte niemals durch etwas anderes als den Speicherzuweiser oder die Routinen des Kernsystems geändert werden. Andernfalls besteht die Gefahr, dass das Programm abstürzt.
Speicher zuweisen
Lassen Sie Ihr Haus von einem Unternehmer bauen und geben Sie die Adresse für das Haus an. Im Gegensatz zur realen Welt kann der Speicherzuweisung nicht mitgeteilt werden, wo sie zugewiesen werden soll, sondern es wird ein geeigneter Platz mit genügend Platz gefunden und die Adresse an den zugewiesenen Speicher zurückgemeldet.
Mit anderen Worten, der Unternehmer wählt den Ort.
Speicherlayout:
Behalten Sie eine Variable mit der Adresse
Schreiben Sie die Adresse Ihres neuen Hauses auf ein Blatt Papier. Dieses Papier dient als Referenz für Ihr Haus. Ohne dieses Stück Papier sind Sie verloren und können das Haus nicht finden, es sei denn, Sie sind bereits darin.
Speicherlayout:
Zeigerwert kopieren
Schreiben Sie einfach die Adresse auf ein neues Blatt Papier. Sie haben jetzt zwei Zettel, mit denen Sie zum selben Haus gelangen, nicht zu zwei separaten Häusern. Alle Versuche, der Adresse eines Papiers zu folgen und die Möbel in diesem Haus neu anzuordnen, lassen den Eindruck entstehen, dass das andere Haus auf die gleiche Weise geändert wurde, es sei denn, Sie können ausdrücklich feststellen, dass es sich tatsächlich nur um ein Haus handelt.
Hinweis Dies ist normalerweise das Konzept, das ich den Menschen am meisten erklären kann. Zwei Zeiger bedeuten nicht zwei Objekte oder Speicherblöcke.
Speicher freigeben
Zerstöre das Haus. Sie können das Papier später später für eine neue Adresse wiederverwenden, wenn Sie dies wünschen, oder es löschen, um die Adresse des Hauses zu vergessen, die nicht mehr existiert.
Hier baue ich zuerst das Haus und erhalte seine Adresse. Dann mache ich etwas mit dem Haus (benutze es, den ... Code, der als Übung für den Leser übrig bleibt) und dann befreie ich es. Zuletzt lösche ich die Adresse aus meiner Variablen.
Speicherlayout:
Baumelnde Zeiger
Sie fordern Ihren Unternehmer auf, das Haus zu zerstören, vergessen jedoch, die Adresse von Ihrem Blatt Papier zu löschen. Wenn Sie später auf das Blatt Papier schauen, haben Sie vergessen, dass das Haus nicht mehr da ist, und besuchen es mit fehlgeschlagenen Ergebnissen (siehe auch den Teil über eine ungültige Referenz unten).
Die Verwendung
h
nach dem Anruf.Free
könnte funktionieren, aber das ist nur reines Glück. Höchstwahrscheinlich wird es bei einem Kunden mitten in einem kritischen Vorgang fehlschlagen.Wie Sie sehen können, zeigt h immer noch auf die Überreste der Daten im Speicher. Da diese jedoch möglicherweise nicht vollständig sind, schlägt die Verwendung wie zuvor möglicherweise fehl.
Speicherleck
Sie verlieren das Stück Papier und können das Haus nicht finden. Das Haus steht jedoch immer noch irgendwo und wenn Sie später ein neues Haus bauen möchten, können Sie diesen Ort nicht wiederverwenden.
Hier haben wir den Inhalt der
h
Variablen mit der Adresse eines neuen Hauses überschrieben , aber das alte steht noch ... irgendwo. Nach diesem Code gibt es keine Möglichkeit, dieses Haus zu erreichen, und es bleibt stehen. Mit anderen Worten, der zugewiesene Speicher bleibt zugewiesen, bis die Anwendung geschlossen wird. Zu diesem Zeitpunkt wird er vom Betriebssystem heruntergefahren.Speicherlayout nach der ersten Zuweisung:
Speicherlayout nach zweiter Zuordnung:
Ein häufigerer Weg, um diese Methode zu erhalten, besteht darin, zu vergessen, etwas freizugeben, anstatt es wie oben zu überschreiben. In Delphi-Begriffen geschieht dies mit der folgenden Methode:
Nachdem diese Methode ausgeführt wurde, gibt es in unseren Variablen keinen Platz, an dem die Adresse des Hauses vorhanden ist, aber das Haus ist immer noch da draußen.
Speicherlayout:
Wie Sie sehen können, bleiben die alten Daten im Speicher erhalten und werden vom Speicherzuweiser nicht wiederverwendet. Der Allokator verfolgt, welche Speicherbereiche verwendet wurden, und verwendet sie erst wieder, wenn Sie sie freigeben.
Freigeben des Speichers, aber Beibehalten einer (jetzt ungültigen) Referenz
Zerstören Sie das Haus, löschen Sie eines der Zettel, aber Sie haben auch ein anderes Zettel mit der alten Adresse darauf. Wenn Sie zur Adresse gehen, werden Sie kein Haus finden, aber vielleicht finden Sie etwas, das den Ruinen ähnelt von einem.
Vielleicht finden Sie sogar ein Haus, aber es ist nicht das Haus, an das Sie ursprünglich die Adresse erhalten haben, und daher können alle Versuche, es so zu verwenden, als ob es Ihnen gehört, schrecklich scheitern.
Manchmal stellen Sie sogar fest, dass auf einer benachbarten Adresse ein ziemlich großes Haus eingerichtet ist, das drei Adressen belegt (Hauptstraße 1-3), und Ihre Adresse befindet sich in der Mitte des Hauses. Alle Versuche, diesen Teil des großen Hauses mit drei Adressen als ein einziges kleines Haus zu behandeln, könnten ebenfalls schrecklich scheitern.
Hier wurde das Haus durch die Referenz abgerissen
h1
, und obwohlh1
es ebenfalls geräumt wurde, hat esh2
immer noch die alte, veraltete Adresse. Der Zugang zu dem Haus, das nicht mehr steht, könnte funktionieren oder nicht.Dies ist eine Variation des oben baumelnden Zeigers. Siehe das Speicherlayout.
Pufferüberlauf
Sie bewegen mehr Sachen in das Haus, als Sie möglicherweise passen können, und verschütten in das Haus oder den Hof des Nachbarn. Wenn der Besitzer des Nachbarhauses später nach Hause kommt, findet er alle möglichen Dinge, die er für seine eigenen hält.
Aus diesem Grund habe ich mich für ein Array mit fester Größe entschieden. Um die Bühne zu bereiten, nehmen wir an, dass das zweite Haus, das wir zuweisen, aus irgendeinem Grund vor dem ersten im Speicher steht. Mit anderen Worten, das zweite Haus hat eine niedrigere Adresse als das erste. Außerdem werden sie direkt nebeneinander zugewiesen.
Also dieser Code:
Speicherlayout nach der ersten Zuweisung:
Speicherlayout nach zweiter Zuordnung:
Der Teil, der am häufigsten zum Absturz führt, ist das Überschreiben wichtiger Teile der von Ihnen gespeicherten Daten, die eigentlich nicht zufällig geändert werden sollten. Zum Beispiel ist es möglicherweise kein Problem, dass Teile des Namens des h1-Hauses geändert wurden, um das Programm zum Absturz zu bringen. Das Überschreiben des Overheads des Objekts stürzt jedoch höchstwahrscheinlich ab, wenn Sie versuchen, das beschädigte Objekt wie gewünscht zu verwenden Überschreiben von Links, die zu anderen Objekten im Objekt gespeichert sind.
Verknüpfte Listen
Wenn Sie einer Adresse auf einem Blatt Papier folgen, gelangen Sie zu einem Haus, und in diesem Haus befindet sich ein weiteres Blatt Papier mit einer neuen Adresse für das nächste Haus in der Kette und so weiter.
Hier stellen wir eine Verbindung von unserem Haus zu unserer Hütte her. Wir können der Kette folgen, bis ein Haus keinen
NextHouse
Bezug mehr hat, was bedeutet, dass es das letzte ist. Um alle unsere Häuser zu besuchen, können wir den folgenden Code verwenden:Speicherlayout (NextHouse als Link im Objekt hinzugefügt, vermerkt mit den vier LLLLs im folgenden Diagramm):
Was ist eine Speicheradresse?
Eine Speicheradresse ist im Grunde genommen nur eine Zahl. Wenn Sie sich Speicher als ein großes Array von Bytes vorstellen, hat das allererste Byte die Adresse 0, das nächste die Adresse 1 und so weiter. Dies ist vereinfacht, aber gut genug.
Also dieses Speicherlayout:
Könnte diese beiden Adressen haben (ganz links - ist Adresse 0):
Was bedeutet, dass unsere oben verlinkte Liste tatsächlich so aussehen könnte:
Es ist typisch, eine Adresse, die "nirgendwo hin zeigt", als Nulladresse zu speichern.
Was ist ein Zeiger?
Ein Zeiger ist nur eine Variable, die eine Speicheradresse enthält. Normalerweise können Sie die Programmiersprache bitten, Ihnen ihre Nummer zu geben, aber die meisten Programmiersprachen und Laufzeiten versuchen, die Tatsache zu verbergen, dass sich darunter eine Nummer befindet, nur weil die Nummer selbst für Sie keine wirkliche Bedeutung hat. Es ist am besten, sich einen Zeiger als Black Box vorzustellen, dh. Sie wissen oder kümmern sich nicht wirklich darum, wie es tatsächlich implementiert wird, solange es funktioniert.
quelle
In meiner ersten Comp Sci-Klasse haben wir die folgende Übung gemacht. Zugegeben, dies war ein Hörsaal mit ungefähr 200 Studenten ...
Professor schreibt an die Tafel:
int john;
John steht auf
Professor schreibt:
int *sally = &john;
Sally steht auf und zeigt auf John
Professor:
int *bill = sally;
Bill steht auf und zeigt auf John
Professor:
int sam;
Sam steht auf
Professor:
bill = &sam;
Bill zeigt jetzt auf Sam.
Ich denke du kommst auf die Idee. Ich denke, wir haben ungefähr eine Stunde damit verbracht, bis wir die Grundlagen der Zeigerzuweisung besprochen haben.
quelle
Eine Analogie, die ich zur Erklärung von Zeigern als hilfreich empfunden habe, sind Hyperlinks. Die meisten Menschen können verstehen, dass ein Link auf einer Webseite auf eine andere Seite im Internet verweist. Wenn Sie diesen Hyperlink kopieren und einfügen können, verweisen beide auf dieselbe ursprüngliche Webseite. Wenn Sie diese Originalseite bearbeiten und dann einem dieser Links (Zeiger) folgen, erhalten Sie diese neue aktualisierte Seite.
quelle
int *a = b
bei nicht zwei Kopien*b
).Der Grund, warum Zeiger so viele Menschen zu verwirren scheinen, ist, dass sie meist wenig oder gar keinen Hintergrund in der Computerarchitektur haben. Da viele keine Ahnung zu haben scheinen, wie Computer (die Maschine) tatsächlich implementiert sind, scheint die Arbeit in C / C ++ fremd.
Ein Drill besteht darin, sie zu bitten, eine einfache Bytecode-basierte virtuelle Maschine (in jeder von ihnen gewählten Sprache funktioniert Python hervorragend) mit einem Befehlssatz zu implementieren, der sich auf Zeigeroperationen (Laden, Speichern, direkte / indirekte Adressierung) konzentriert. Bitten Sie sie dann, einfache Programme für diesen Befehlssatz zu schreiben.
Alles, was etwas mehr als eine einfache Addition erfordert, wird Zeiger beinhalten und sie werden es sicher bekommen.
quelle
Das Konzept eines Platzhalters für einen Wert - Variablen - ist auf etwas abgebildet, das wir in der Schule gelernt haben - Algebra. Es gibt keine Parallele, die Sie zeichnen können, ohne zu verstehen, wie der Speicher in einem Computer physisch aufgebaut ist, und niemand denkt über solche Dinge nach, bis er sich mit Dingen auf niedriger Ebene befasst - auf der C / C ++ / Byte-Kommunikationsebene .
Adressfelder. Ich erinnere mich, als ich lernte, BASIC in Mikrocomputer zu programmieren, gab es diese hübschen Bücher mit Spielen, und manchmal musste man Werte in bestimmte Adressen stecken. Sie hatten ein Bild von einer Reihe von Boxen, die schrittweise mit 0, 1, 2 beschriftet waren ... und es wurde erklärt, dass nur eine kleine Sache (ein Byte) in diese Boxen passen konnte, und es gab viele von ihnen - einige Computer hatte so viele wie 65535! Sie standen nebeneinander und hatten alle eine Adresse.
Für eine Übung? Erstellen Sie eine Struktur:
Gleiches Beispiel wie oben, außer in C:
Ausgabe:
Vielleicht erklärt das einige der Grundlagen anhand eines Beispiels?
quelle
Der Grund, warum ich anfangs Schwierigkeiten hatte, Hinweise zu verstehen, ist, dass viele Erklärungen viel Mist über das Übergeben von Referenzen enthalten. Das alles verwirrt das Problem. Wenn Sie einen Zeigerparameter verwenden, übergeben Sie immer noch den Wert. aber der Wert ist eher eine Adresse als beispielsweise ein int.
Jemand anderes hat bereits auf dieses Tutorial verlinkt, aber ich kann den Moment hervorheben, in dem ich anfing, Hinweise zu verstehen:
Ein Tutorial zu Zeigern und Arrays in C: Kapitel 3 - Zeiger und Zeichenfolgen
In dem Moment, als ich diese Worte las, teilten sich die Wolken und ein Sonnenstrahl umhüllte mich mit Zeigerverständnis.
Selbst wenn Sie ein VB .NET- oder C # -Entwickler sind (wie ich) und niemals unsicheren Code verwenden, lohnt es sich dennoch zu verstehen, wie Zeiger funktionieren, oder Sie werden nicht verstehen, wie Objektreferenzen funktionieren. Dann haben Sie die verbreitete, aber falsche Vorstellung, dass das Übergeben einer Objektreferenz an eine Methode das Objekt kopiert.
quelle
Ich fand Ted Jensens "Tutorial zu Zeigern und Arrays in C" eine ausgezeichnete Quelle, um etwas über Zeiger zu lernen. Es ist in 10 Lektionen unterteilt, beginnend mit einer Erklärung, was Zeiger sind (und wofür sie sind) und endend mit Funktionszeigern. http://home.netcom.com/~tjensen/ptr/cpoint.htm
Von dort aus lehrt Beejs Leitfaden zur Netzwerkprogrammierung die Unix-Sockets-API, mit der Sie anfangen können, wirklich lustige Dinge zu tun. http://beej.us/guide/bgnet/
quelle
Die Komplexität von Zeigern geht über das hinaus, was wir leicht lehren können. Es ist ein großartiges Lernwerkzeug, wenn die Schüler aufeinander zeigen und Zettel mit Hausadressen verwenden. Sie machen einen großartigen Job bei der Einführung der Grundkonzepte. In der Tat ist das Erlernen der Grundkonzepte entscheidend für die erfolgreiche Verwendung von Zeigern. Im Produktionscode ist es jedoch üblich, in viel komplexere Szenarien zu geraten, als diese einfachen Demonstrationen zusammenfassen können.
Ich war an Systemen beteiligt, bei denen Strukturen auf andere Strukturen zeigten, die auf andere Strukturen zeigten. Einige dieser Strukturen enthielten auch eingebettete Strukturen (anstatt Zeiger auf zusätzliche Strukturen). Hier werden Zeiger wirklich verwirrend. Wenn Sie mehrere Indirektionsebenen haben und am Ende Code wie folgt erhalten:
Es kann sehr schnell verwirrend werden (stellen Sie sich viel mehr Zeilen und möglicherweise mehr Ebenen vor). Wenn Sie Zeigerarrays und Zeiger von Knoten zu Knoten (Bäume, verknüpfte Listen) einfügen, wird es noch schlimmer. Ich habe gesehen, dass einige wirklich gute Entwickler verloren gingen, als sie anfingen, an solchen Systemen zu arbeiten, selbst Entwickler, die die Grundlagen wirklich gut verstanden haben.
Komplexe Strukturen von Zeigern weisen auch nicht unbedingt auf eine schlechte Codierung hin (obwohl dies möglich ist). Komposition ist ein wesentliches Element einer guten objektorientierten Programmierung und führt in Sprachen mit rohen Zeigern unweigerlich zu einer mehrschichtigen Indirektion. Darüber hinaus müssen Systeme häufig Bibliotheken von Drittanbietern mit Strukturen verwenden, die in Stil oder Technik nicht zueinander passen. In solchen Situationen wird natürlich Komplexität entstehen (obwohl wir sie sicherlich so weit wie möglich bekämpfen sollten).
Ich denke, das Beste, was Colleges tun können, um Schülern beim Erlernen von Zeigern zu helfen, ist, gute Demonstrationen zu verwenden, kombiniert mit Projekten, die Zeigergebrauch erfordern. Ein schwieriges Projekt wird mehr zum Verständnis der Zeiger beitragen als tausend Demonstrationen. Demonstrationen können Ihnen ein flaches Verständnis vermitteln, aber um Hinweise tief zu erfassen, müssen Sie sie wirklich verwenden.
quelle
Ich dachte, ich würde dieser Liste eine Analogie hinzufügen, die ich als Informatik-Tutor sehr hilfreich fand, wenn ich Zeiger (damals) erklärte. Lassen Sie uns zuerst:
Stellen Sie die Bühne ein :
Stellen Sie sich einen Parkplatz mit 3 Stellplätzen vor. Diese Stellplätze sind nummeriert:
In gewisser Weise ist dies wie Speicherorte, sie sind sequentiell und zusammenhängend. Eine Art Array. Im Moment sind keine Autos in ihnen, also ist es wie ein leeres Array (
parking_lot[3] = {0}
).Fügen Sie die Daten hinzu
Ein Parkplatz bleibt nie lange leer ... wenn es so wäre, wäre es sinnlos und niemand würde einen bauen. Nehmen wir also an, der Tag füllt sich mit 3 Autos, einem blauen Auto, einem roten Auto und einem grünen Auto:
Diese Autos sind alle vom gleichen Typ (Auto) so eine Art und Weise daran zu denken ist , dass unsere Autos sind eine Art von Daten (etwa ein
int
) , aber sie haben unterschiedliche Werte (blue
,red
,green
, das eine Farbe sein könnteenum
)Geben Sie den Zeiger ein
Wenn ich Sie jetzt auf diesen Parkplatz bringe und Sie auffordere, mir ein blaues Auto zu suchen, strecken Sie einen Finger aus und zeigen damit auf ein blaues Auto in Punkt 1. Dies ist so, als würde man einen Zeiger nehmen und ihn einer Speicheradresse zuweisen (
int *finger = parking_lot
)Ihr Finger (der Zeiger) ist nicht die Antwort auf meine Frage. Ein Blick auf Ihren Finger sagt mir nichts, aber wenn ich schaue, wohin Ihr Finger zeigt (den Zeiger dereferenzieren), kann ich das Auto (die Daten) finden, nach dem ich gesucht habe.
Neuzuweisen des Zeigers
Jetzt kann ich Sie bitten, stattdessen ein rotes Auto zu finden, und Sie können Ihren Finger auf ein neues Auto umleiten. Jetzt zeigt mir Ihr Zeiger (derselbe wie zuvor) neue Daten (der Parkplatz, auf dem sich das rote Auto befindet) des gleichen Typs (das Auto).
Der Zeiger hat sich physisch nicht geändert, es ist immer noch Ihr Finger, nur die Daten, die er mir zeigte, haben sich geändert. (die "Parkplatz" -Adresse)
Doppelzeiger (oder ein Zeiger auf einen Zeiger)
Dies funktioniert auch mit mehr als einem Zeiger. Ich kann fragen, wo sich der Zeiger befindet, der auf das rote Auto zeigt, und Sie können Ihre andere Hand verwenden und mit einem Finger auf den ersten Finger zeigen. (das ist wie
int **finger_two = &finger
)Wenn ich nun wissen möchte, wo sich das blaue Auto befindet, kann ich der Richtung des ersten Fingers zum zweiten Finger, zum Auto (den Daten) folgen.
Der baumelnde Zeiger
Nehmen wir jetzt an, Sie fühlen sich sehr wie eine Statue und möchten Ihre Hand auf unbestimmte Zeit auf das rote Auto richten. Was ist, wenn das rote Auto wegfährt?
Ihr Zeiger zeigt immer noch auf die Stelle, an der sich das rote Auto befand , befindet sich aber nicht mehr. Nehmen wir an, dort fährt ein neues Auto ein ... ein orangefarbenes Auto. Wenn ich Sie jetzt noch einmal frage: "Wo ist das rote Auto?", Zeigen Sie immer noch dorthin, aber jetzt liegen Sie falsch. Das ist kein rotes Auto, das ist orange.
Zeigerarithmetik
Ok, Sie zeigen also immer noch auf den zweiten Parkplatz (der jetzt vom orangefarbenen Auto besetzt ist).
Nun, ich habe jetzt eine neue Frage ... Ich möchte die Farbe des Autos auf dem nächsten Parkplatz wissen . Sie können sehen, dass Sie auf Punkt 2 zeigen, also fügen Sie einfach 1 hinzu und zeigen auf den nächsten Punkt. (
finger+1
), jetzt, da ich wissen wollte, was die Daten dort waren, müssen Sie diesen Punkt (nicht nur den Finger) überprüfen, damit Sie den Zeiger (*(finger+1)
) überprüfen können, um zu sehen, dass dort ein grünes Auto vorhanden ist (die Daten an diesem Ort) )quelle
"without getting them bogged down in the overall concept"
als Verständnis auf hoher Ebene. Und zu Ihrem Punkt:"I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"
- Sie wären sehr überrascht, wie viele Leute Zeiger auch auf dieses Niveau nicht verstehenIch denke nicht, dass Zeiger als Konzept besonders schwierig sind - die mentalen Modelle der meisten Schüler sind auf so etwas abgebildet, und einige schnelle Box-Skizzen können helfen.
Die Schwierigkeit, zumindest die, die ich in der Vergangenheit erlebt habe und mit der sich andere befasst haben, besteht darin, dass die Verwaltung von Zeigern in C / C ++ unnötig kompliziert sein kann.
quelle
Ein Beispiel für ein Lernprogramm mit einer Reihe guter Diagramme hilft beim Verständnis von Zeigern erheblich .
Joel Spolsky macht in seinem Artikel Guerilla Guide to Interviewing einige gute Punkte zum Verständnis von Hinweisen :
quelle
Das Problem mit Zeigern ist nicht das Konzept. Es geht um die Ausführung und Sprache. Zusätzliche Verwirrung entsteht, wenn Lehrer davon ausgehen, dass das KONZEPT der Zeiger schwierig ist und nicht der Jargon oder das verworrene Durcheinander, das C und C ++ aus dem Konzept machen. Es ist so mühsam, das Konzept zu erklären (wie in der akzeptierten Antwort auf diese Frage), und es ist so ziemlich nur für jemanden wie mich verschwendet, weil ich das alles bereits verstehe. Es erklärt nur den falschen Teil des Problems.
Um Ihnen eine Vorstellung davon zu geben, woher ich komme, verstehe ich Zeiger sehr gut und kann sie kompetent in Assemblersprache verwenden. Weil sie in der Assembler-Sprache nicht als Zeiger bezeichnet werden. Sie werden als Adressen bezeichnet. Wenn es darum geht, Zeiger in C zu programmieren und zu verwenden, mache ich viele Fehler und bin sehr verwirrt. Ich habe das immer noch nicht geklärt. Lassen Sie mich Ihnen ein Beispiel geben.
Wenn eine API sagt:
was will es
es könnte wollen:
eine Zahl, die eine Adresse zu einem Puffer darstellt
(Um das zu geben, sage ich
doIt(mybuffer)
oderdoIt(*myBuffer)
?)eine Zahl, die die Adresse zu einer Adresse zu einem Puffer darstellt
(ist das
doIt(&mybuffer)
oderdoIt(mybuffer)
oderdoIt(*mybuffer)
?)eine Zahl, die die Adresse zu der Adresse zu der Adresse zu dem Puffer darstellt
(Vielleicht ist das
doIt(&mybuffer)
. Oder dochdoIt(&&mybuffer)
? Oder sogardoIt(&&&mybuffer)
)und so weiter, und die betreffende Sprache macht es nicht so klar, weil es die Wörter "Zeiger" und "Referenz" enthält, die für mich nicht so viel Bedeutung und Klarheit haben wie "x hält die Adresse für y" und " Diese Funktion erfordert eine Adresse an y ". Die Antwort hängt außerdem davon ab, womit zum Teufel "mybuffer" anfangen soll und was es damit zu tun beabsichtigt. Die Sprache unterstützt nicht die Verschachtelungsebenen, die in der Praxis auftreten. Zum Beispiel, wenn ich einen "Zeiger" an eine Funktion übergeben muss, die einen neuen Puffer erstellt, und den Zeiger so ändert, dass er auf die neue Position des Puffers zeigt. Will es wirklich den Zeiger oder einen Zeiger auf den Zeiger, damit es weiß, wohin es gehen muss, um den Inhalt des Zeigers zu ändern. Meistens muss ich nur raten, was unter "
"Pointer" ist einfach zu überladen. Ist ein Zeiger eine Adresse auf einen Wert? oder ist es eine Variable, die eine Adresse für einen Wert enthält? Wenn eine Funktion einen Zeiger möchte, möchte sie die Adresse, die die Zeigervariable enthält, oder die Adresse der Zeigervariable? Ich bin verwirrt.
quelle
double *(*(*fn)(int))(char)
, ist das Ergebnis der Auswertung*(*(*fn)(42))('x')
adouble
. Sie können Bewertungsebenen entfernen, um zu verstehen, welche Zwischentypen erforderlich sind.(*(*fn)(42))('x')
dann?x
), bei der Sie, wenn Sie bewerten*x
, ein Doppel bekommen.fn
ist und in Hinblick darauf, was Sie tun mitfn
Ich denke, das Haupthindernis für das Verständnis von Hinweisen sind schlechte Lehrer.
Fast jedem werden Lügen über Zeiger beigebracht: Dass sie nichts anderes als Speicheradressen sind oder dass Sie auf beliebige Stellen verweisen können .
Und natürlich, dass sie schwer zu verstehen, gefährlich und halbmagisch sind.
Nichts davon ist wahr. Zeiger sind eigentlich ziemlich einfache Konzepte, solange Sie sich an das halten, was die C ++ - Sprache über sie zu sagen hat, und ihnen keine Attribute verleihen, die sich "normalerweise" in der Praxis herausstellen, aber dennoch nicht durch die Sprache garantiert werden und sind daher nicht Teil des eigentlichen Konzepts eines Zeigers.
Ich habe vor einigen Monaten versucht, eine Erklärung dafür in diesem Blog-Beitrag zu schreiben - hoffentlich hilft es jemandem.
(Beachten Sie , bevor jemand pedantisch auf mich bekommt, ja, der C ++ Standard nicht sagen , dass die Zeiger darstellen Speicheradressen. Aber es sagt nicht , dass „Zeiger sind Speicheradressen, und nichts als Speicheradressen und verwendet werden können , oder gedacht austauschbar mit Speicher Adressen ". Die Unterscheidung ist wichtig)
quelle
Ich denke, was das Lernen von Zeigern schwierig macht, ist, dass Sie bis zu Zeigern mit der Idee vertraut sind, dass "an diesem Speicherort eine Reihe von Bits ein Int, ein Double, ein Zeichen, was auch immer darstellt".
Wenn Sie zum ersten Mal einen Zeiger sehen, erhalten Sie nicht wirklich, was sich an diesem Speicherort befindet. "Was meinst du damit, es enthält eine Adresse ?"
Ich stimme der Vorstellung nicht zu, dass "Sie sie entweder bekommen oder nicht".
Sie werden leichter zu verstehen, wenn Sie anfangen, echte Verwendungen für sie zu finden (z. B. keine großen Strukturen in Funktionen zu übergeben).
quelle
Der Grund, warum es so schwer zu verstehen ist, ist nicht, dass es ein schwieriges Konzept ist, sondern dass die Syntax inkonsistent ist .
Sie erfahren zunächst, dass der am weitesten links stehende Teil einer Variablenerstellung den Typ der Variablen definiert. Die Zeigerdeklaration funktioniert in C und C ++ nicht so. Stattdessen sagen sie, dass die Variable auf den Typ links zeigt. In diesem Fall:
*
mypointer zeigt auf ein int.Ich habe Zeiger erst vollständig verstanden, als ich versucht habe, sie in C # zu verwenden (mit unsicherer Funktion). Sie funktionieren genauso, aber mit logischer und konsistenter Syntax. Der Zeiger ist ein Typ selbst. Hier MyPointer ist ein Zeiger auf eine int.
Lassen Sie mich nicht einmal mit Funktionszeigern anfangen ...
quelle
int *p;
hat eine einfache Bedeutung:*p
ist eine ganze Zahl.int *p, **pp
bedeutet:*p
und**pp
sind ganze Zahlen.*p
und**pp
sind nicht ganze Zahlen, weil Sie nie initialisiertp
oderpp
oder*pp
zu Punkt zu nichts. Ich verstehe, warum manche Leute es vorziehen, sich an die Grammatik zu halten, insbesondere weil einige Randfälle und komplexe Fälle dies erfordern (obwohl Sie das in allen mir bekannten Fällen trivial umgehen können) ... Aber ich denke nicht, dass diese Fälle wichtiger sind als die Tatsache, dass das Unterrichten der richtigen Ausrichtung für Neulinge irreführend ist. Ganz zu schweigen von hässlich! :)Ich konnte mit Zeigern arbeiten, wenn ich nur C ++ kannte. Ich wusste irgendwie, was ich in einigen Fällen tun sollte und was ich nicht durch Ausprobieren tun sollte. Aber das, was mir das volle Verständnis gegeben hat, ist die Assemblersprache. Wenn Sie mit einem von Ihnen geschriebenen Assembler-Programm ernsthaftes Debuggen auf Befehlsebene durchführen, sollten Sie in der Lage sein, viele Dinge zu verstehen.
quelle
Ich mag die Hausadressanalogie, aber ich habe immer daran gedacht, dass die Adresse an die Mailbox selbst gerichtet ist. Auf diese Weise können Sie das Konzept der Dereferenzierung des Zeigers (Öffnen der Mailbox) visualisieren.
Folgen Sie beispielsweise einer verknüpften Liste: 1) Beginnen Sie mit Ihrem Papier mit der Adresse. 2) Gehen Sie zu der Adresse auf dem Papier. 3) Öffnen Sie die Mailbox, um ein neues Blatt Papier mit der nächsten Adresse darauf zu finden
In einer linear verknüpften Liste enthält das letzte Postfach nichts (Ende der Liste). In einer zirkulären verknüpften Liste enthält das letzte Postfach die Adresse des ersten Postfachs.
Beachten Sie, dass in Schritt 3 die Dereferenzierung auftritt und Sie abstürzen oder einen Fehler machen, wenn die Adresse ungültig ist. Angenommen, Sie könnten zum Postfach einer ungültigen Adresse gehen, stellen Sie sich vor, dass sich dort ein Schwarzes Loch oder etwas befindet, das die Welt auf den Kopf stellt :)
quelle
Ich denke, dass der Hauptgrund, warum die Leute Probleme damit haben, darin besteht, dass es im Allgemeinen nicht auf interessante und engagierte Weise unterrichtet wird. Ich würde gerne sehen, wie ein Dozent 10 Freiwillige aus der Menge holt und ihnen jeweils ein 1-Meter-Lineal gibt, sie dazu bringt, in einer bestimmten Konfiguration herumzustehen und mit den Linealen aufeinander zu zeigen. Zeigen Sie dann Zeigerarithmetik, indem Sie Personen bewegen (und wohin sie ihre Lineale richten). Es wäre eine einfache, aber effektive (und vor allem einprägsame) Möglichkeit, die Konzepte zu zeigen, ohne sich in der Mechanik zu verlieren.
Sobald Sie zu C und C ++ kommen, scheint es für einige Leute schwieriger zu werden. Ich bin mir nicht sicher, ob dies daran liegt, dass sie endlich die Theorie umsetzen, die sie nicht richtig in die Praxis umsetzen, oder dass die Manipulation von Zeigern in diesen Sprachen von Natur aus schwieriger ist. Ich kann mich nicht so gut an meinen eigenen Übergang erinnern, aber ich kannte Zeiger in Pascal und wechselte dann zu C und verlor mich total.
quelle
Ich denke nicht, dass Zeiger selbst verwirrend sind. Die meisten Menschen können das Konzept verstehen. Nun, an wie viele Zeiger können Sie denken oder mit wie vielen Indirektionsebenen fühlen Sie sich wohl? Es braucht nicht zu viele, um die Leute über den Rand zu bringen. Die Tatsache, dass sie versehentlich durch Fehler in Ihrem Programm geändert werden können, kann es auch sehr schwierig machen, sie zu debuggen, wenn in Ihrem Code etwas schief geht.
quelle
Ich denke, es könnte tatsächlich ein Syntaxproblem sein. Die C / C ++ - Syntax für Zeiger scheint inkonsistent und komplexer zu sein, als sie sein muss.
Ironischerweise stieß ich beim Verständnis von Zeigern auf das Konzept eines Iterators in der c ++ Standard Template Library . Es ist ironisch, weil ich nur annehmen kann, dass Iteratoren als Verallgemeinerung des Zeigers konzipiert wurden.
Manchmal kann man den Wald einfach nicht sehen, bis man lernt, die Bäume zu ignorieren.
quelle
(*p)
wäre(p->)
, und so hätten wirp->->x
statt der mehrdeutigen*p->x
a->b
bedeutet einfach(*a).b
.* p->x
bedeutet,* ((*a).b)
während*p -> x
bedeutet(*(*p)) -> x
. Das Mischen von Präfix- und Postfix-Operatoren führt zu mehrdeutigem Parsen.1+2 * 3
das 9 sein sollte.Die Verwirrung kommt von den mehreren Abstraktionsschichten, die im "Zeiger" -Konzept miteinander vermischt sind. Programmierer werden durch gewöhnliche Referenzen in Java / Python nicht verwirrt, aber Zeiger unterscheiden sich darin, dass sie Merkmale der zugrunde liegenden Speicherarchitektur offenlegen.
Es ist ein gutes Prinzip, Abstraktionsebenen sauber zu trennen, und Zeiger tun dies nicht.
quelle
foo[i]
bedeutet, zu einem bestimmten Punkt zu gehen, eine bestimmte Strecke vorwärts zu gehen und zu sehen, was dort ist. Was die Dinge kompliziert, ist die viel kompliziertere zusätzliche Abstraktionsschicht, die vom Standard nur zum Nutzen des Compilers hinzugefügt wurde, die Dinge jedoch so modelliert, dass sie sowohl den Anforderungen des Programmierers als auch des Compilers nicht gerecht werden.Ich habe es gerne anhand von Arrays und Indizes erklärt - die Leute kennen sich vielleicht nicht mit Zeigern aus, aber sie wissen im Allgemeinen, was ein Index ist.
Stellen Sie sich also vor, der RAM ist ein Array (und Sie haben nur 10 Byte RAM):
Dann ist ein Zeiger auf eine Variable wirklich nur der Index (das erste Byte) dieser Variablen im RAM.
Wenn Sie also einen Zeiger / Index haben
unsigned char index = 2
, ist der Wert offensichtlich das dritte Element oder die Zahl 4. Bei einem Zeiger auf einen Zeiger nehmen Sie diese Zahl und verwenden sie als IndexRAM[RAM[index]]
.Ich würde ein Array auf eine Papierliste zeichnen und es nur verwenden, um Dinge wie viele Zeiger anzuzeigen, die auf denselben Speicher zeigen, Zeigerarithmetik, Zeiger auf Zeiger und so weiter.
quelle
Postfachnummer.
Es ist eine Information, mit der Sie auf etwas anderes zugreifen können.
(Und wenn Sie mit Postfachnummern rechnen, haben Sie möglicherweise ein Problem, weil der Brief in das falsche Feld geht. Und wenn jemand in einen anderen Zustand wechselt - ohne Weiterleitungsadresse - dann haben Sie einen baumelnden Zeiger. Ein andererseits - wenn die Post die Post weiterleitet, haben Sie einen Zeiger auf einen Zeiger.)
quelle
Keine schlechte Möglichkeit, es über Iteratoren zu verstehen. Aber schauen Sie weiter, Sie werden sehen, wie Alexandrescu anfängt, sich über sie zu beschweren.
Viele Ex-C ++ - Entwickler (die nie verstanden haben, dass Iteratoren ein moderner Zeiger sind, bevor sie die Sprache ausgeben) springen zu C # und glauben immer noch, dass sie anständige Iteratoren haben.
Hmm, das Problem ist, dass alles, was Iteratoren sind, völlig im Widerspruch zu dem steht, was die Laufzeitplattformen (Java / CLR) erreichen wollen: neue, einfache Verwendung für alle Entwickler. Was gut sein kann, aber sie sagten es einmal im lila Buch und sie sagten es sogar vor und vor C:
Indirektion.
Ein sehr leistungsfähiges Konzept, aber niemals, wenn Sie es vollständig ausführen. Iteratoren sind nützlich, da sie bei der Abstraktion von Algorithmen helfen, ein weiteres Beispiel. Und die Kompilierungszeit ist der Ort für einen Algorithmus, sehr einfach. Sie kennen Code + Daten oder in dieser anderen Sprache C #:
IEnumerable + LINQ + Massive Framework = 300 MB Laufzeitstrafe Indirektion von miesen, schleppenden Apps über Haufen von Instanzen von Referenztypen.
"Le Pointer ist billig."
quelle
Einige der obigen Antworten haben behauptet, dass "Zeiger nicht wirklich schwer sind", aber nicht direkt angesprochen, wo "Zeiger schwer sind!" kommt von. Vor einigen Jahren unterrichtete ich CS-Studenten im ersten Jahr (nur ein Jahr lang, da ich eindeutig daran gesaugt habe), und mir war klar, dass die Idee des Zeigers nicht schwer ist. Was schwierig ist, ist zu verstehen, warum und wann Sie einen Zeiger wollen würden .
Ich glaube nicht, dass Sie diese Frage - warum und wann Sie einen Zeiger verwenden sollten - von der Erklärung allgemeinerer Fragen der Softwareentwicklung trennen können. Warum sollte jede Variable keine globale Variable sein und warum sollte man ähnlichen Code in Funktionen zerlegen (die Zeiger verwenden, um ihr Verhalten auf ihre Aufrufstelle zu spezialisieren).
quelle
Ich sehe nicht, was an Zeigern so verwirrend ist. Sie zeigen auf eine Stelle im Speicher, dh sie speichert die Speicheradresse. In C / C ++ können Sie den Typ angeben, auf den der Zeiger zeigt. Zum Beispiel:
Sagt, dass my_int_pointer die Adresse zu einem Speicherort enthält, der ein int enthält.
Das Problem mit Zeigern besteht darin, dass sie auf einen Speicherort im Speicher verweisen, sodass es einfach ist, an einen Speicherort zu gelangen, an dem Sie sich nicht befinden sollten. Sehen Sie sich als Beweis die zahlreichen Sicherheitslücken in C / C ++ - Anwendungen vor Pufferüberlauf an (Inkrementieren des Zeigers) über die zugewiesene Grenze hinaus).
quelle
Um die Dinge ein bisschen mehr zu verwirren, muss man manchmal mit Ziehpunkten anstelle von Zeigern arbeiten. Handles sind Zeiger auf Zeiger, sodass das Back-End Dinge im Speicher verschieben kann, um den Heap zu defragmentieren. Wenn sich der Zeiger mitten in der Routine ändert, sind die Ergebnisse unvorhersehbar. Sie müssen also zuerst das Handle sperren, um sicherzustellen, dass nichts irgendwohin geht.
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 spricht etwas kohärenter darüber als ich. :-)
quelle
Jeder C / C ++ - Anfänger hat das gleiche Problem und dieses Problem tritt nicht auf, weil "Zeiger schwer zu lernen sind", sondern "wer und wie es erklärt wird". Einige Lernende erfassen es mündlich, andere visuell. Die beste Art, es zu erklären, ist die Verwendung eines "Zug" -Beispiels (Anzüge für verbale und visuelle Beispiele).
Wo "Lokomotive" ein Zeiger ist, der nichts halten kann, und "Wagen" ist das, was "Lokomotive" zu ziehen versucht (oder darauf zeigt). Danach können Sie den "Wagen" selbst klassifizieren, Tiere, Pflanzen oder Menschen (oder eine Mischung aus ihnen) aufnehmen.
quelle