Was sind die Hindernisse für das Verständnis von Zeigern und was kann getan werden, um sie zu überwinden? [geschlossen]


449

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.


20
Die These dieser Frage ist, dass Hinweise schwer zu verstehen sind. Die Frage bietet keinen Beweis dafür, dass Zeiger schwerer zu verstehen sind als alles andere.
Bmargulies

14
Vielleicht fehlt mir etwas (weil ich in GCC-Sprachen codiere), aber ich dachte immer, wenn Zeiger im Speicher als Schlüssel-> Wertestruktur sind. Da es teuer ist, große Datenmengen in einem Programm weiterzugeben, erstellen Sie die Struktur (Wert) und geben den Zeiger / die Referenz (Schlüssel) weiter, da der Schlüssel eine viel kleinere Darstellung der größeren Struktur ist. Der schwierige Teil ist, wenn Sie zwei Zeiger / Referenzen vergleichen müssen (vergleichen Sie die Schlüssel oder die Werte), was mehr Arbeit erfordert, um in die in der Struktur enthaltenen Daten (Werte) einzudringen.
Evan Plaice

2
@ Wolfpack'08 "Es scheint mir, dass ein Speicher in der Adresse immer ein int sein wird." - Dann sollte es Ihnen scheinen, dass nichts einen Typ hat, da sie alle nur Bits im Speicher sind. "Tatsächlich ist der Zeigertyp der Typ der Variable, auf die der Zeiger zeigt" - Nein, der Zeigertyp ist der Zeiger auf den Typ der Variable, auf die der Zeiger zeigt - was natürlich ist und offensichtlich sein sollte.
Jim Balter

2
Ich habe mich immer gefragt, was so schwer zu verstehen ist, dass Variablen (und Funktionen) nur Speicherblöcke sind und Zeiger Variablen, die Speicheradressen speichern. Dieses vielleicht zu praktische Gedankenmodell beeindruckt vielleicht nicht alle Fans abstrakter Konzepte, aber es hilft perfekt zu verstehen, wie Zeiger funktionieren.
Christian Rau

8
Kurz gesagt, die Schüler verstehen wahrscheinlich nicht, weil sie nicht richtig oder überhaupt nicht verstehen, wie der Speicher eines Computers im Allgemeinen und speziell das C-Speichermodell funktionieren. Dieses Buch Programmieren von Grund auf bietet eine sehr gute Lektion zu diesen Themen.
Abbafei

Antworten:


745

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:

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

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:

--- [ttttNNNNNNNNNN] ---
     ^^
     | |
     | + - das FName-Array
     |
     + - Overhead

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.

THouse.Create('My house');

Speicherlayout:

--- [ttttNNNNNNNNNN] ---
    Mein Haus

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.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

Speicherlayout:

    h
    v
--- [ttttNNNNNNNNNN] ---
    Mein Haus

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.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1
    v
--- [ttttNNNNNNNNNN] ---
    Mein Haus
    ^
    h2

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.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

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:

    h <- +
    v + - vor frei
--- [ttttNNNNNNNNNN] --- |
    1234Mein Haus <- +

    h (zeigt jetzt nirgendwo hin) <- +
                                + - nach frei
---------------------- | (Beachten Sie, dass der Speicher möglicherweise noch vorhanden ist
    xx34Mein Haus <- + enthält einige Daten)

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).

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

Die Verwendung hnach 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.

    h <- +
    v + - vor frei
--- [ttttNNNNNNNNNN] --- |
    1234Mein Haus <- +

    h <- +
    v + - nach frei
---------------------- |
    xx34Mein Haus <- +

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.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    h := THouse.Create('My house'); // uh-oh, what happened to our first house?
    ...
    h.Free;
    h := nil;

Hier haben wir den Inhalt der hVariablen 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:

    h
    v
--- [ttttNNNNNNNNNN] ---
    Mein Haus

Speicherlayout nach zweiter Zuordnung:

                       h
                       v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234Mein Haus 5678Mein Haus

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:

