Pointer
Autor: Motzi
E-Mail:
motzi_84@aon.at
Tutorial zum Downloaden
Inhalt:
1.0 - Pointer
1.1 - Was sind Pointer?
1.2 - Wozu Pointer ?
1.3 - Die Arbeit mit Pointers
1.3.1 - Der @-Operator
1.3.2 - Der ^-Operator
2.0 - Der Bezug Objekte-Pointer
2.1 - Die Situation in "Old-Pascal"
2.2 - Die Situation in Delphi
2.3 - Die Hintergründe
3.0 - Das Programm
3.1 - Die Vorbereitung
3.2 - Das Erzeugen
3.3 - Das Anzeigen/Verstecken
3.4 - Das Löschen
3.5 - Das Beenden
1.0 - Pointer
Da dieses Programm auf Pointers aufbaut hier zuerst einmal eine allgemeine Einführung in die Arbeit mit Pointer. Wer mit diesem Bereich schon vertraut ist kann diesen Teil überspringen.
1.1 - Was sind Pointer?
Im 32-bittigen Windows hat jedes Programm seinen eigenen 2 GB großen Adressraum (eigentlich sinds 4 GB, allerdings verwendet Windows 2 GB in eigener Regie. Das Programm hat daher nur Zugriff auf die unteren 2 GB) In diesem Adressraum werden alle Objekte (z.B.: Komponenten, ...) und Variablen gespeichert. Allerdings muss man auf diese auch zugreifen können, und das funktioniert mit sogenannten Pointers oder Zeigern. Ein Pointer ist also eigentlich nichts anderes als eine Integer-Variable in der eine Adresse gespeichert ist, also ein Zeiger auf diese Adresse. An dieser Adresse können nun Daten abgelegt werden oder auch schon liegen.
Angenommen wir haben einen Schrank der jetzt den Speicher repräsentiert. In diesem Schrank haben wir 100 Schubladen und an jeder Schublade wurde eine Nummer angebracht, also von 1 bis 100. Diese Nummern alle möglichen Adressen für diesen Speicher (den Schrank) Genauso wie eine Nummer angibt in welcher Lade sich die benötigten Daten befinden, genauso kann man sich den Zugriff über einen Pointer vorstellen. Die Nummer gibt nur die Lade an, nicht was sich darin befindet. Ein Pointer gibt nur die Adresse im Speicher an, nicht was sich dahinter verbirgt.
1.2 - Wozu Pointer ?
Diese Frage wird immer wieder gestellt... Wozu sind Pointer überhaupt gut? Die Verwendung von Zeigern ist aus mehreren Gründen sinnvoll. Sie sind für das Verständnis der Sprache Object Pascal wichtig, da sie in einem Programm oft hinter den Kulissen agieren und nicht explizit auftreten. Zeiger werden von allen Datentypen verwendet, die große, dynamisch zugewiesene Speicherblöcke benötigen. Beispielsweise sind lange String-Variablen ebenso wie Klassenvariablen implizite Zeiger. In vielen komplexen Programmierkonstrukten ist die Verwendung von Zeigern unverzichtbar.
In vielen Situationen sind Zeiger die einzige Möglichkeit, die strikte Typisierung der Daten durch Object Pascal zu umgehen. Du kannst z.B. die in einer Variablen gespeicherten Daten ohne Berücksichtigung ihres Typs verarbeiten, indem du sie über den Allzweckzeiger Pointer referenzieren, diesen in den gewünschten Typ umwandeln und ihn anschließend wieder dereferenzieren. (die Begriffe referenzieren und dereferenzieren werden weiter unten erklärt) Außerdem werden in vielen Algos und Programmen ganze Blöcke verschoben und herumkopiert. Wenn man sich mit Pointern auskennt erspart man sich das und verbiegt einfach die Zeiger ein bisschen. D.h. dass statt kilobyte schweren Blöcken nur die 4 Byte des Pointers kopiert werden, und das das schneller geht sollte doch jedem einleuchten.
1.3 - Die Arbeit mit Pointers
1.3.1 - Der @-Operator
Der @-Operator liefert die Adresse an der eine Variable zu finden ist, d.h. er erzeugt einen Zeiger auf seinen Operanden. z.B.:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7:
| var p: Pointer; i: Integer; begin i := 1; p := @i; end; |
Der @-Operator kann auch im Zusammenhang mit Funktionen Prozeduren oder Methoden auftauchen, allerdings möchte ich nicht näher darauf eingehen, da dies in diesem Programm nicht verwendet wird. Wer Lust hat kann das ja in der Delphi-Hilfe nachlesen.
Hinweis: Der @-Operator kann nicht im Zusammenhang mit Objekt-Eigenschaften verwendet werden! Eine Objekt-Eigenschaft ist nur eine Schnittstelle über die indirekt auf eine intern verwaltete Variable zugegriffen werden kann. Eine Objekt-Eigenschaft ist also keine Variable in diesem Sinne und hat daher auch keine eigene Adresse. Und auf die privaten Variablen hat man keinen Zugriff!
1.3.2 - Der ^-Operator
Der ^-Operator referenziert bzw. dereferenziert einen Pointer, erfüllt also 2 Funktionen. Er kann vor einem Typbezeichner stehen, z.B.:
Delphi-Quelltext
In diesem Fall bezeichnet der Operator einen Typ, der Zeiger auf Variablen des Typs Typname darstellt. Der Typ Typname wird referenziert.
Das Symbol ^ kann aber auch auf eine Zeigervariable folgen:
Delphi-Quelltext
In diesem Fall dereferenziert der Operator den Zeiger, d.h. er liefert den Wert an der Speicheradresse, die der Zeiger angibt.
Beispiel:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7:
| var X, Y: Integer; P: ^Integer; begin X := 17; P := @X; Y := P^; end; |
Hier nochmal eine genauere Erklärung zu den einzelnen Zeilen:
P: ^Integer;
Hier wird eine Variable deklariert, die einen Pointer auf einen Integer darstellt. Dem Compiler wird klar gemacht, dass es sich bei dieser Variable um einen Pointer handelt und dass an der Adresse, auf die dieser Pointer zeigt eine Integer-Variable liegt.
P := @X;
Wie schon oben beschrieben bekommt man durch den @-Operator die Adresse einer Variable. Diese Adresse kann nun einer Pointer-Variable (egal ob das jetzt der "Allzweck-Pointer" Pointer ist, oder irgendein Pointer der einen andren Objekt-Typ referenziert) zugewiesen werden. Der Pointer zeigt dann auf die ihm zugewiesene Adresse. Da in diesem Bsp. X eine Integer-Variable ist und P ein Pointer der eine Integer-Vraible referenziert, kann man nach dieser Zuweisung P ohne Probleme dereferenzieren und man bekommt den Inhalt der Integer-Variable auf die der Pointer zeigt.
Y := P^;
Hier wird der Pointer P dereferenziert und der dadurch bekommene Wert wird Y zugewiesen. Dereferenzieren bedeutet, dass man auf das Objekt, auf das der Pointer zeigt, zugreift. Ein dereferenzierter Pointer verhält sich genauso wie eine normale Variable von der Klasse die der Pointer referenziert. Damit ein Pointer dereferenziert werden kann, muss er ein Objekt referenzieren. Das ist zwingend notwendig, da der Compiler erst mit dieser Referenzierung den "Bauplan" des Objektes mit auf den Weg bekommt. Erst dadurch weiß der Compiler um was für ein Objekt es sich da handelt auf das der Pointer zeigt und wie es gehandhabt werden muss.
Wenn jetzt jedem klar ist, dass das obige Bsp. im Prinzip genau dasselbe macht wie die Zuweisung Y := X hat dieser Teil seinen Zweck erfüllt!
Hinweis: Der Allzwecktyp Pointer kann nicht dereferenziert werden. Er muss zuvor in einen anderen Pointertyp umgewandelt werden!
Der @- bzw der ^-Operator sind also genau das Gegenteil voneinander und heben sich damit auf. Die Verwendung von @IrgendeinPointer^ ist also ident mit der Verwendung von IrgendeinPointer.
Das war es dann auch schon. Wenn ihr euch diese Seite durchgelesen habt, habt ihr hoffentlich das Wesentliche bereits verstanden. Bevor wir jetzt zum Programm kommen noch kurz was über Objekte.
2.0 - Der Bezug Objekte-Pointer
Viele werden sich jetzt vielleicht denken, was Objekte mit Pointers/Zeigern zu tun haben.. mehr als Delphi vermuten lässt!
2.1 - Die Situation in "Old-Pascal"
Also.. damit man vielleicht den Hintergrund ein bisschen besser versteht mal ein Code aus alten Pascal Zeiten:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
| type PBeispiel = ^TBeispiel; TBeispiel = object(TObject) ID: Integer; constructor Init; end; var aBsp: PBeispiel; begin aBsp := New(PBeispiel, Init); aBsp^.ID := 5; Dispose(aBsp); end; |
Was passiert hier?
PBeispiel = ^TBeispeil;
TBeispiel = object(TObject)
Es wird ein neues Objekt TBeispiel deklariert und ein Pointer PBeispiel der ein TBeispiel-Objekt referenziert.
aBsp := New(PBeispiel, Init);
Hier wird ein Objekt vom Typ TBeispiel erzeugt und der Pointer auf diese wird aBsp zugwiesen.
aBsp^.ID := 5;
Da wir nur einen Pointer auf das Objekt haben müssen wir diesen dereferenzieren wenn wir auf das Objekt bzw. dessen Eigenschaften zugreifen wollen.
Dispose(aBsp);
Hiermit geben wir das Objekt über dessen Pointer wieder frei.
2.2 - Die Situation in Delphi
Und hier das ganze nochmal aus "Delphi-Sicht":
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:
| type TBeispiel = class(TObject) ID: Integer; constructor Create; end; var aBsp: TBeispiel; begin aBsp := TBeispiel.Create; aBsp.ID := 5; aBsp.Free; end; |
Ich hoffe diese Zeilen sind allen klar.
2.3 - Die Hintergründe
Borland Pascal
Der Unterschied sollte jedem sofort auffallen! Ein Objekt unter Borland Pascal muss einen Zeigertyp explizit deklarieren. Um eine Instanz dieses Objekts, ist der Aufruf der Funktion New notwendig, wobei New den "Bauplan" des Objekts via Parameter mit auf den Weg bekommt. Auf Objektfelder (Eigenschaften, Methoden) dieses Objektes darf erst nach der Dereferenzierung zugegriffen werden und auch für das Entsorgen der Instanz ist eine globale Prozedur, Dispose zuständig.
Delphi
Ganz anders - und vor allem einfacher - sieht die Situation in Delphi aus. Zum einen wird kein spezieller Zeigertyp für das Objekt benötigt, da die Instanzvariable für das Objekt vom gleichen Typ ist wie das Objekt selbst. Auf Objektfelder (Eigenschaften, Methoden) wird direkt - also ohne Dereferenzierung - zugegriffen, obwohl die Instanzvariable nur ein Zeiger auf die Objektinstanz ist. Der Unterschied zum Pascal-Bsp. ist, dass Delphi diese Objekte implizit dereferenziert und du gar nichts davon mitbekommst. Allerdings verbirgt dieser Mechanismus auch etwas, was vielen Anfängern und Unwissenden nicht bwusst ist. Wir haben gerade gehört, Objekte sind Pointer, also gilt für diese Zuweisung:
Delphi-Quelltext
Es werden keine Objektfelder kopiert! Stattdessen wird Object1 der Wert von Object2 zugewiesen und der Wert eines Objektes ist nunmal der Pointer auf dieses. Daher bekommt Object1 die Adresse von Object2. Und da jetzt beide Objecte (Pointer) auf dieselbe Adresse zeigen sind die beiden Object ident, da sie eigentlich nur ein Objekt kapseln.
Das gefährliche dabei ist, dass wenn ein Unwissender eine solche Zuweisung in seinem Programm verwendet und beide Objekte instanziert sind geht der eine Pointer verloren (in obigem Bsp. eben der von Object1) und es entsteht ein Speicherleck. Sofern man nicht noch ein andres Objekt/Pointer hat, das auf auf diese Adresse zeigt hat man keine Möglichkeit mehr auf das Objekt zuzugreifen und den Speicher freizugeben!
PS: Jedes Delphi-Objekt führt intern eine Variable Self mit, mit der auf das besitzende Objekt zugreifen kann.
3.0 - Das Programm
Wie man z.B. bei ICQ sehen kann, kann man ein Fenster auch mehrmals anzeigen. Da aber jedes Fenster unabhängig von den anderen Fenstern sein soll, braucht jedes seine eigene Adresse. Dies ließe sich auf zwei Arten lösen. Eine Möglichkeit wäre einen Dynamischen Array zu nehmen, die zweite eben mit Pointers zu arbeiten, wobei diese Methode flexibler ist. (Wenn jemand meint mit einem Array wäre es einfacher, dann wünsche ich ihm viel Spaß beim ausprobieren!)
3.1 - Die Vorbereitung
Also, für unser Programm brauchen wir zwei Formen. Eine Hauptform (bei mir MainForm genannt) und eine Form die wir zur Laufzeit öfters erzeugen wollen (bei mir MultipleForm). Auf der MainForm befinden sich diverse Buttons zum Erzeugen, Löschen und Anzeigen und außerdem noch eine Listbox in der wir dann die Liste der Formulare anzeigen. (Das Label auf der MultipleForm ist nur optional um zu zeigen, das jedes Fenster unabhängig von den anderen ist)
Aufgrund der ungarischen Notation ist es üblich alle Objektdefinitionen mit einem großen T zu beginnen (z.B. TForm, TButton, TEdit, ...). Daher werden Pointer eben mit einem großen P definiert.
Delphi-Quelltext
1:
| type PMultipleForm = ^TMultipleForm; |
Bei einem Klick auf den CreateButton soll also eine Form erzeugt und in die Liste eingefügt werden, nur wie? Alles was wir brauchen ist eine TList die wir bereits zu Beginn (im OnCreate-Ereignis) erzeugen.
Delphi-Quelltext
1: 2: 3: 4:
| procedure TMainForm.FormCreate(Sender: TObject); begin FormListe := TList.Create; end; |
Der Typ TList verwaltet eine dynamische Liste mit Pointers.
3.2 - Das Erzeugen
Nun geht es ans Erzeugen. Also, gehen wir langsam das OnClick-Ereignis des Create Button durch.
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8:
| procedure TMainForm.CreateButtonClick(Sender: TObject); begin GetMem(FormP, SizeOf(TMultipleForm)); FormP^ := TMultipleForm.Create(Self); FormP^.Caption := FormP^.Caption + IntToStr(Liste.Items.Count + 1); FormP^.Label1.Caption := LabelText.Text; FormP^.OnHide := ListeClick; FormListe.Add(FormP); Liste.Items.Add('Form' + IntToStr(Liste.Items.Count + 1)); end; |
GetMem(FormP, SizeOf(TMultipleForm));
FormP ist ein Zeiger auf ein Objekt des Typs TMultipleForm. Nun muss an dieser Adresse aber noch der Speicherplatz reserviert werden den ein TMultipleForm-Objek belegt, und das geschieht eben mit GetMem.
FormP^ := TMultipleForm.Create(Self);
Mit FormP^ wird der Pointer FormP dereferenziert, also auf das Objekt auf das er zeigt zugegriffen und mit TMultipleForm.Create die neu erzeugte Form als das Objekt von FormP im Speicher abgelegt.
FormP^.Caption := FormP^.Caption + IntToStr(Liste.Items.Count + 1);
FormP^.Label1.Caption := LabelText.Text;
FormP^.OnHide := ListeClick; { --> beim verstecken des Fensters
muss ja die Button-Aufschrift aktualisiert werden...}
Diese Zeilen sollten sich von selbst erklären.
FormListe.Add(FormP);
Die Form Liste vom Typ TList verwaltet alle Pointer an denen unsere Formen gespeichert sind, also müssen wir den Pointer dazugeben, damit wir nachher wissen wo die Form gespeichert ist und auf sie zugreifen können.
3.3 - Das Anzeigen/Verstecken
So, jetzt geht's ans Anzeigen.
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9:
| procedure TMainForm.ShowHideClick(Sender: TObject); begin FormP := FormListe[Liste.ItemIndex]; FormP^.Visible := not FormP^.Visible; if FormP^.Visible then ShowHide.Caption := 'Verstecken' else ShowHide.Caption := 'Anzeigen'; end; |
Sollte eigentlich allen klar sein was da passiert.
3.4 - Das Löschen
Irgendwann will man die Formen sicher wieder löschen, also schauen wir uns folgendes an.
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8:
| procedure TMainForm.DelButtonClick(Sender: TObject); begin FormP := FormListe[Liste.ItemIndex]; FormP^.Free; FreeMem(FormP, SizeOf(TMultipleForm)); FormListe.Delete(Liste.ItemIndex); Liste.Items.Delete(Liste.ItemIndex); end; |
FormP^.Free;
Normalerweise kümmert sich bei einem Aufruf von Free Delphi darum, dass alles gelöscht und der Arbeitsspeicher wieder freigegeben wird, hier ist es allerdings nicht so. Nach dem Aufruf von Free sind zwar die Informationen über die Form aus dem Arbeitsspeicher verschwunden, aber wir haben ja auch Platz für den Pointer reserviert. (Siehe 2.2 - Das Erzeugen) Also müssen wir den auch freigeben.
FreeMem(FormP, SizeOf(TMultipleForm));
Mit dem Aufruf von FreeMem wird exakt der Speicher den wir am Anfang mit GetMem an der Adresse von FormP reserviert haben wieder freigegeben
3.5 - Das Beenden
Beim Beenden einer Anwendung wird zwar der gesamte Inhalt des Adressbereiches gelöscht, aber man sollte es sich trotzdem zur Angewohnheit machen, alle manuell erzeugten Objekte auch selbst wieder freizugeben.
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19:
| procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction); var i: Integer; begin for i := 0 to FormListe.Count-1 do begin FormP := FormListe[i]; FormP^.Hide; FormP := FormListe[i]; FormP^.Free; FreeMem(FormP, SizeOf(TMultipleForm)); end; FormListe.Free; end; |
Wenn man den Rest verstanden hat sollte das auch klar sein.
Das war es auch schon. Im Source-Code sind zwar noch ein paar Feinheiten enthalten, aber wenn das alles klar ist sollten die auch kein Problem darstellen.