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'.


Abbildung (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:

Selbstschutz

Es ist eine gute Idee, einen Zeiger, der noch keine konkrete Zieladresse zugewiesen bekommen hat, 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. Allerdings gab es auch den Wunsch nach einer speziellen Null für Zeiger. Darum wurde mit C++11 nullptr als spezielle 0 für Zeiger eingeführt.

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.


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.


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.

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