procedure OpenTheFrontDoorOfANewHouse;
var
    h: THouse;
begin
    h := THouse.Create('My house');
    h.OpenFrontDoor;
    // uh-oh, no .Free here, where does the address go?
end;

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:

    h <- +
    v + - vor dem Verlust des Zeigers
--- [ttttNNNNNNNNNN] --- |
    1234Mein Haus <- +

    h (zeigt jetzt nirgendwo hin) <- +
                                + - nach dem Verlust des Zeigers
--- [ttttNNNNNNNNNN] --- |
    1234Mein Haus <- +

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.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1.Free;
    h1 := nil;
    h2.OpenFrontDoor; // uh-oh, what happened to our house?

Hier wurde das Haus durch die Referenz abgerissen h1, und obwohl h1es ebenfalls geräumt wurde, hat es h2immer 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:

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := THouse.Create('My other house somewhere');
                         ^-----------------------^
                          longer than 10 characters
                         0123456789 <-- 10 characters

Speicherlayout nach der ersten Zuweisung:

                        h1
                        v
----------------------- [ttttNNNNNNNNNN]
                        5678Mein Haus

Speicherlayout nach zweiter Zuordnung:

    h2 h1
    vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
    1234Mein anderes Haus irgendwo
                        ^ --- + - ^
                            |
                            + - überschrieben

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.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;

Hier stellen wir eine Verbindung von unserem Haus zu unserer Hütte her. Wir können der Kette folgen, bis ein Haus keinen NextHouseBezug mehr hat, was bedeutet, dass es das letzte ist. Um alle unsere Häuser zu besuchen, können wir den folgenden Code verwenden:

var
    h1, h2: THouse;
    h: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;
    ...
    h := h1;
    while h <> nil do
    begin
        h.LockAllDoors;
        h.CloseAllWindows;
        h := h.NextHouse;
    end;

Speicherlayout (NextHouse als Link im Objekt hinzugefügt, vermerkt mit den vier LLLLs im folgenden Diagramm):

    h1 h2
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234Home + 5678Cabin +
                   | ^ |
                   + -------- + * (kein Link)

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:

    h1 h2
    vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
    1234Mein Haus 5678Mein Haus

Könnte diese beiden Adressen haben (ganz links - ist Adresse 0):

  • h1 = 4
  • h2 = 23

Was bedeutet, dass unsere oben verlinkte Liste tatsächlich so aussehen könnte:

    h1 (= 4) h2 (= 28)
    vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234Home 0028 5678Cabin 0000
                   | ^ |
                   + -------- + * (kein Link)

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.


59
Der Pufferüberlauf ist komisch. "Nachbar kommt nach Hause, reißt seinen Schädel auf und rutscht auf deinem Müll und verklagt dich in Vergessenheit geraten."
GTD

12
Dies ist sicher eine schöne Erklärung des Konzepts. Das Konzept ist NICHT das, was ich an Zeigern verwirrend finde, daher war dieser ganze Aufsatz etwas verschwendet.
Bretonischer

10
Aber nur um gefragt zu haben, was finden Sie verwirrend an Zeigern?
Lasse V. Karlsen

11
Ich habe diesen Beitrag mehrmals besucht, seit Sie die Antwort geschrieben haben. Ihre Klarstellung mit Code ist ausgezeichnet, und ich schätze es, dass Sie ihn erneut besuchen, um weitere Gedanken hinzuzufügen / zu verfeinern. Bravo Lasse!
David McGraw

3
Es gibt keine Möglichkeit, dass eine einzelne Textseite (egal wie lang sie ist) jede Nuance der Speicherverwaltung, Referenzen, Zeiger usw. zusammenfassen kann. Da 465 Personen darüber abgestimmt haben, würde ich sagen, dass sie gut genug ist Startseite auf der Information. Gibt es noch mehr zu lernen? Klar, wann ist es nicht?
Lasse V. Karlsen

