Pascal-Tutorial
Zeiger, dynamische Speicherzuweisung
10.1. Zeiger
Etwas von seinem "Schrecken" verliert das Thema Zeiger in der Programmiersprache Pascal, verglichen mit anderen Sprachen, insb. C und C++. Dennoch sind Zeiger ein sehr komplexes Sprachelement. An dieser Stelle die wichtigsten Dinge zum Thema behandelt.
Ein Zeiger speichert die Adresse eines Bereiches im Arbeitsspeicher, er zeigt auf einen Bereich im Arbeitsspeicher. Stellen Sie sich den Zeiger selbst als eine Variable vor, deren Aufgabe es ist, eine Adresse abzuspeichern. Diese Adresse ist notwendig, um auf einen Bereich im Arbeitspeicher zuzugreifen. Es ist möglich, auf die Adresse selbst zuzugreifen (etwa, um sie anzuzeigen), aber auch auf den Bereich, auf den der Zeiger zeigt.
Beispielsweise können Sie einen Zeiger definieren, der auf eine Integer-Variable zeigt. Genauer gesagt: Der Zeiger zeigt auf die Speicherstelle, an der die Integer-Variable ihre Zahl speichert. Sie können nun auf den Zeiger (fast) wie auf eine normale Variable zugreifen, den Wert auslesen, aber auch einen neuen festlegen. Es soll an dieser Stelle aber nicht der Eindruck entstehen, Zeiger wären nicht mehr als bloße Alternativbezeichner für Variablen. Wichtig ist, dass jeder Zeiger von einem gewissen Typ sein muss. Ein Zeiger auf eine Integer-Variable (man spricht von einem Zeiger auf Integer) muss ebenso vom Typ Integer sein.
Doch warum ist das so? Wie Sie wissen, belegen Datentypen unterschiedlich viel Speicherplatz, da Variablen dieser Typen auch unterschiedlich große/speicherplatzverbrauchende Werte/Daten abspeichern. Variablen der Typen Byte und Char benötigen nur 1 Byte (8 Bit), da die zu speichernden Werte mit 8 Bit darstellbar sind.
Exkurs zu Char: Jedes im ASCII-Code vorhandene Zeichen wird durch eine Zahl repräsentiert, die einen Höchstwert von 255 hat. Da Zahlen dieser Größe mit 8 Bit darstellbar sind, benötigt Char auch nicht mehr Platz. Ähnliches gilt für Byte. Byte speichert ebenso nur Zahlen mit einem Maximalwert von 255, allerdings werden diese Zahlen nicht automatisch in ein Zeichen umgewandelt, bzw. es muss nicht immer ein Zeichen zugewiesen werden.
Ein Integer benötigt im Vergleich dazu 2 Bytes, womit weitaus größere Zahlen darstellbar sind. Wenn auf eine Variable zugegriffen wird, so erfolgt der Zugriff auf einen Bereich im Speicher. Der Datentyp gibt an, wieviele Bytes der Bereich groß ist. Bei Integer wird auf 2 Bytes zugriffen, also der Wert dieser 2 Bytes geändert oder gelesen. Gleiches gilt für Zeiger. Der Zeiger speichert prinzipiell nur die Startadresse. Auf wieviele Bytes ab dieser Adresse zugegriffen wird, entscheidet der Typ des Zeigers. Ist der Zeigertyp Integer, wird auf die nächsten zwei Bytes zugegriffen, bei Char oder Byte nur auf das nächste Byte usw.
Doch sehen wir uns nun an, wie wir einen solchen Zeiger deklarieren und definieren. Nach der Deklaration existiert die Zeigervariable, sie zeigt aber auf einen beliebigen oder auf keinen Bereich. Der Zugriff auf eine solche Variable kann böse Folgen haben (wobei "moderne" Betriebssysteme verhindern, dass auf fremde Speicherbereiche zugegriffen wird), weshalb es ratsam ist, Zeiger nie undefiniert zu lassen. Bei der Definition wird angegeben, wohin der Zeiger verweist. Die Zeigerdeklaration im Allgemeinen sieht folgendermaßen aus:
VAR Zeiger : ^Datentyp;
Beispielsweise:
VAR Int_Zeiger : ^Integer;
Hiermit wurde ein Zeiger auf einen Integer deklariert. Die Zeigerdeklaration alleine nützt noch nicht viel, solange der Zeiger nicht definiert wurde. Bei der Definition wird dem Zeiger die Adresse zugewiesen, auf die er zeigen soll. Um die Adresse einer Variable zu ermitteln, gibt es den @-Operator sowie die Funktion Addr. Beide funktionieren vollkommen gleich und erzielen letztendlich auch dieselbe Wirkung.
In unserem nächsten Beispiel existiert die Variable original vom Typ Integer. Der Zeiger ptr (Abk. für pointer) soll auf die Variable original zeigen. Über den Zeiger ptr soll auf die Daten, die original speichert, zugegriffen werden.
PROGRAM Zeigerbeispiel; USES Crt; VAR original: Integer; ptr: ^Integer; (* der Zeiger *) BEGIN ptr := @original; (* Zeiger definieren *) original := 1; ptr^ := 2; WriteLn ('Wert der Variable original: ', original); ReadKey; END.
Das obige Beispiel - das an Nutzlosigkeit kaum noch zu übertreffen ist - demonstriert die Verwendung von Zeigern sowie des @-Operators. Es wird die Variable original vom Typ Integer sowie der Zeiger ptr vom Typ ^Integer deklariert. Der vorangestellte Hochpfeil (Zirkumflex) ^ lässt den Compiler erkennen, dass es sich hierbei um einen Zeiger, und nicht um eine normale Variable handelt.
In der ersten Anweisung im Anweisungsblock wird dem Zeiger ptr die Adresse der Variable original übergeben. Damit dies geschieht, wird der Operator @ benötigt. In der nächsten Anweisung bekommt die Variable original den Wert 1 zugewiesen (normale Zuweisung). In der darauffolgenden Anweisung passiert ebenso eine Zuweisung, die sich auf die Variable original auswirkt. Allerdings wird hierfür über den Zeiger ptr auf den Speicherplatz, in dem die Variable original zuvor 1 gespeichert hatte, zugegriffen. Als Folge dessen wird aus der 1 eine 2 (immerhin soll eine 2 zugewiesen werden). Natürlich wäre stattdessen auch original := 2; möglich gewesen, aber hier soll die Verwendung eines Zeigers demonstriert werden. (Keine Sorge, es gibt sinnvollere Anwendungen!) Anschließend wird der Wert der Variable original am Bildschirm ausgegeben. Ausgegeben wird der Wert 2.
An der ersten Zeile im Anweisungsblock ist erkennbar, wie auf einen Zeiger zugegriffen wird, damit eine Adresse zugewiesen werden kann:
Zeiger := Adresse;
Wie eine Adresse im RAM in Pascal noch angegeben werden kann, werde ich später zeigen. Ebenso ist im oberen Beispiel erkennbar, wie man über einen bereits definierten Zeiger auf den Speicherbereich zugreift, auf den er verweist:
... Zeiger^ ...
Ein bereits definierter Zeiger kann wie eine normale Variable verwendet werden! Vergessen Sie bei der Verwendung eines Zeigers aber nicht, den Hochpfeil ^ an das Ende des Zeigerbezeichners anzufügen. Lassen Sie diesen nämlich weg, sprechen Sie nicht den referenzierten Speicherbereich an, sondern die Adresse, die der Zeiger speichert! @ und Addr werden im Allgemeinen folgendermaßen verwendet: (ließ sich ebenfalls im oberen Beispiel bereits erkennen)
@Variable
Addr(Variable)
Betrachten Sie beides einfach als Funktionen. Beide Varianten haben denselben Effekt.
Bisher noch nicht erwähnt habe ich die Speicherung von Adressen in Pascal. Adressen werden in Segment und Offset unterteilt. Die "Gesamtadresse" errechnet sich den Werten von Segment und Offset. Segment ist hierbei ein Vielfaches von 10, was bedeutet, dass der dezimale Segment-Wert 10 als 100 (dezimal) anzusehen ist. Segment 50 sowie Offset 70 würde als Gesamtadresse 570 ergeben. Ich werde hier nicht näher auf diese Adressierung eingehen, da sie zum weiteren Verständnis auch nicht notwendig ist. Wichtig zu wissen ist lediglich, dass beide Werte benötigt werden, um die genaue Adresse zu bestimmen.
Pascal bietet auch die Möglichkeit, den Arbeitsspeicher direkt anzusprechen (was heutige Betriebssysteme aber unterbinden; wäre auch sowohl ein Sicherheits- als auch ein Absturz-Risiko). Hierzu gibt es die vordefinierten Arrays Mem, MemW und MemL. Diese können sowohl gelesen als auch geschrieben werden. Das nächste Beispiel zeigt die Verwendung von Mem. MemW und MemL haben exakt die gleiche Syntax.
... Mem [SEG:OFS] ...
SEG gibt hierbei das Segment und OFS den Offset an. Beispielsweise wäre folgendes möglich:
Mem [$D300:$0004] := 34;
Zugegriffen wird hier auf den Speicherbereich hex D3004, und zwar nur auf 1 Byte. Übrigens muss immer, wenn in Pascal ein Wert hexadezimal angegeben wird, diesem ein $ vorangestellt werden. Das gilt allgemein und unabhängig vom Zeiger-Thema. Sie können auch einer Variable einen hexadezimalen Wert zuweisen. Im Beispiel wird ein Byte mit dem Dezimalwert 34 überschrieben. Wenn auf 2 Bytes zugriffen werden soll, wird MemW benötigt, soll auf 4 Bytes zugegriffen werden, MemL.
An dieser Stelle eine kleine Enttäuschung: Mem funktioniert nicht mit Lazarus (bzw. Free Pascal). Macht ja auch keinen Sinn, da - heutzutage - Speicher nicht mehr beliebig adressiert werden kann. Wenn Sie Mem mit Turbo Pascal (7) unter Windows testen, erhalten Sie einen Speicherzugriffsfehler. Das Programm stürzt dabei ab. Wer es selbst probieren möchte: Vorher Daten sichern! Unter DOS war das noch möglich.
10.2. Dynamische Speicherzuweisung
Das Kapitel Zeiger ist noch nicht abgeschlossen. Die dynamische Speicherplatzzuweisung ist ein praktisches Anwendungsbeispiel, wofür Zeiger benötigt werden. Das Prinzip beruht darauf, Speicherplatz zu reservieren, wenn dieser benötigt wird, und wieder freizugeben, wenn er nicht mehr gebraucht wird. Der Speicherplatz globaler Variablen wird reserviert, noch bevor der Hauptanweisungsblock ausgeführt wird, und wieder freigegeben, wenn das Programm beendet wird. Der Speicherplatz lokaler Variablen wird mit Eintritt in die Funktion/Prozedur reserviert, und beim Austritt aus dieser wieder freigegeben.
Der Speicherplatz einer dynamischen Variable (bisher haben wir immer mit statischen Variablen gearbeitet) kann nach Belieben alloziiert (reserviert) bzw. freigegeben werden. Damit können auch größere Datenmengen im Arbeitsspeicher untergebracht und der Arbeitsspeicher besser ausgenützt werden.
Eine dynamische Variable wird deklariert wie ein Zeiger. Wenn die dynamische Variable benötigt wird, muss Speicherplatz mit der Prozedur New oder GetMem reserviert werden. Wird die dynamische Variable nicht mehr benötigt, kann der Speicher mit Dispose oder FreeMem wieder freigegeben werden.
Die Verwendung von New sieht folgendermaßen aus:
New (Variable);
Wieviel Speicherplatz reserviert werden muss, wird automatisch anhand des Zeigertyps festgestellt. Darum müssen Sie sich also nicht kümmern. Anders ist das allerdings bei GetMem. Hier müssen Sie explizit angeben, wieviel Speicherplatz in Byte reserviert werden soll.
GetMem (Variable, Anzahl_Bytes);
Auf den Inhalt der dynamischen Variable wird wie bei einem "normalen" Zeiger zugegriffen:
... Variable^ ...
Das Freigeben des Speicherplatzes erfolgt mit Dispose oder FreeMem. Mit Dispose kann Speicherplatz ebenso einfach wieder freigegeben werden, wie er reserviert wurde:
Dispose (Variable);
Darum, wieviele Bytes freizugeben sind, müssen Sie sich auch hier nicht kümmern. Bei FreeMem ist das anders:
FreeMem (Variable, Anzahl_Bytes);
PROGRAM Dynamische_Variablen; USES Crt; VAR dynvar: ^Integer; BEGIN New (dynvar); (* Speicher reservieren *) dynvar^ := 1000; (* unproblematisch; Speicher wurde ja bereits reserviert *) Writeln (dynvar^); (* Wert ausgeben *) Dispose (dynvar); (* Speicher freigeben *) ReadKey; END.
Die Variable dynvar wurde vom Typ ^Integer deklariert und soll später als dynamische Variable verwendet werden. Mit New wird der Speicherplatz für diese Variable reserviert. Achten Sie darauf, den Variablennamen ohne Hochpfeil anzugeben!
Nun ist es möglich, auf die Variable zuzugreifen und den Wert 1000 zuzuweisen. Dieser wird zur Überprüfung auf dem Bildschirm ausgegeben. Abschließend werden die 2 Bytes, die Integer benötigt, wieder freigegeben. Prinzipiell wäre es in diesem Beispiel egal gewesen, hätten wir eine statische Variable verwendet, denn hier gibt es keinen größeren Speicherbedarf. Die dynamische Speicherplatzzuweisung macht etwa bei großen Arrays Sinn. Wie üblich, soll das Beispiel die Verwendung des Neuen zeigen und nicht mit Unnötigem verwirren.
Vorheriges Kapitel | Nächstes Kapitel |