Der Zeiger und die Adresse
|
Willemers Informatik-Ecke
Zeiger sind ganz besondere Variablen. Ihr Inhalt ist eigentlich nebensächlich.
Viel interessanter ist, dass sie auf andere Variablen zeigen können.
Sie betrachten also nicht die Zeigervariable selbst, sondern über
die Zeigervariable auf den Inhalt einer anderen Variablen.
Diese Indirektion ist zu Anfang nicht ganz leicht zu verstehen und auch
später durchaus Anlass zur Verwirrung.
Das moderne Parkhaus
Stellen Sie sich vor, Sie fahren jeden Tag mit dem Auto zur Arbeit und
parken in einem hochmodernen Parkhaus. Um zu parken, fahren Sie das Auto
in einen Fahrstuhl, steigen aus und drücken eine Taste.
Daraufhin schließt sich die Fahrstuhltür und eine Automatik verfrachtet
Ihr Auto auf einen freien Parkplatz. Sobald das passiert ist, erhalten Sie
eine Magnetkarte.
Wenn Sie abends Ihr Auto wieder abholen, stecken Sie die Magnetkarte ein
und nach einiger Zeit öffnet der Fahrstuhl und drin steht Ihr Auto.
Sie können einsteigen und nach Hause fahren.
Magnetkarte als Zeiger
Die Magnetkarte entspricht einer Zeigervariablen. Über die Magnetkarte
kommen Sie an Ihr Auto, das irgendwo in dem großen Parkhaus steht. Wo das
Auto steht und was wirklich auf der Karte steht, wissen Sie nicht. Es
interessiert Sie eigentlich auch nicht, sofern Sie Ihr Auto später
wieder zurückbekommen. Wenn Sie dagegen die Karte verlieren, ist das beinahe
so, als ob Sie Ihr Auto verloren haben. Es belegt zwar den Parkplatz und ist
noch physikalisch vorhanden. Sie kommen aber nicht an das Auto heran.
Info-Terminal
Weil es Menschen gibt, die für technische Neuerungen weniger aufgeschlossen
sind als Sie, hat der Betreiber ein Info-Terminal eingerichtet.
Dort können Sie die Magnetkarte einschieben und erhalten die Daten, die über
Ihr Auto beim Einparken gewonnen wurden. So wurde das Auto im Fahrstuhl
gewogen, damit nicht jemand die Statik mit einem Panzer überfordert.
Per Lichtschranke wurde Höhe, Breite und Länge bestimmt, damit die Parkbuchten
optimal genutzt werden können. Und schließlich wurde das Kennzeichen
durch einen Scanner eingelesen.
Stecken Sie also die Magnetkarte ins Terminal erhalten Sie diese Informationen
über Ihr Auto angezeigt: Gewicht, Höhe, Länge, Breite und Kennzeichen.
Diese Daten sind nicht die Daten der Karte, sondern gehören zu dem Auto, auf
das die Karte zeigt.
Darüber hinaus hat der Parkhausbetreiber einen neuen Service eingerichtet.
Die Besitzer, die Ihre Karte verloren haben, können Ihr Auto wieder bekommen,
wenn Sie das Kennzeichen eintippen.
Variablen und Zeiger
Ihre Variablen verhalten sich wie die Autos. Sie befinden sich im
Hauptspeicher, der mit dem Parkhaus vergleichbar ist.
Wo sich Ihre Variablen tatsächlich im Speicher befinden, interessiert Sie
normalerweise nicht. Sie sprechen Sie normalerweise über den Variablennamen
an, so wie Sie das Auto auch über das Kennzeichen erreichen können.
Sie können aber auch eine Zeigervariable definieren, die einer Magnetkarte
entspricht. Wenn Sie der Zeigervariablen die Adresse einer Variablen
zuweisen, können Sie über die Zeigervariablen auf diese Variable zugreifen.
Das ist der gleiche Vorgang, wie beim Beschriften der Magnetkarte. Der
Parkhaus-Computer wird auf der Magnetkarte kodieren, wo er das Auto eingeparkt
hat. Was auf der Magnetkarte wirklich steht, interessiert Sie genauso wenig
wie der Inhalt der Zeigervariablen. Anstatt auf deren direkten Inhalt,
verwenden Sie diese dazu, auf die Variable zu schauen, auf die sie verweist.
Verlieren Sie den Inhalt der Zeigervariablen,
dann bleibt der Variableninhalt unerreichbar, sofern Sie nicht noch den
Variablennamen haben.
Zeigervariable
Ein Zeiger ist eine Variable, deren Inhalt die Position einer anderen
Variablen im Hauptspeicher enthält.
Eine Zeigervariable dient dazu, indirekt
auf einen Speicherinhalt zu verweisen und zuzugreifen.
Definition
Um eine Zeigervariable zu definieren, wird zunächst der Typ angegeben, auf
den der Zeiger zugreifen kann. Es folgt ein Stern und dann der Name der
Variablen.
char *charZeiger;
Die Variable charZeiger ist also ein Zeiger auf eine Variable
vom Typ char. Anders ausgedrückt, kann in der Variablen
charZeiger die Position der Speicherstelle abgestellt werden, an der
sich eine char-Variable befindet.
Adresse
Eine Speicherposition wird Adresse genannt. Die Speicherstellen im Computer
sind durchnummeriert und so verbirgt sich hinter der Adresse einfach eine
Zahl. Um die Adresse einer Variablen zu ermitteln, wird ihr ein kaufmännisches
Und-Zeichen vorangestellt. Dieses Zeichen wird im Englischen als Ampersand
bezeichnet und wird auch in Programmiererkreisen meist so genannt.
Das folgende Beispiel zeigt, wie dem Zeiger charZeiger die Adresse
der Variablen Buchstabe zugewiesen wird.
char Buchstabe = 'A';
charZeiger = &Buchstabe;
Zugriff über den Zeiger
Wie gesagt, interessiert uns der Inhalt der Zeigervariablen eigentlich gar
nicht, sondern der Inhalt der Variablen, auf den der Zeiger zeigt.
Nachdem die Zeigervariable mit der Adresse der Variablen gefüllt ist,
können Sie darauf zugreifen, indem Sie der Zeigervariablen einen Stern
voranstellen:
cout << *charZeiger;
Durch diese Anweisung würde das 'A', das in der Variablen Buchstabe
steht, auf dem Bildschirm ausgegeben. Sie können über die Zeigervariable
sogar neue Inhalte in die Variable Buchstabe schleusen:
*charZeiger = 'B';
cout << Buchstabe;
Durch den Stern vor der Zeigervariablen wird deutlich gemacht, dass wir nicht
auf den Inhalt der Zeigervariablen zugreifen, sondern auf den Speicherplatz,
auf den der Zeiger zeigt. Da charZeiger immer noch die Adresse der
Variablen Buchstabe enthält, wird deren Inhalt nun verändert.
Wenn die Variable Buchstabe ausgegeben wird, erscheint auf dem
Bildschirm jetzt ein 'B'.
Im Buch steht hier die Grafik (abbzeiger)
Die Abbildung zeigt die Situation nach der Zuweisung
des 'B' über den Zeiger charZeiger.
Oben rechts befindet sich die Variable Buchstabe. Es wird hier
einfach davon ausgegangen, dass sie an der Speicherstelle 17543 angelegt
wurde. Genau diese Nummer enthält die Zeigervariable charZeiger,
die ihrerseits an der Stelle 23164 im Speicher liegt.
Auch das Größenverhältnis ist durchaus passend. Während eine Variable vom
Typ char meist ein Byte belegt, benötigt eine Zeigervariable
auf einem normalen PC vier Byte.
Wozu?
Sie können sich sicher vorstellen, dass mit Zeigern sehr flexible Zugriffe
auf die Variablen möglich sind. Und tatsächlich werden Zeiger recht häufig
gebraucht. Hier einige typische Anwendungen für Zeiger, die Sie später im Buch
wiederfinden werden:
- Sie können ein Programmstück schreiben, das über einen Zeiger auf eine
Variable zugreift. Sobald Sie den Zeiger auf die Adresse einer anderen
Variablen >>umbiegen<<, arbeitet das gleiche Programmstück mit
einer anderen Variablen. Sie werden dies noch im Zusammenhang mit
Funktionsparametern kennenlernen.
- Sie können im Laufe des Programms neuen Speicher anfordern. Wie Sie später
sehen werden, wird dazu der Befehl new verwendet. Damit das
Programm auf den neuen Speicher zugreifen kann, liefert new einen
Zeiger darauf.
- Sie können mit Hilfe von Zeigern komplexe Datenkonstrukte nachbilden.
Dazu bilden Sie Variablenverbunde
aus Daten und Zeigern. Zeigt der Zeiger eines Variablenverbundes auf den
nächsten, lassen sich daraus Ketten bilden, die man als
lineare Liste bezeichnet. Wenn Sie in den Datenverbund mehrere Zeiger
anhängen, können Sie ein baumartiges Gebilde erzeugen.
Selbstschutz
Es ist eine gute Idee, einen Zeiger, der noch keine konkrete Zieladresse
auf 0 zu setzen. An der Speicherstelle 0 kann sich definitiv keine Variable
befinden und aus diesem Grund würde ein versehentlicher Zugriff über einen
Nullzeiger immer zum sofortigen Absturz führen. Ein Ende mit Schrecken ist
immer noch besser, als ein Schrecken ohne Ende. Der Schrecken ohne Ende tritt
ein, wenn der Zeiger auf einen zufälligen Wert verweist und das Programm
versehentlich damit weiterarbeitet, ohne dass der Fehler bemerkt wird.
char *charZeiger; // charZeiger definieren
charZeiger=0; // als Nullzeiger sichern
Soll die Variable mit 0 initialisiert werden, können Sie wie
bei einer Integer-Variablen einfach =0 anhängen. In C-Programmen
finden Sie in diesem Zusammenhang oft eine Konstante namens NULL. Dabei
handelt es sich um eine 0, die
explizit für die Verwendung mit Zeigern deklariert ist. In C++ ist NULL weder
erforderlich noch sinnvoll.
char *charZeiger = 0;
Indirekter Zugriff
Um über eine Zeigervariable auf den Inhalt einer anderen Variablen zugreifen
zu können, wird dem Variablennamen ein Stern vorangestellt.
Dadurch wird nicht auf die Zeigervariable selbst, sondern auf die
Speicherstelle zugegriffen, deren Adresse die Zeigervariable enthält. Zur
Wiederholung sind die im vorigen Abschnitt gezeigten Befehle noch einmal
zusammengestellt:
char *charZeiger; // Definition der Zeigervariable
charZeiger = 0; // Sichern als Nullzeiger
char Buchstabe = 'A'; // Variable soll später das Ziel sein
charZeiger = &Buchstabe; // Adresse von Buchstabe zuweisen
*charZeiger = 'B'; // Buchstabe enthält nun 'B'
Indirektionsoperator
Dieser Stern ist dasselbe Symbol, das auch bei der Multiplikation verwendet
wird. Der Compiler erkennt aber aus dem Zusammenhang, wie der Stern zu
interpretieren ist. Beim Zugriff über eine Zeigervariable steht er an
der Position, wo man sonst ein Vorzeichen findet. Da ein Vorzeichen nur ein
Plus oder ein Minus sein kann, erkennt der Compiler den Stern an dieser Stelle
als Operator für den indirekten Zugriff. Man nennt den Stern darum auch
Indirektionsoperator.
Die folgenden Spielereien sollen Ihnen ein Gefühl dafür vermitteln, was Sie mit
Zeigern alles machen können. In den Kommentaren ist beschrieben, was die
einzelnen Anweisungen bewirken.
int main()
{
int *intZeiger = 0;
int intVar = 5;
intZeiger = &intVar;
// der Zeiger bekommt die Adresse von intVar
// dann zeigt intZeiger auf intVar
*intZeiger = 1;
// die Variable, auf die intZeiger zeigt, wird
// mit dem Wert 1 belegt. Damit ist nun der
// Inhalt von intVar 1.
intVar = *intZeiger + 1;
// intVar berechnet sich aus 1 und dem Wert, auf den
// intZeiger zeigt. Das ist aber intVar selbst.
// Darum ist intVar anschließend 2.
}
Im Beispiel wurde die Zeigervariable wie empfohlen bereits bei ihrer
Definition auf 0 gesetzt. Wenn die Zeigervariable lokal ist, kann sie ohne
Initialisierung einen beliebigen Wert enthalten. Das bedeutet, sie zeigt auf
eine zufällige Position im Hauptspeicher. Wird nun aus Versehen über diesen
Zeiger zugegriffen, bevor er korrekt belegt wurde, greift das Programm irgendwo
in den Speicher. So werden unsinnige Daten ermittelt oder gar Daten geändert,
ohne dass es sofort zu einem Fehler kommt. Das Programm läuft mit
defekten Daten weiter und wird erst viel später einen Fehler liefern, dessen
Ursprung dann aber nur schwer erkennbar ist. Wurde der Zeiger dagegen auf 0
gesetzt, wird der erste Zugriff über ihn zum Absturz des Programms führen.
Mit Hilfe eines Debuggers können Sie dann leicht feststellen, wo dieser Absturz
passiert. Sie können auch sofort sehen, dass der Zeiger auf 0 steht, und werden
sicher schnell erkennen, wo Sie vergessen haben, den Zeiger korrekt zu setzen.
Arrays und Zeiger
Verwandtschaft
In C und C++ sind Arrays und Zeiger auf wundersame Weise miteinander verwandt.
Sie können einer Zeigervariablen direkt ein Array zuweisen. Das Ergebnis ist,
dass der Zeiger auf das erste Element des Arrays zeigt.
int Zahlen[4];
int *ZahlZeiger = 0;
ZahlZeiger = Zahlen;
Besonders interessant
ist, dass Sie einer Zeigervariablen auch die eckigen Array-Klammern verpassen
können. Und siehe da: Die Zeigervariable verhält sich, als wäre sie als Array
geboren worden. Im folgenden Beispiel wird die Zeigervariable als Arrayvariable
verwendet:
ZahlZeiger = Zahlen;
ZahlZeiger[3] = 4;
Diese Situation wird in der Abbildung (abbarrayptr) dargestellt.
Nicht ohne Grund wird auch die Arrayvariable Zahlen als Zeiger
dargestellt.
An dieser Stelle befindet sich im Buch die Abbildung (abbarrayptr).
Die folgenden Konstruktionen werden Ihnen vielleicht den Angstschweiß auf die
Stirn treiben; Ihrem Compiler wird es nicht einmal merkwürdig erscheinen.
Die Ausgangssituation scheint zunächst ähnlich:
int Zahlen[4];
int *ZahlZeiger = 0;
ZahlZeiger = Zahlen;
ZahlZeiger[3] = 5;
ZahlZeiger = &Zahlen[2];
ZahlZeiger[1] = 3; // landet in Zahlen[3]!
Die Zeile, in der der Zeiger auf die Adresse des dritten Array-Elements
gesetzt wird, ist besonders originell:
ZahlZeiger = &Zahlen[2];
Da Zahlen[2] nichts anderes als eine gewöhnliche Integer-Variable
ist, kann der Zeiger natürlich auch auf dessen Adresse gesetzt werden.
Wenn Sie diesen Zeiger mit eckigen Klammern verwenden, verhält er sich wie
das Array Zahlen, allerdings um zwei Elemente nach rechts versetzt.
Sie können die Situation in der Abbildung unten sehen.
Im Buch befindet sich hier die Abbildung (abbarrayptr2).
Durch die Verschiebung wird ZahlZeiger[0] identisch zu
Zahlen[2]. Daraus resultiert, dass
ZahlZeiger[1] das letzte Element des Arrays
Zahlen ist.
Würde also das Element ZahlZeiger[2] verarbeitet, würde auf
einen Speicherbereich zugegriffen werden, der außerhalb des reservierten
Raums steht. Die Ergebnisse sind unvorhersehbar.
Der entscheidende Unterschied: Bei der Definition eines Arrays wird der
Speicher für die Array-Elemente reserviert. Bei der Definition eines Zeigers
wird nur Speicher für den Zeiger selbst angelegt.
Zeigerarithmetik
Sie können Zeiger inkrementieren und dekrementieren. Dadurch wird der Zeiger
um so viele Byte weitergesetzt, wie die Größe des Typs ist, auf den er zeigt.
Wenn Sie also einen Zeiger haben, der auf den Anfang eines Arrays zeigt,
wird er durch das Inkrementieren auf die nächste Position im Array gesetzt.
Darum wird gern ein Zeiger verwendet, um ein Array zu durchstreifen.
Beispiel
Wenn Sie ein Array
auf 0 setzen wollen, so können Sie dies über einen Indexoperator tun:
int wert[MAX];
for(i=0; i<MAX; i++)
{
wert[i] = 0;
}
Alternativ können Sie einen Zeiger verwenden und ihn durch das Array laufen
lassen:
int wert[MAX];
int *lauf = wert;
for(i=0; i<MAX; i++)
{
*lauf = 0;
lauf++;
}
Effizienz
Von den beiden Schleifen ist die zweite schneller. Das leuchtet sehr schnell
ein, denn bei Verwendung des Zeigers muss dieser in jedem Durchlauf einmal
erhöht werden. Dazu addiert der Prozessor die Größe des Typs zum Zeiger hinzu.
In der ersten Schleife muss dagegen die Typgröße zunächst mit dem Index
multipliziert werden und dann auf die Array-Adresse addiert werden, um den
Zugriff auf das Element zu bekommen. Danach muss noch der Index erhöht
werden, allerdings nur um 1.
An dieser Stelle befindet sich im Buch die Abbildung
(abbzeigerarith)
for-Schleife
Zeichenketten lassen sich sehr elegant mit der for-Schleife
abarbeiten.
In der Klammer hinter dem Schlüsselwort for stehen nacheinander
die Startanweisung, die Bedingung, unter der die Schleife läuft,
und dann die Anweisung, die am Ende des Schleifenkörpers durchgeführt wird.
Das folgende Beispiel geht davon aus, dass die Variable Quelle eine
Zeichenkette enthält, und gibt sie Buchstabe für Buchstabe auf dem Bildschirm
aus.
char Quelle[MAX];
for (char *p = Quelle; *p; p++)
{
cout << *p ;
}
In der Startanweisung wird die Zeigervariable p definiert und mit der
Array-Variablen Quelle initialisiert. Die nächste Anweisung ist die
Bedingung, unter der
die Schleife weiterläuft. Irritierend ist natürlich die Kürze. *p
liefert das Zeichen, auf das der Zeiger p momentan zeigt. Das ist zwar kein
boolescher Wert, aber Sie erinnern sich, dass C++ Nullwerte als falsch und
alle anderen als wahr interpretiert. Das heißt hier, dass die Schleife
weiterlaufen wird, bis sie auf die Abschluss-Null der Zeichenkette stößt.
Erst durch die Abschluss-Null der Zeichenkette wird der Ausdruck *p
falsch.
Die Abschlussanweisung sorgt schließlich dafür, dass nach jedem Durchlauf des
Schleifenkörpers die Variable p auf das nächste Zeichen vorrückt.
Addition und Subtraktion
Zeiger können nicht nur inkrementiert oder dekrementiert werden. Es ist
auch möglich, Zahlen aufzuaddieren. Dieses Addieren ist insofern konsistent
zum Inkrementieren, als dass der hinzugezählte Wert als Einheit für die
Typgröße behandelt wird. Der folgende Ausdruck verweist auf zwei Elemente
hinter das Element, auf das der Zeiger zeigt:
*(Zeiger+2) = 4;
Zeiger[2] = 4;
In der zweiten Anweisung wird exakt das Gleiche durchgeführt wie in der
ersten. Es wird auf das zweite Element hinter dem Anfangselement zugegriffen.
Die Klammer in der ersten Zeile ist erforderlich, da der Indirektionsoperator
eine höhere Priorität hat als das Pluszeichen.
Konstante Zeiger
Das Schlüsselwort const kennen Sie bereits von der Deklaration von
Konstanten. Es kann auch im Zusammenhang mit Zeigern verwendet werden.
Dabei kann es an zwei verschiedenen Stellen stehen und besitzt dann jeweils
eine etwas andere Bedeutung.
const int *konstantesZiel = &ZielVariable;
Konstantes Ziel
Dieser Zeiger ist so definiert, dass das Schlüsselwort const direkt
vor dem Typ des Ziels steht. Über diesen Zeiger darf die Zielvariable nicht
verändert werden. Der Zeiger selbst kann durchaus inkrementiert werden.
Sie können damit beispielsweise ein Array durchlaufen und ausgeben.
int * const konstanterZeiger = &ZielVariable;
Konstanter Zeiger
Der Zeiger konstanterZeiger darf bezüglich seines Ziels
nicht geändert werden. Das Schlüsselwort const steht direkt vor
dem Namen des Zeigers und deutet darauf hin, dass er sich nicht bewegen darf.
Darum muss der Zeiger bei der Definition bereits endgültig initialisiert werden.
Die Variable, auf die er zeigt, kann allerdings beliebig verändert werden.
const int * const komplettKonstant = &ZielVariable;
Alles konstant
Beim Zeiger komplettKonstant darf weder die referenzierte
Variable noch der Zeiger selbst verändert werden.
Konstante Zeiger werden am häufigsten bei der Beschreibung von
Funktionsparametern eingesetzt.
Anonyme Zeiger
Zeiger haben immer die gleiche Größe, egal auf welche Daten sie zeigen.
Letztlich enthalten sie immer eine Speicheradresse, und die ist für alle
Typen gleich.
Die Größe ist von der Maschinenarchitektur abhängig. So ist auf den heutigen
32-Bit-Systemen ein Zeiger 32 Bit, also 4 Byte, groß.
Auf welchen Typ ein Zeiger zeigt, ist aus Sicht des Computers völlig gleich.
Der Compiler allerdings überwacht, dass ein Zeiger auf eine
char-Variable nicht plötzlich dazu verwendet wird, um auf eine
Variable vom Typ float zuzugreifen.
In bestimmten Situationen kann es sinnvoll sein, Zeiger zu speichern, deren
Ziel nicht bekannt ist. Solche Zeiger werden als Zeiger auf den Datentyp
void definiert.
void *ptr;
Kompatibel
Eine Variable vom Typ void gibt es nicht. Die Definition einer
solchen Variablen würde also zu einem Fehler führen.
Es ist aber durchaus erlaubt einen Zeiger auf void zu definieren.
Sie können einer als Zeiger auf void definierten Variablen einen
beliebigen Zeiger zuweisen, ohne dass sich der Compiler darüber beklagt.
Ansonsten verweigert C++ das Zuweisen von Zeigervariablen unterschiedlichen
Typs. Meist wird ein Zeiger auf void als Transportvehikel für einen
Zeiger verwendet, dessen Zieltyp sich erst noch im Laufe des Programms ergibt.
Einbahnstraße
Wie gesagt, können Sie jeden Zeiger einem void-Zeiger zuweisen.
Wenn Sie aber umgekehrt einem Zeiger einen void-Zeiger zuweisen wollen,
ist dies nur möglich, wenn Sie den void-Zeiger auf den Zieltyp casten.
void *voidPtr;
int *intPtr;
voidPtr = intPtr; // funktioniert problemlos
intPtr = (int *)voidPtr; // explizites Casting notwendig