153

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.


5
Ich glaube nicht, dass ich es falsch verstanden habe. Meine Absicht war es, den Wert der Variablen, auf die verwiesen wird, von John in Sam zu ändern. Es ist etwas schwieriger, mit Menschen zu repräsentieren, da Sie anscheinend den Wert beider Zeiger ändern.
Tryke

24
Aber der Grund, warum es verwirrend ist, ist, dass es nicht so ist, als wäre John von seinem Platz aufgestanden und Sam hätte sich dann hingesetzt, wie wir uns vorstellen könnten. Es ist eher so, als wäre Sam herübergekommen und hätte seine Hand in John gesteckt und die Sam-Programmierung in Johns Körper geklont, als würde Hugo in einer neu geladenen Matrix weben.
Bretonisch

59
Eher wie Sam nimmt Johns Platz ein und John schwebt durch den Raum, bis er auf etwas Kritisches stößt und einen Segfault verursacht.
just_wes

2
Ich persönlich finde dieses Beispiel unnötig kompliziert. Mein Professor sagte mir, ich solle auf ein Licht zeigen und sagte: "Ihre Hand ist der Zeiger auf das Lichtobjekt."
Celeritas

Das Problem bei dieser Art von Beispielen ist, dass Zeiger auf X und X nicht identisch sind. Und das wird nicht mit Menschen dargestellt.
Isaac Nequittepas

124

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.


15
Ich mag das sehr. Es ist nicht schwer zu erkennen, dass beim zweimaligen Schreiben eines Hyperlinks nicht zwei Websites angezeigt werden (genau wie int *a = bbei nicht zwei Kopien *b).
Detly

4
Dies ist eigentlich sehr intuitiv und etwas, mit dem sich jeder identifizieren kann. Obwohl es viele Szenarien gibt, in denen diese Analogie auseinander fällt. Ideal für eine kurze Einführung. +1
Brian Wigginton

Ein Link zu einer Seite, die zweimal geöffnet wird, erstellt normalerweise zwei fast völlig unabhängige Instanzen dieser Webseite. Ich denke, ein Hyperlink könnte vielleicht eher eine gute Analogie zu einem Konstruktor sein, aber nicht zu einem Zeiger.
Utkan Gezer

@ThoAppelsin Nicht unbedingt wahr, wenn Sie beispielsweise auf eine statische HTML-Webseite zugreifen, greifen Sie auf eine einzelne Datei auf dem Server zu.
Dramzy

5
Du überdenkst es. Die Hyperlinks verweisen auf Dateien auf dem Server, das ist der Umfang der Analogie.
Dramzy

48

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.


2
Interessant. Keine Ahnung, wie ich damit anfangen soll. Ressourcen zum Teilen?
Karolis

1
Genau. Zum Beispiel habe ich gelernt, in Assembly vor C zu programmieren und zu wissen, wie Register funktionieren. Das Erlernen von Zeigern war einfach. Tatsächlich gab es nicht viel zu lernen, alles war sehr natürlich.
Milan Babuškov

Nehmen Sie eine einfache CPU, sagen Sie etwas, das Rasenmäher oder Geschirrspüler betreibt, und implementieren Sie sie. Oder eine sehr sehr grundlegende Teilmenge von ARM oder MIPS. Beide haben eine sehr einfache ISA.
Daniel Goldberg

1
Es könnte erwähnenswert sein, dass dieser pädagogische Ansatz von Donald Knuth selbst befürwortet / praktiziert wurde. Knuths Kunst der Computerprogrammierung beschreibt eine einfache hypothetische Architektur und fordert die Schüler auf, Lösungen zu implementieren, um Probleme in einer hypothetischen Assemblersprache für diese Architektur zu üben. Sobald dies praktisch machbar wurde, implementieren einige Schüler, die Knuths Bücher lesen, seine Architektur tatsächlich als VM (oder verwenden eine vorhandene Implementierung) und führen ihre Lösungen tatsächlich aus. IMO ist dies eine großartige Möglichkeit zu lernen, wenn Sie die Zeit haben.
WeirdlyCheezy

