Pointer. Bei diesem Wort läuft es C-Anfängern kalt den Rücken runter. Diese seltsamen Variablen mit dem Sternchen vor dem Namen, sie lösen Angst und Schrecken aus. Dabei sind sie genau genommen sehr einfach zu verstehen. Hinzu kommt noch, daß Pointer der Sprache C in vielen Bereichen erst ihre sprichwörtliche Leistungsfähigkeit geben und sie damit von anderen Sprachen wie beispielsweise BASIC abheben.
Um ein bißchen Licht in das Dunkel rund um Pointer zu bringen, habe ich diesen Workshop (übrigens mein erster für den AmZeiger :-) verfasst. Also, lehnen Sie sich zurück, nehmen Sie sich ein kühles Getränk und genießen sie den Exkurs.
Bevor wir darauf eingehen, was es mit Pointern auf sich hat, sollten wir erst einen Blick darauf werfen, wie "herkömmliche" Variablen vom System bzw. einem Programm bearbeitet werden. Das erscheint zunächst sehr komplex, ich werde dennoch versuchen, es so verständlich wie möglich zu erklären.
Auf dem Amiga stehen Ihnen, ebenso wie auch auf dem PC, sieben Datenregister (die man mit den Kürzeln d0-d7 abkürzt) und sechs Adressregister (a0-a6) zur Verfügung. Das siebte Adressregister a7 ist der sogenannte "Stack-Pointer", doch dazu später noch mehr. Diese Register können Sie sich wie Schubladen eines Schranks vorstellen, in denen das System bzw. ein Programm ganz beliebig Daten ablegen kann, um sie später weiterverarbeiten zu können.
Ein kurzes Beispiel:
#include <exec/types.h>void main() { int a = 42; }
An dieser Stelle passiert folgendes: Der Wert 42 wird in einem der sieben Datenregister abgelegt, meistens in d0 oder d1, das kann aber auch situationsabhängig variieren. Ohne zu weit abschweifen zu wollen, möchte ich an dieser Stelle noch erwähnen, daß Sie wahlweise auch selbst festlegen können, welches Datenregister verwendet wird, und zwar mit der Angabe "register":
#include <exec/types.h>void main() { register __d3 int a = 42; }
Nun würde der Wert der Variable "a" grundsätzlich im Datenregister d3 abgelegt werden. Oder einfacher ausgedrückt: Sie befehlen dem Programm, auf jeden Fall die Schublade "d3" des Register-"Schranks" zu benutzen. Das hat allerdings nur in sehr wenigen Fällen einen echten Sinn. Überlassen Sie daher die Registerzuweisung dem Programm.
So, weiter im Text. Neben den sieben Datenregistern gibt es, wie schon erwähnt, auch noch sechs Adressregister. Diese Register unterscheiden sich von den bisher beschriebenen in einem Punkt: Man legt in ihnen keine Datenwerte wie unser "42" ab sondern Speicheradressen! Ferner können Sie nun über die Adressregister auf den Inhalt just dieser Speicheradresse zugreifen. Oder mit anderen Worten: In einem Datenregister können Sie einen beliebigen Zahlenwert ablegen und wieder auslesen, sonst nichts. Mit Adressregistern können Sie auf jede beliebige Adresse des Amiga- Speichers zugreifen, die dort befindlichen Daten manipulieren, abändern oder auslesen.
So, und an dieser Stelle hat sich die Frage nach dem Unterschied zwischen Variablen und Pointern bereits geklärt: Variablen sind das C-Äquivalent zu den Datenregistern, Pointer das Äquivalent zu Adressregistern. Oder ausführlicher: In einer Variable können Sie lediglich einen Zahlenwert ablegen, Pointern teilen Sie die Adresse einer Speicherstelle zu und können ab diesem Zeitpunkt auf den Inhalt dieser Speicherstelle zugreifen.
Der Name "Pointer" leitet sich übrigens aus dessen Funktion ab: Der "zeigt" (engl. "to point") auf eine Speicheradresse.
Bevor wir zu ein paar Pointer-Beispielen kommen, möchte ich, wie oben versprochen, noch den Stack-Pointer a7 erwähnen. Jedes Programm besitzt einen sogenannten "Stack", was zu Deutsch "Stapel" oder "Haufen" bedeutet. Dieser Stack ist eine beliebig große Menge Speicher (die Sie übrigens für jedes Programm selbst per AmigaDOS-"Stack"-Befehl oder im Icon-Informations -Requester einstellen können!), auf den das Programm den Inhalt von Daten- oder Adressregistern auslagert, sofern z.Zt. alle belegt sind und Platz für neue Daten geschaffen werden muß.
Vergleichbar ist das also mit einer Rumpelkammer, in die man momentan Unbenutztes reinwirft, um ein bißchen Platz zu schaffen. Ist dieser Speicher erschöpft (d.h. komplett belegt), so bekommen Sie einen "Stacküberlauf"-Fehler. Dürfte jedem von Ihnen schon mal passiert sein, denke ich :-).
So, jetzt aber zu ein paar Pointerbeispielen. Wie ich oben schon erwähnt habe, kann man über einen Pointer auf jede beliebige Stelle im Speicher des Amiga zugreifen, die dort befindlichen Daten abändern, auslesen oder sonstwas damit machen. Ein sehr schönes Beispiel hierfür ist folgendes kleine Programm:
#include <exec/types.h> #include <stdio.h>void main() { int test; int *ptr;
test = 42; ptr = &test;
printf( " Speicheradresse von test: 0x%08x\n", &test ); printf( " Speicheradresse von ptr : 0x%08x\n", ptr ); }
"test" ist eine ganz normale Variable vom Typ "Integer" (kurz: int). "ptr" hingegen ist ein Pointer, erkennbar an dem Asterisk '*' vor dem Namen. Zunächst wird der Variable der Wert "42" zugewiesen, dann wird dem Pointer die Speicheradresse der Variable zugewiesen (das '&'-Zeichen steht immer für "Adresse von"!). Die nachfolgenden printf()'s demonstrieren das anschaulich: Der Pointer zeigt auf den Speicherbereich der Variable "test".
Interessant ist auch die zweite printf()-Anweisung. Hier wird nicht etwa der Inhalt der Speicheradresse ausgelesen, auf die der Pointer zeigt (wie das geht, besprechen wir weiter unten), sondern die Adresse des Speicherbereichs. Das ist aus einem einfachen Grund so: Der Pointer hat mit dem Inhalt einer Speicherstelle eigentlich nichts zu schaffen. Während Variablen Zahlenwerte als "Inhalt" haben, so besteht der "Inhalt" eines Pointers aus einer Adresse. Eigentlich logisch, oder?
Im übrigen gibt es keine Regeln oder Grenzen, was die Adresse angeht, auf die ein Pointer zeigt. Sie könnten z.B. auch folgendes machen:
#include <exec/types.h> #include <stdio.h>void main() { char *test;
test = (char *) 0x000016bc; printf( " Speicheradresse: 0x%08x\n", test );
test = (char *) 0x0000aa19; printf( " Speicheradresse: 0x%08x\n", test ); }
Es hat allerdings nicht den geringsten Sinn (und kann darüberhinaus auch gefährlich sein!), einem Pointer eine so wahllos ausgesuchte Speicheradresse wie beispielsweise 0x000016bc zuzuweisen. Wenden Sie dieses Prinzip wirklich nur dann an, wenn Sie ganz genau wissen, daß die Adresse korrekt ist und welche Art von Daten sich bei ihr befinden! Ansonsten können Sie leicht einen Systemabsturz hervorrufen, da Pointer NICHT darauf achten, ob die Speicheradresse, auf die sie weisen, belegt oder sonstwie "in use" ist. Damit haben sie nichts zu tun, sie zeigen einfach nur drauf. Was das für Folgen haben kann, damit beschäftigen wir uns etwas später.
So, aber weiter im Text. Unser Pointer zeigt also auf den Speicherbereich der Variable "test". Damit können wir auch über den Pointer auf den Inhalt der Speicheradresse (und damit der Variable "test") zugreifen. Sie erinnern sich, ich habe es oben erwähnt. Ein Beispiel dazu:
#include <exec/types.h> #include <stdio.h>void main() { int test; int *ptr;
test = 42; ptr = &test;
(*ptr) = (*ptr) + 1;
printf( " Wert von test: %ld\n", test ); }
Obwohl "test" der Wert 42 zugewiesen wurde, gibt printf() den Wert 43 aus. Das hat einen einfachen Grund: Wir haben den Wert über den Pointer um 1 erhöht. Sehen wir uns die betreffende Zeile nochmal genauer an:
(*ptr) = (*ptr) + 1;
Additionen dieser Art werden Sie schon kennen, allerdings nur mit normalen Variablen. Beispiel:
int a; a = a + 1;
In diesem Beispiel kann man auch schön erkennen, wie man denn überhaupt auf den Inhalt der Speicherstelle eines Pointers zugreift: Indem man dem Pointernamen das Asterisk '*' voranstellt! Das Asterisk steht in diesem Fall also für "Inhalt der Speicheradresse von".
(*ptr) = (*ptr) + 1;Inhalt des Pointers ist gleich Inhalt des Pointers plus eins;
Der Vollständigkeit halber sei erwähnt, daß man diese Zeile in obigem Beispiel auch etwas vereinfachen könnte:
(*ptr)++;
Diese Zeile hat denselben Effekt. Die runden Klammern um das "*ptr" sind in diesem Fall übrigens unbedingt notwendig (im ersten Beispiel mit der normalen Addition habe ich sie lediglich der leichteren Verständlichkeit halber eingefügt), denn sonst würde das Programm folgendes machen:
*ptr; ptr++;
Wörtlich heißt das: Das Programm würde erst den Inhalt der Speicheradresse auslesen, auf die der Pointer zeigt, und ihn in's Nirvana schicken, und dann würde es die Adresse des Pointers um 1 erhöhen. Der Grund ist eben der, daß das Asterisk in diesem Fall eine höhere Priorität hat, als das Post-Inkrement '++'. Aber lassen wir das, ich will hier nicht zu weit ausholen :-).
Kommen wir stattdessen zu einem weiteren Beispiel, das die manuelle Erhöhung der Speicheradresse eines Pointers demonstriert:
#include <exec/types.h> #include <stdio.h>void main() { char test; char *ptr;
test = 42; ptr = &test;
printf( " Speicheradresse von ptr: 0x%08x\n", ptr );
ptr++;
printf( " Speicheradresse von ptr: 0x%08x\n", ptr ); }
Wenn Sie das obige Programm kompilieren und ausführen, werden Sie sehen, daß die Speicheradresse des zweiten printf() ganz genau um eins größer ist, als die des ersten printf().
Und genau diese Tatsache bringt ein Problem mit sich. Erhöht man aus Versehen (oder mit Absicht??) die Adresse eines Pointers und verändert DANN erst den Inhalt des Speicherblocks, so kann es sein, daß man fremde Daten im Speicher überschreibt. Denn, wie oben schon erwähnt: Ein Pointer führt keinen Test aus, ob die Speicherstelle, auf die er zeigt, irgendwie besetzt ist. Das wäre auch dumm, denn in einem solchen Fall könnten wir in dem Beispiel weiter oben nicht über den Pointer auf den Wert der Variable zugreifen.
Wenn Sie die Adresse eines Pointers ändern, dann müssen Sie sich hundertprozentig sicher sein, daß Sie auf die neue Speicheradresse zugreifen können, ohne dabei fremde und/oder wichtige Daten zu verändern. In der Regel ist das einfach zu bewerkstelligen, dennoch gibt es ein paar Stolpersteine, die ich hier ausführlich erwähnen möchte:
1) Uninitialisierte Pointer
Stellen Sie sich vor, wir hätten in dem Additions-Programm von oben folgende Zeile weggelassen:
ptr = &test;
Was wäre in so einem Fall passiert? Nun, wenn Sie einen Pointer erstellen, dann zeigt er erst mal irgendwo hin in die Speicherpampa. An irgendeine Adresse. Erst mit der Zuweisung der Variablenadresse konnten wir den gewünschten Effekt erzielen. Würden wir die Zeile weglassen, so würden wir mit der Addition willkürlich den Wert der unbekannten Speicheradresse ändern, was fatal enden kann. Schlimmstenfalls mit einem Systemabsturz. Es ist daher wichtig, daß JEDER Pointer mit einer Adresse initialisiert (belegt) wird, bevor er zum Einsatz kommt.
Das Problem mit dem uninitialisierten Pointer hat auch einen Ableger, den man gerne "Zugriff auf einen NULL-Pointer" nennt. Viele Programmierer initialisieren einen Pointer gerne mit der Adresse 0x00000000 (NULL), solange sie ihn noch nicht brauchen:
char *ptr = NULL;
Würde man hier vergessen, dem Pointer vor Gebrauch eine "echte" Adresse zuzweisen, käme es zum System-Crash. Dieselbe Ursache, dieselbe Wirkung.
2) Pointer falscher Größe
In den oben genannten Beispielen habe ich korrekterweise immer einen Pointer vom Typ "char" erstellt, wenn ich damit die char-Variable "test" manipulieren wollte. Auch wenn das unwichtig wirkt, es ist essenziell!
Denn jede Variable hat eine eigene Größe, und eine LONG-Variable ist beispielsweise größer, als eine char-Variable (nämlich um ganze 24 Bit oder 3 Bytes!). Würde ich also einen Pointer vom Typ LONG verwenden, um damit den Wert einer Variable vom Typ char zu verändern, dann würde ich 3 Bytes zuviel überschreiben. Auch das kann locker zum Systemabsturz führen, wenn die 3 Bytes wichtige Daten enthalten.
Nach diesem kurzen Exkurs in die Welt der möglichen Pointer-Fehler kann man sich (berechtigterweise) folgende Frage stellen: Hat es überhaupt einen Sinn oder Nutzen, die Speicheradresse eines Pointers durch Addition oder andere Verfahren zu verändern? Ja, hat es! Zum Beispiel ab dem Moment, wo Arrays in's Spiel kommen. Ein kurzes Beispiel:
#include <exec/types.h> #include <stdio.h>void main() { char test[4] = { 5, 13, 7, 21 }; char *ptr;
ptr = test;
printf( " Wert von ptr: %ld\n", *ptr );
ptr++;
printf( " Wert von ptr: %ld\n", *ptr ); }
Das erste printf() wird den Wert 5 ausgeben, das zweite den Wert 13. Warum ist das so? Nun, in obigem Beispiel wird dem Pointer die Adresse des Arrays "test" zugewiesen. Dem Namen des Arrays muß übrigens KEIN "&"-Zeichen voranstehen! Das hat einen einfachen Grund: "test" ist keine einzelne Variable mehr, sondern steht für einen ganzen Speicherblock ( ein Array eben). Damit hat es denselben Aufbau wie ein Pointer, auch was die zugewiesene Speicheradresse angeht.
Weiter im Text. Die zugewiesene Adresse weist auf den Anfang (!) des ersten Elements des Arrays, in diesem Fall den Wert 5. Dieser wird in dem printf() via "*ptr" ausgelesen. Anschließend wird die Speicheradresse des Pointers um den Faktor 1 erhöht. Nun zeigt der Pointer an den Anfang des zweiten (!) Elements des Arrays, also 13. Damit dürfte das Geheimnis gelüftet sein.
Das funktioniert übrigens auch mit allen anderen Pointertypen, ob long, short oder bool. Wenn Sie beispielsweise einen Pointer vom Typ long erstellen (eine long-Variable hat auf dem Amiga eine Größe von 4 Bytes - das kann von Computersystem zu Computersystem aber auch variieren), dann werden genau 4 Bytes übersprungen, wenn Sie zur Speicheradresse des Pointers den Faktor eins dazuaddieren. Gleichermaßen bei char (1 Byte groß): Wenn Sie die Adresse um 1 erhöhen, überspringen Sie 1 Byte. Probieren Sie es ruhig mal aus, in dem Sie das weiter oben gebrachte Beispiel etwas umschreiben:
#include <exec/types.h> #include <stdio.h>void main() { long test; /* <- Hier hat sich was geändert! */ long *ptr; /* <- Hier hat sich was geändert! */
test = 42; ptr = &test;
printf( " Speicheradresse von ptr: 0x%08x\n", ptr );
ptr++;
printf( " Speicheradresse von ptr: 0x%08x\n", ptr ); }
Während die Differenz bei einem char-Pointer bei 1 lag, so liegt sie nun bei 4. Wie schon erwähnt: Eine long-Variable ist 4 Bytes groß, eine char- Variable nur 1 Byte. Zumindest auf dem Amiga!
Nach all den Beispielen, die ich zum Thema Pointer gebracht habe, stellt sich jetzt aber folgende berechtigte Frage: Wozu braucht man überhaupt Pointer? Zugegeben, es hat keinen großen Sinn, auf den Wert einer Variable über einen Pointer zuzugreifen, wenn man ebenso gut über die Variable selbst darauf zugreifen kann. Den eigentlichen Nutzen von Pointern kann man wie folgt definieren: Mit Pointern kann man direkt auf Speicherblöcke und Daten des Computers zugreifen, auf die man sonst keinen Zugriff hätte.
Um ein praktisches Beispiel zu bringen, stellen Sie sich die folgende Situation einmal vor: Sie möchten ein Programm schreiben, welches eine Datei, sagen wir eine Bilddatei, öffnet, die Daten komplett ausliest und dann auf dem Monitor ausgibt. Das Öffen, Lesen und Ausgeben ist keine große Aufgabe, dazu stellt uns C die Funktionen fopen(), fread() und fwrite() zur Verfügung (siehe Include-Datei "stdio.h"). Doch eine Frage bleibt: Wohin sollen wir die gelesenen Daten überhaupt hintun? Um sie bis zur Ausgabe auf dem Monitor zu speichern, benötigen wir einen Puffer, und zwar einen, der exakt so groß ist, daß er die gesamten Daten der Datei speichern kann.
Nun stehen wir aber vor einem Problem: Jede Datei ist unterschiedlich groß! Die eine ist beispielsweise 5 KB groß, die andere 320 KB. Manche sind auch nur winzige 50 Bytes groß. Der Datenpuffer müßte sich in seiner Größe also jedesmal der Dateigröße anpassen.
Manch ein Programmierer macht es sich in diesem Punkt sehr einfach. Er legt ein Array von, sagen wir mal, 300.000 Elementen an und benutzt dieses fortan als Datenpuffer. Das ist nicht nur billig, sondern verstößt auch in nahezu jedem Punkt gegen die Richtlinien eines sauberen Programmierstils. Erst einmal ist es nicht sicher, daß ein Array mit 300.000 Elementen auch wirklich und immer ausreicht. Dateien können, besonders in der heutigen Zeit, um ein Vielfaches größer sein. Zweitens belegt das Array sehr viel Speicher, der wohl nur in den seltensten Fällen ganz ausgenutzt wird. Wenn wir annehmen, daß es sich bei dem obengenannten Array um ein char-Array handelt, dann belegt es 300 KB RAM. Immer! Egal ob die Datei nun 200 KB oder nur 10 Bytes groß ist. Das ist natürlich totaler Blödsinn.
Die einzig korrekte Lösung des Problems ist "Speicherallokierung" (manch einer sagt auch "Alloziierung" dazu, das klingt in meinen Ohren aber irgendwie blöd ;-). Bevor wir die Daten einlesen, müssen wir einen Speicherblock vom System anfordern, der exakt so groß ist, wie die Datei. Sie können hierzu auf die C-eigene Funktion malloc() zugreifen (was sich in der Regel auch empfiehlt) oder aber die exec.library Funktionen AllocMem() oder AllocVec(). Beachten Sie bitte immer, daß Sie den angeforderten Speicher auch wieder freigeben müssen, wenn Sie ihn nicht mehr brauchen. Bei malloc() wäre das dazugehörige Gegenstück die Funktion free(), bei AllocMem() und AllocVec() wäre es FreeMem() und FreeVec(). Details dazu finden Sie im Handbuch Ihres Compilers oder in diversen Amiga-Programmierbüchern.
Es stellt sich aber folgende Frage: Wie erfahren wir denn, welchen Speicherblock die Funktionen angefordert haben und wo er liegt? Wie können wir auf ihn zugreifen und Daten in ihm speichern? Die Antwort dürfte Sie nicht überraschen: Mittels Pointern! Beide Funktionen, sowohl AllocMem() als auch AllocVec() fordern einen Speicherblock der gewünschten Größe an und geben dessen Adresse (!) zurück. Diese Adresse können wir nun einem Pointer zuweisen. Voila! Der Kreis schließt sich.
Zur Verdeutlichung ein kurzes Beispiel:
#include <exec/types.h> #include <exec/memory.h> #include <clib/exec_protos.h>void main() { char *ptr = NULL;
if( ( ptr = (char *) AllocMem( 300, MEMF_PUBLIC ) ) != NULL ) { FreeMem( ptr, 300 ); } }
Das Programm macht nichts weiter, als einen 300 Byte großen Speicherblock vom System anzufordern. Sofern ein Block dieser Größe gefunden und reserviert werden konnte, gibt AllocMem() dessen Adresse zurück, die sofort dem Pointer "ptr" zugewiesen wird. Schlug das Anfordern hingegen fehl, gibt die Funktion NULL zurück.
Beachten Sie bitte Folgendes: Überprüfen Sie bei Speicherallokierung IMMER (!!!), ob das Anfordern des Speicherblocks erfolgreich war oder nicht. Ich habe es schon weiter oben erwähnt: Wenn Sie auf einen NULL- Pointer zugreifen, dann machen Sie Bekanntschaft mit der System-Failure- Meldung!
Auf dieselbe Weise können wir nun den Speicher für unseren Datenpuffer anfordern. Wir überprüfen einfach die Größe der Datei und allokieren dann einen Speicherblock derselben Größe mittels AllocMem(). Der Pointer bekommt die Adresse des Blocks zugewiesen und wir können ohne Probleme auf ihn zugreifen.
Das Amiga-System hantiert sehr viel mit Pointern herum. Wenn Sie beispielsweise mit der intuition.library Funktion OpenWindow() ein Fenster öffnen, dann erhalten Sie als Rückgabewert, na was wohl, die Adresse des Windows im Speicher. Diese Adresse können Sie in einem Pointer speichern, in diesem Fall einem Pointer vom Typ "struct Window", und anschließend ohne Probleme auf die Elemente des Fensters zugreifen.
Auf diesen Punkt möchte ich übrigens noch etwas genauer zu sprechen kommen. Denn bisher haben wir nur "einfache" Pointer, d.h. Pointer vom Typ char, erstellt. Sie können aber auch Pointer erstellen, die auf Structs weisen. Ein Beispiel:
#include <exec/types.h> #include <stdio.h>struct MeinStrukt { int nummer1; int nummer2; };
void main() { struct MeinStrukt ms = { 14, 41 }; struct MeinStrukt *ptr;
ptr = &ms;
printf( " nummer2 von ms : %ld\n", ms.nummer2 ); printf( " nummer2 von ptr: %ld\n", (*ptr).nummer2 ); }
Beide printf()'s geben 41 aus. Warum ist das so? Nun, was weiter oben mit normalen Variablen funktioniert hat, das klappt auch mit Struct's, denn Struct's sind AUCH Variablen! Nur eben Variablen, die aus vielen Einzelvariablen bestehen, ähnlich einem Array. Wenn wir einen Pointer von demselben Typ auf die Adresse des Structs im Speicher weisen lassen, dann können wir auch problemlos auf die Elemente des Structs zugreifen.
Sehen Sie sich das zweite printf() ruhig einmal noch etwas genauer an. Es sollte Ihnen nicht schwerfallen, es zu verstehen. "*ptr" steht für "Inhalt der Speicheradresse von ptr". Der Inhalt sind in diesem Fall die Elemente des Structs. Auf ein einzelnes Element eines Structs kann man zugreifen, wenn man dem Namen des Structs einen Punkt '.' und dann den Namen des Elements folgen läßt. Just dieses tun wir hier.
Da Pointer sehr oft auf Struct's weisen und die Schreibweise mit dem "*ptr" auf Dauer etwas lästig sein kann, haben sich die Erfinder von C etwas Besonderes einfallen lassen: Man kann sich die Geschichte mit dem Sternchen vor dem Namen des Pointers sparen, wenn man statt des Punktes '.' den Strich mit dem Pfeil '->' verwendet:
ptr->nummer2
ist also gleichbedeutend mit
(*ptr).nummer2
So, und damit wären wir auch am Ende dieses Workshops angekommen. Ich hoffe, daß ich Ihnen das Thema Pointer etwas "schmackhaft" und natürlich auch verständlich machen konnte. Für Fragen jeder Art stehe ich selbstverständlich gerne zur Verfügung: empire_mm@t-online.de.
![]() |
Inhaltsverzeichnis | ![]() |
| © '99 Der AmZeiger | ||