Zeiger

Einführung

In der letzten Lektion sind Sie in den Genuss von Referenzen gekommen ;-D. Nun lernen Sie ein ähnliches, ein jedoch viel mächtigeres Mittel kennen: Den Zeiger. 

Sobald Sie eine Variable deklarieren wird Speicher reserviert und die Variable repräsentiert den Speicher. Jeder Variablen lässt sich eine Adresse zuordnen, die im Prinzip angibt wo Sie sich im physischen Speicher befindet. Ihr Datentyp legt dagegen fest wieviel Speicher sie belegt. Der Ort im Speicher ist als Ganzzahl (32-Bit) als sogenannte Adresse zu verstehen. Einen Zeiger dessen Datenyp wie bei Referenzen von jedem Datentyp abgeleitet werden kann, hält als Wert diese Ganzzahl also die Adresse einer Variablen.

Deklaration und Initialisierung

Die Adresse einer Variablen lässt sich mit dem Adressoperator '&' ermitteln. Die Adresse von 'age_a' wäre demnach '&age_a'. Um einen Zeiger von einem "einfachen Datentyp unterscheiden zu können, sieht seine Deklaration (wie bei den Referenzen) geringfügig anders aus.

<Datentyp> <Variable> = <Wert>;
<Datentyp>* <Zeiger> = &<Variable>;

oder

<Datentyp> <Variable> = <Wert>;
<Datentyp> *<Zeiger> = &<Variable>;

Zur Deklaration eines Zeigers wird der *-Operator verwendet. Im Gegensatz zu Referenzen lassen sich Zeiger immer neu zuweisen. So könnte ein Zeiger 'p' z.B. erst auf die Variable 'age_a' und dann auf 'age_b' zeigen:

unsigned short age_a = 65, age_b = 66, *p = &age_a;
(...)
p = &age_b;

An dieser Form der Deklaration lässt sich des weiteren erkennen dass sich Zeigervariablen und "einfache" Variablen in der gleichen Zeile Deklarieren lassen. Ich empfehle ihnen aber aus Gründen der übersicht dies zu vermeiden. Folgende Notation könnte nämlich arge Probleme in der Interpretation (für den Programmierer) bedeuten:

int* a, b;

In diesem Fall wäre Variable 'a' nämlich ein Zeiger und Variable 'b' nicht. Wenn Sie schon Zeiger und Nicht-Zeiger innerhalb einer Zeile deklarieren (und womöglich auch noch initialisieren) wollen, sollten sie die Zeile wie folgt abändern:

int *a, b;

Nun sind die Unterschiede von Variable 'a' und 'b' in jedem Fall deutlicher. 

Dereferenzierung

Fassen wir noch einmal kurz zusammen: Was macht ein Zeiger genau? Er bietet die Möglichkeit eine beliebige Adresse zu halten, wobei sich jene von den Variablen mit Hilfe des Adressoperators '&' ermitteln lassen. Nun gibt es selbstverständlich auch eine Möglichkeit mit einem Zeiger den Wert der Adresse zu ändern. Dazu benötigt man den Dereferenzierungsoperator '*'. Ich werde dies einmal an folgendem Code verdeutlichen:

void main()			//1
{				//2
  int a = 0, *pa = &a;		//3: a hält den Wert 0, pa die Adresse von a
  *pa = -1;			//4: a erhält nun den Wert -1;
  a = 0;			//5: a wird wieder zurückgesetzt
  int b = *pa;			//6: b erhält nun den Wert von a also 0
}				//7

Das heißt also: Stellt man dem Zeiger den Derefernzierungsoperator voran, so kann man für den Wert, an der Adresse auf die der Zeiger zeigt, sowohl Schreib- (Zeile 4) als auch Lesezugriff (Zeile 6) erlangen.

Da jeder Zeiger, egal von welchem Datentyp, immer eine Adresse hält, also somit eine Ganzzahl, muss für jeden Zeiger gleichviel Speicher (bei WIN32) nämlich genau 32 Bit also 4 Byte reserviert werden. Also muss man für ein 'char'-Zeiger genauso viel Speicher, wie für ein 'double'-Zeiger, nämlich jeweils 4 Byte reservieren. 

Will man einen Zeiger bei seiner Deklaration noch nicht mit der Adresse einer Variablen initialisieren, so sollte man ihn mit null initialisieren:

int *p = 0;

Sobald ein Zeiger auf einen ungültigen Wert z.B. auf  0 (in Hexadezimalschreibweise 0x00000000) zeigt, sollte man ihn nicht dereferenzieren. Im Falle einer Nichtbeachtung stürzt das jeweilige Programm unweigerlich ab. Für Zeiger gilt ähnliches wie für Referenzen. Ihre Verwendung gewinnt erst bei Methoden an Bedeutung. Bei Zeigern aber vor allem noch bei dynamischer Speicherverwaltung / dynamischen Arrays.