4
@ Luke Ich denke nicht, dass es so einfach ist, Leute zu verstehen, die einfach keine Zeiger erfassen können (oder genauer gesagt, Indirektion im Allgemeinen). Sie gehen im Grunde davon aus, dass Personen, die Zeiger in C nicht verstehen, in der Lage sind, Assembler zu lernen, die zugrunde liegende Architektur des Computers zu verstehen und mit einem Verständnis der Zeiger zu C zurückzukehren. Dies mag für viele zutreffen, aber einigen Studien zufolge scheinen einige Menschen die Indirektion selbst im Prinzip nicht zu erfassen (ich finde das immer noch sehr schwer zu glauben, aber vielleicht hatte ich einfach Glück mit meinen "Schülern" ").
Luaan

27

Warum sind Zeiger für viele neue und sogar alte College-Studenten in der C / C ++ - Sprache so ein Hauptverwirrungsfaktor?

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 .

Gibt es Werkzeuge oder Denkprozesse, mit denen Sie verstehen können, wie Zeiger auf der Ebene von Variablen, Funktionen und darüber hinaus funktionieren?

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.

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.

Für eine Übung? Erstellen Sie eine Struktur:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

Gleiches Beispiel wie oben, außer in C:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

Ausgabe:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

Vielleicht erklärt das einige der Grundlagen anhand eines Beispiels?


+1 für "ohne zu verstehen, wie das Gedächtnis physisch gelegt wird". Ich kam aus der Assemblersprache zu C und das Konzept der Zeiger war sehr natürlich und einfach. und ich habe Leute mit nur einem höheren Sprachhintergrund gesehen, die Schwierigkeiten hatten, es herauszufinden. Um es noch schlimmer zu machen, ist die Syntax verwirrend (Funktionszeiger!), Daher ist das gleichzeitige Erlernen des Konzepts und der Syntax ein Rezept für Probleme.
Brendan

4
Ich weiß, dass dies ein alter Beitrag ist, aber es wäre großartig, wenn die Ausgabe des bereitgestellten Codes zum Beitrag hinzugefügt würde.
Josh

Ja, es ist ähnlich wie bei Algebra (obwohl Algebra einen zusätzlichen Verständlichkeitspunkt hat, wenn ihre "Variablen" unveränderlich sind). Aber ungefähr die Hälfte der Menschen, die ich kenne, hat in der Praxis kein Verständnis für Algebra. Es berechnet einfach nicht für sie. Sie kennen all diese "Gleichungen" und Vorschriften, um zum Ergebnis zu gelangen, aber sie wenden sie etwas zufällig und ungeschickt an. Und sie können nicht verlängern sie für ihre eigenen Zwecke - es ist nur einige unveränderliche, uncomposeable Blackbox für sie. Wenn Sie Algebra verstehen und effektiv einsetzen können, sind Sie dem Rudel bereits weit voraus - auch unter Programmierern.
Luaan

24

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

int puts(const char *s);

Ignorieren Sie im Moment den const.Parameter Der übergebene Parameter puts()ist ein Zeiger, dh der Wert eines Zeigers (da alle Parameter in C als Wert übergeben werden), und der Wert eines Zeigers ist die Adresse, auf die er zeigt, oder einfach , eine Adresse. Wenn wir also schreiben, puts(strA);wie wir gesehen haben, übergeben wir die Adresse von strA [0].

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.


Ich frage mich, was der Sinn eines Zeigers ist. In den meisten Codeblöcken, auf die ich stoße, wird ein Zeiger nur in seiner Deklaration angezeigt.
Wolfpack'08

@ Wolfpack'08 ... was ?? Welchen Code suchen Sie?
Kyle Strand

@ KyleStrand Lass mich einen Blick darauf werfen.
Wolfpack'08

19

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/


1
Ich habe Ted Jensens Tutorial unterstützt. Es zerlegt Zeiger auf eine Detailebene, die nicht zu detailliert ist, kein Buch, das ich gelesen habe. Extrem hilfreich! :)
Dave Gallagher

