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 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.
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 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:
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 h1
es ebenfalls geräumt wurde, hat es h2
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:
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 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:
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):
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.