Zeiger auf Zeiger

Es besteht auch die Möglichkeit einen Zeiger zu erstellen, der die Adresse eines anderen Zeigers enthält. So könnte beispielsweise ein Zeiger auf einen Zeiger zeigen, der wiederum auf eine Variable zeigt. Dazu folgendes Beispiel:

int i = 0;
int *pi, **ppi;
pi = &i;		//Zeigt auf i
ppi = &pi;		//Zeigt auf pi

Ein einfacher Zeiger der auf eine Variable zeigt unterscheidet sich von einem Zeiger, der auf einen anderen Zeiger zeigt, unterscheidet sich zunächst um einen weiteres '*'. Möchte man nun von dem Doppelzeiger ('ppi') auf die Variable ('i') zugreifen, dann muss man den Derefernzierungsoperator doppelt anwenden:

**ppi = 3;	//i: 3

Natürlich ist es nun auch möglich einen Zeiger zu erstellen, der auf einen Doppelzeiger zeigt etc. Ein solcher Zeiger muss wiederum um ein weiteres '*' bei der Deklaration ergänzt werden.

const-Zeiger

Wie auch bei einfachen Variablen lässt sich auch für Zeiger das Schlüsselwort 'const' verwenden. Ein einfacher Zeiger, der mit dem Schlüsselwort 'const' deklariert wird, lässt sich nach einmaliger Initialisierung in seinem Wert nicht mehr verändern:

int i = 0, j = 0;
int * const pi = &i;
*pi = 1;
//pi = &j;		//C2166: L-Wert gibt ein konstantes Objekt an

Der Zeiger 'pi' hat im Prinzip jetzt genau die gleichen Einschränkungen, wie eine Referenz (einmal initialisiert, lässt sich der Verweis also die Adresse nicht mehr ändern). Möchte man allerdings, dass nicht der Wert des Zeigers unveränderlich ist, sondern vielmehr der Speicherbereich auf den der Zeiger zeigt, dann muss das Schlüsselwort wie folgt verwendet werden:

int i = 0, j = 0;
int const *pi = &i;	//oder const int *pi = &i
//*pi = 1;		//C2166: L-Wert gibt ein konstantes Objekt an
pi = &j;		

Das Schlüsselwort 'const' ist von links nach rechts zu lesen. Steht bei der Deklaration des Zeigers rechts vom Schlüsselwort 'const' noch 'pi', dann ist der Wert von 'pi' konstant. Steht hingegen rechts von 'const' noch '*pi', dann ist der Wert von '*pi' konstant. Diese Faustregel lässt sich auch für Doppel- und Dreifachzeiger verwenden:

int i = 0;
int *pi = &i;
const int* pi1 = &i;
int const ** ppi1 = &pi1;	//**ppi1 ist konstant
int * const * ppi2 = &pi;	//*ppi2 ist konstant
int ** const ppi3 = &pi;	//ppi3 ist konstant

Das Schlüsselwort 'const' lässt sich auch mehrmals verwenden (bei einfachen Zeigern bis zu zweimal, bei Doppelzeigern bis zu dreimal, etc). Dazu folgendes Beispiel eines Einfachzeigers:

int i = 0, j = 0;
int const * const pi = &i;	//*pi und pi konstant
pi = &j;			//C2166: L-Wert gibt ein konstantes Objekt an
*pi = 1;			//C2166: L-Wert gibt ein konstantes Objekt an

Der Zeiger hat im Prinzip nun die gleichen Beschränkungen wie eine konstante Referenz. Weder Verweis, noch die Variable lassen sich nach einmaliger Initialisierung ändern.

Für die nächsten Lektionen

Sie sollten nun in der Lage sein einen Zeiger zu deklarieren, ihm die Adresse einer Variablen zuzuweisen und ihn zu dereferenzieren. Generell ist es wichtig, dass sie wissen, dass ein Zeiger eine beliebige Adresse (auch ein ungültige) als 32-Bit Wert hält.  Weitere Anwendungsmöglichkeiten von Zeigern sollten sich Ihnen in den Folgelektionen nach und nach erschließen. Bis hier ist es lediglich wichtig zu wissen, was ein Zeiger ist und wie (noch nicht unbedingt wofür) er verwendet wird. Die erste wesentliche Anwendung lernen Sie in der Folgelektion kennen: Die Arrays.

Zurück Nach oben Weiter