12

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:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

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.


10

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:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

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:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

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önnte enum)


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?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

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).

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

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) )


Verwenden Sie einfach nicht das Wort "Doppelzeiger". Zeiger können auf alles zeigen, so dass Sie offensichtlich Zeiger haben können, die auf andere Zeiger zeigen. Sie sind keine Doppelzeiger.
Gnasher729

Ich denke, dies verfehlt den Punkt, dass die "Finger" selbst, um Ihre Analogie fortzusetzen, jeweils "einen Parkplatz besetzen". Ich bin mir nicht sicher, ob Menschen Schwierigkeiten haben, Zeiger auf der hohen Abstraktionsebene Ihrer Analogie zu verstehen. Es ist zu verstehen, dass Zeiger veränderbare Dinge sind, die Speicherplätze belegen, und wie nützlich dies ist, scheint den Menschen auszuweichen.
Emmet

1
@Emmet - Ich bin nicht anderer Meinung, dass es viel mehr gibt, auf die man in WRT-Zeiger eingehen könnte, aber ich lese die Frage: "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 verstehen
Mike

Gibt es irgendeinen Grund, die Auto-Finger-Analogie auf eine Person auszudehnen (mit einem oder mehreren Fingern - und einer genetischen Abnormalität, die es jedem von ihnen ermöglichen kann, in eine beliebige Richtung zu zeigen!), Die in einem der Autos saß und auf ein anderes Auto zeigte (oder beugte sich vor, um als "nicht initialisierter Zeiger" auf das Ödland neben dem Grundstück zu zeigen, oder eine ganze Hand spreizte sich und zeigte auf eine Reihe von Zwischenräumen als "Array von Zeigern mit fester Größe [5]" oder rollte sich in den "Nullzeiger" der Handfläche. das deutet auf irgendwo hin, wo bekannt ist, dass es NIE ein Auto gibt) ... 8-)
SlySven

Ihre Erklärung war bemerkenswert und eine gute für einen Anfänger.
Yatendra Rathore

9

Ich 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.


9

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 :

Aus irgendeinem Grund scheinen die meisten Menschen ohne den Teil des Gehirns geboren zu sein, der Zeiger versteht. Dies ist eine Eignungssache, keine Fähigkeitssache - es erfordert eine komplexe Form des doppelt indirekten Denkens, die manche Menschen einfach nicht können.


8

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:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

was will es

es könnte wollen:

eine Zahl, die eine Adresse zu einem Puffer darstellt

(Um das zu geben, sage ich doIt(mybuffer)oder doIt(*myBuffer)?)

eine Zahl, die die Adresse zu einer Adresse zu einem Puffer darstellt

(ist das doIt(&mybuffer)oder doIt(mybuffer)oder doIt(*mybuffer)?)

eine Zahl, die die Adresse zu der Adresse zu der Adresse zu dem Puffer darstellt

(Vielleicht ist das doIt(&mybuffer). Oder doch doIt(&&mybuffer)? Oder sogar doIt(&&&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.


Ich habe es so erklärt gesehen: Wenn Sie eine Zeigerdeklaration wie sehen double *(*(*fn)(int))(char), ist das Ergebnis der Auswertung *(*(*fn)(42))('x')a double. Sie können Bewertungsebenen entfernen, um zu verstehen, welche Zwischentypen erforderlich sind.
Bernd Jendrissek

@BerndJendrissek Ich bin mir nicht sicher, ob ich folge. Was ist das Ergebnis der Bewertung (*(*fn)(42))('x') dann?
Bretonischer

Sie bekommen eine Sache (nennen wir es x), bei der Sie, wenn Sie bewerten *x, ein Doppel bekommen.
Bernd Jendrissek

@BerndJendrissek Soll das etwas über Zeiger erklären? Ich verstehe es nicht Worauf willst du hinaus? Ich habe eine Ebene entfernt und keine neuen Informationen über Zwischentypen erhalten. Was erklärt es darüber, was eine bestimmte Funktion akzeptieren wird? Was hat das mit irgendetwas zu tun?
Bretonischer

Vielleicht ist die Nachricht in dieser Erklärung (und es ist nicht von mir, ich wünschte , ich könnte , wo ich zum ersten Mal sieht es) ist , daran zu denken weniger in Hinblick darauf, was fn ist und in Hinblick darauf, was Sie tun mitfn
Bernd Jendrissek

8

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)


Schließlich zeigt ein Nullzeiger nicht auf die Nulladresse im Speicher, obwohl der C-Wert Null ist. Es ist ein völlig separates Konzept, und wenn Sie falsch damit umgehen, werden Sie möglicherweise etwas ansprechen (und dereferenzieren), was Sie nicht erwartet hatten. In einigen Fällen kann dies sogar eine Nulladresse im Speicher sein (insbesondere jetzt, wo der Adressraum normalerweise flach ist), in anderen Fällen kann sie von einem optimierenden Compiler als undefiniertes Verhalten weggelassen werden oder auf einen anderen Teil des Speichers zugreifen, der zugeordnet ist mit "Null" für den angegebenen Zeigertyp. Es kommt zu Heiterkeit.
Luaan

Nicht unbedingt. Sie müssen in der Lage sein, den Computer in Ihrem Kopf so zu modellieren, dass Zeiger sinnvoll sind (und auch andere Programme debuggen können). Das kann nicht jeder.
Thorbjørn Ravn Andersen

5

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).


5

Der Grund, warum es so schwer zu verstehen ist, ist nicht, dass es ein schwieriges Konzept ist, sondern dass die Syntax inkonsistent ist .

   int *mypointer;

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.

  int* mypointer;

Lassen Sie mich nicht einmal mit Funktionszeigern anfangen ...


2
Tatsächlich sind beide Fragmente gültig C. Es ist eine Frage vieler Jahre des C-Stils, dass das erste häufiger vorkommt. Die zweite ist zum Beispiel in C ++ etwas häufiger.
RBerteig

1
Das zweite Fragment funktioniert bei komplexeren Deklarationen nicht wirklich gut. Und die Syntax ist nicht so "inkonsistent", wenn Sie feststellen, dass der rechte Teil einer Zeigerdeklaration Ihnen zeigt, was Sie mit dem Zeiger tun müssen, um etwas zu erhalten, dessen Typ der atomare Typbezeichner auf der linken Seite ist.
Bernd Jendrissek

2
int *p;hat eine einfache Bedeutung: *pist eine ganze Zahl. int *p, **ppbedeutet: *pund **ppsind ganze Zahlen.
Miles Rout

@ MilesRout: Aber genau das ist das Problem. *pund **ppsind nicht ganze Zahlen, weil Sie nie initialisiert poder ppoder *ppzu 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! :)
Leichtigkeitsrennen im Orbit

@LightnessRacesinOrbit Das Lehren der richtigen Ausrichtung ist alles andere als irreführend. Es ist die einzig richtige Art, es zu lehren. NICHT zu lehren ist irreführend.
Miles Rout

5

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.


4

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 :)


Eine böse Komplikation mit der Mailbox-Nummern-Analogie besteht darin, dass die von Dennis Ritchie erfundene Sprache das Verhalten in Bezug auf die Adressen von Bytes und die in diesen Bytes gespeicherten Werte definiert, die vom C-Standard definierte Sprache jedoch "optimierende" Implementierungen zur Verwendung eines Verhaltens einlädt Modell, das komplizierter ist, aber verschiedene Aspekte des Modells auf mehrdeutige, widersprüchliche und unvollständige Weise definiert.
Supercat

3

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.


2

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.


2

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.


Das Problem liegt hauptsächlich in der C-Deklarationssyntax. Aber Zeigergebrauch wäre sicher einfacher gewesen, wenn gewesen (*p)wäre (p->), und so hätten wir p->->xstatt der mehrdeutigen*p->x
MSalters

@ MSalters Oh mein Gott, du machst Witze, oder? Es gibt dort keine Inkonsistenzen. a->bbedeutet einfach (*a).b.
Miles Rout

@ Meilen: In der Tat, und durch diese Logik * p->xbedeutet, * ((*a).b)während *p -> xbedeutet (*(*p)) -> x. Das Mischen von Präfix- und Postfix-Operatoren führt zu mehrdeutigem Parsen.
MSalters

@ MSalters nein, da Leerzeichen irrelevant sind. Das ist so, als würde man sagen, dass 1+2 * 3das 9 sein sollte.
Miles Rout

2

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.


1
Interessant ist, dass C-Zeiger tatsächlich keine Merkmale der zugrunde liegenden Speicherarchitektur verfügbar machen. Die einzigen Unterschiede zwischen Java-Referenzen und C-Zeigern bestehen darin, dass Sie komplexe Typen mit Zeigern haben können (z. B. int *** oder char * ( ) (void * )). Es gibt eine Zeigerarithmetik für Arrays und Zeiger auf Strukturelemente, das Vorhandensein der Leere * und Array / Zeiger-Dualität. Davon abgesehen funktionieren sie genauso.
Jpalecek

Guter Punkt. Es ist die Zeigerarithmetik und die Möglichkeit von Pufferüberläufen - Ausbruch aus der Abstraktion durch Ausbruch aus dem aktuell relevanten Speicherbereich -, die dies bewirkt.
Joshua Fox

@jpalecek: Es ist ziemlich einfach zu verstehen, wie Zeiger auf Implementierungen funktionieren, die ihr Verhalten in Bezug auf die zugrunde liegende Architektur dokumentieren. Sprichwort 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.
Supercat

2

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):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

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 Index RAM[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.


1

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.)


1

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."


4
Was hat das mit irgendetwas zu tun?
Neil Williams

... was versuchst du zu sagen, abgesehen von "statische Verknüpfung ist das Beste, was es je gab" und "Ich verstehe nicht, wie etwas anderes als das, was ich zuvor gelernt habe, funktioniert"?
Luaan

Luaan, du könntest unmöglich wissen, was man lernen kann, wenn man die JIT im Jahr 2000 zerlegt, oder? Dass es in einer Sprungtabelle aus einer Zeigertabelle endet, wie im Jahr 2000 online in ASM gezeigt, sodass es eine andere Bedeutung haben kann, nichts anderes zu verstehen: Sorgfältiges Lesen ist eine wesentliche Fähigkeit. Versuchen Sie es erneut.
Rama-Jka Toti

1

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).


0

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:

int* my_int_pointer;

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).


0

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. :-)


-1: Handles sind keine Zeiger auf Zeiger; Sie sind in keiner Weise Zeiger. Verwirre sie nicht.
Ian Goldby

"Sie sind in keiner Weise Zeiger" - ähm, ich bin anderer Meinung.
SarekOfVulcan

Ein Zeiger ist ein Speicherort. Ein Handle ist eine eindeutige Kennung. Es könnte ein Zeiger sein, aber es könnte genauso gut ein Index in einem Array sein oder irgendetwas anderes. Der Link, den Sie angegeben haben, ist nur ein Sonderfall, bei dem das Handle ein Zeiger ist, dies aber nicht sein muss. Siehe auch parashift.com/c++-faq-lite/references.html#faq-8.8
Ian Goldby

Dieser Link unterstützt auch nicht Ihre Behauptung, dass es sich in keiner Weise um Zeiger handelt - "Zum Beispiel könnten die Handles Fred ** sein, wobei die Zeiger auf Fred * zeigen ..." Ich glaube nicht das -1 war fair.
SarekOfVulcan

0

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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.