8. Zeiger

Zeiger gehören eigentlich zu den elementaren Datentypen.

Die bisher vorgestellten Datentypen ermöglichen es, unabhängig von Speicheradressen zu arbeiten. Es wurde nur mit dem Namen der Variablen gearbeitet und nur das Programm selber wusste, welche Speicheradressen sich hinter den Variablennamen verbergen.

Zu einer Variablen gehören folgende Dinge:
• der Name (kann, muss aber nicht!),
• die Speicheradresse, an der der Wert der Variablen gespeichert ist,
• der Inhalt der Variable, also der Wert an der Speicheradresse und
• der Datentyp, um den Inhalt zu interpretieren und um die Anzahl der Speicherzellen zu bestimmen.

Beispiel:

 Name  Adresse (hex.)  Inhalt  Datentyp  Interpretation 
 Zahl1  00003D24h  00000000 00001011  short  11
 00003D25h
 Zahl2  00003D26h  00000000 00011110  short  30
 00003D27h
 Zeichen  00003D28h  01000010  char  'B'

Die Adressen in diesem Beispiel sind frei erfunden; jede Ähnlichkeit mit vorhandenen Speicheradressen ist rein zufällig! :-)

Jede Speicheradresse kann 1 Byte = 8 Bits aufnehmen. Die beiden Zahlen im Beispiel sind short-Zahlen (16 Bit-Zahlen) und belegen daher 2 Speicheradressen. Dagegen hat die char-Variable nur 8 Bits und belegt damit auch nur eine Speicheradresse.

8.1. Zeiger und Adressen

In C/C++ wird an vielen Stellen mit Zeigern (im engl. Pointer) gearbeitet, um unabhängig von Variablennamen auf die Inhalte der Variablen zugreifen zu können. Ein Zeiger "zeigt" auf eine Variable, d.h. ein Zeiger ist eine Zahlenvariable und beinhaltet als Wert die Adresse der Variablen, auf die sie zeigt. Dadurch benötigen Zeiger immer den gleichen Speicherplatz (z.B. 32 Bit unter einem 32Bit-Betriebssystem).

Bei der Deklaration/Definition von Zeigern wird der Datentyp angegeben, auf den der Zeiger zeigen soll. Vor den Variablennamen von Zeigern wird ein Sternchen (*) gesetzt.

Beispiel:

int *ip;

Diese Zeile bewirkt, dass ein Zeiger mit dem Namen ip deklariert und definiert wird. Dieser Zeiger zeigt jetzt grundsätzlich auf Variablen vom Typ int. Diese Datentypangabe ist wichtig, da mit ihr nicht nur auf die angegebene Speicheradresse, sondern auch noch auf die nächsten 3 zugegriffen wird (weil eine int-Zahl 32 Bits = 4 Bytes = 4 Speicheradressen beinhaltet). Im nächsten Beispiel wird mit einem Zeiger auf eine int-Variable gezeigt.

Beispiel:

int i;     /* Deklaration der int-Zahl i */
int *ip;   /* Deklaration des Zeigers ip */

ip = &i;   /* Speicheradresse von i in ip speichern */
i = 99;
*ip = 100; /* weist i die Zahl 100 zu, da ip auf i zeigt */

Der unäre Adressoperator & liefert die Speicheradresse eines Objekts bzw. einer Variablen. Es wird in der Literatur manchmal auch fälschlicherweise von einer Referenz auf das Objekt bzw. vom Referenzieren gesprochen. Unter dem Begriff Referenz wird jedoch in C++ etwas anderes verstanden!

Im Beispiel wird also die Speicheradresse der Variable i dem Zeiger ip zugewiesen. Danach wird der Variable i der Wert 99 zugewiesen. In der letzten Zeile wird dem Inhalt der Variablen, auf die der Zeiger ip zeigt, der Wert 100 zugewiesen. Da der Zeiger auf die Variable i zeigt, hat i anschließend den Wert 100. Dies wird durch den unären Variablenoperator * erreicht. Es wird auch fälschlicherweise vom Dereferenzieren (siehe oben) gesprochen. Im Speicher sieht es nach dem Ablauf des Programms folgendermaßen aus (die Speicheradressen sind wieder rein zufällig gewählt):

 Name  Adresse (hex.)  Inhalt  Datentyp
 i  00004711h  100  int
 00004712h
 00004713h
 00004714h
 ip  00004715h  00004711h  int*
 00004716h
 00004717h
 00004718h

Hier noch einmal eine Tabelle mit verschiedenen Ausdrücken und die im Beispiel resultierenden Werte:

 Ausdruck  Ergebnis
 i  100
 &i  00004711h
 ip  00004711h
 *ip  100
 &ip  00004715h

Der Variablenoperator liefert aber nicht nur den Zugriff auf den Wert der Variable, auf die er zeigt, sondern er liefert die komplette Variable (daher auch der Begriff Variablenoperator)! D.h. es kann auch wieder auf die Adresse der Variablen zugegriffen werden, wie das folgende Beispiel zeigt.

Beispiel:

kap08_01.c

01 #include <stdio.h>
02
03 int main()
04 {
05    int i = 5;
06    int *ip = &i;
07
08    printf("    ip = %p\n", ip);     /* Liefert  */
09    printf("  &*ip = %p\n", &*ip);   /* immer    */
10    printf("  *&ip = %p\n", *&ip);   /* die      */
11    printf("&*&*ip = %p\n", &*&*ip); /* gleiche  */
12    printf("*&*&ip = %p\n", *&*&ip); /* Adresse! */
13
14    return 0;
15 }

Dabei ist zu sehen, dass sich Adressoperator und Variablenoperator aufheben, egal ob erst der Adressoperator und dann der Variablenoperator steht oder umgekehrt; in allen Fällen wird die gleiche Adresse - nämlich die Speicheradresse der Variablen i - ausgegeben.

8.2. Der Zeigerwert NULL

Es gibt einen speziellen Zeigerwert, nämlich den Wert NULL (definiert in der Headerdatei stdio.h). Dieser Zeigerwert zeigt nicht irgendwo hin, sondern definitiv auf "nichts". Die meisten Compiler definieren den NULL-Zeiger als einen Zeiger, der auf die Speicheradresse 00000000h zeigt, also auf die allererste Speicheradresse im Arbeitsspeicher. Dieser Zeigerwert kann abgefragt werden, z.B.

if (ip == NULL) ....

Genauso wie die "normalen" Variablen haben Zeiger nach der Definition einen unbekannten Wert, d.h. sie "zeigen" irgendwo hin. Um dies zu vermeiden, sollten Zeiger immer mit dem Zeigerwert NULL initialisiert werden.

8.3. Typprüfung

In C wird - im Gegensatz zu C++ und anderen Programmiersprachen - der Typ eines Zeigers nicht geprüft. Es ist also möglich, einen int-Zeiger plötzlich auf ein char zeigen zu lassen.

Wird mal ein Zeiger benötigt, bei dem noch nicht feststeht, auf welchen Datentypen er zeigen soll, muss ein Zeiger auf void verwendet werden.

Beispiel:

int *ip = NULL;  /* Zeiger auf int */
char *cp = NULL; /* Zeiger auf char */
void *vp;        /* Zeiger auf void */

vp = ip;  /* korrekt */
vp = cp;  /* korrekt */
/* umgekehrt: auch korrekt (aber nur in C!) */
ip = vp;  /* nur in C korrekt */
cp = vp;  /* nur in C korrekt */

In C++ können die umgekehrten Zuweisungen nur mit einer expliziten Typumwandlung durchgeführt werden. Dies sollte aber nicht ohne wichtigen Grund gemacht werden, weil damit die Typkontrolle des C++-Compilers umgangen wird und somit eine weitere Fehlerquelle gegeben ist.

Beispiel:

int *ip = NULL;  /* Zeiger auf int */
void *vp = NULL; /* Zeiger auf void */
ip = (int *) vp; /* jetzt auch in C++ korrekt! */

8.4. Zeiger-Arithmetik

Mit Zeiger-Arithmetik wird die Addition und Subtraktion von Zeigern mit ganzen Zahlen bezeichnet. Auch lassen sich zwei Zeiger subtrahieren - vorausgesetzt, sie zeigen auf den gleichen Datentypen.

Beispiel:

int i1 = 5, i2 = 7;
int *ip1 = &i1, *ip2 = &i2;

ip2 = ip1 + 1;  /* erlaubt */
ip1 = ip2 - 1;  /* erlaubt */
ip2 = 1 + ip1;  /* erlaubt */
ip1 = 1 - ip2;  /* Fehler! */
i1 = ip1 + ip2; /* Fehler! */
i2 = ip2 - ip1; /* erlaubt */

Alle anderen Operationen wie Multiplizieren usw. dürfen nicht auf Zeiger angewendet werden.

Dabei wird bei der Zeiger-Arithmetik nicht in Bytes sondern in Anzahl von Elementen des Datentyps, auf den der Zeiger zeigt, gerechnet. Wird also zu einem int-Zeiger eine 1 addiert, wird nicht auf die nächste Speicheradresse, sondern auf den nächsten int gezeigt.

Beispiel:

int i = 100;
int *ip1 = &i;
int *ip2 = ip1 + 1;

In diesem Beispiel zeigt der Zeiger ip2 nach der Initialisierung (ip1 + 1; also 00004711h + 1) nicht auf die Adresse 00004712h sondern ein Element (also ein int) weiter auf die Adresse 00004715h.

 Name  Adresse (hex.)  Inhalt  Datentyp
 i  00004711h  100  int
 00004712h
 00004713h
 00004714h
 ip1  00004715h  00004711h  int*
 00004716h
 00004717h
 00004718h
 ip2 = ip1 + 1  00004719h  00004715h  int*
 0000471ah
 0000471bh
 0000471ch

Entsprechend liefert die Differenz von zwei Zeigern nicht die Anzahl der dazwischen liegenden Bytes, sondern die Anzahl der dazwischen liegenden Datenelementen.

Bei einem void-Zeiger steht der Datentyp, auf den der Zeiger zeigt, nicht fest. Es wird hier in Bytes gerechnet. Wenn also im obigen Beispiel die Zeiger ip1 und ip2 jeweils ein Zeiger auf void wäre, würde ip2 auf die Adresse 00004712h zeigen.

void *ip1 = &i;
void *ip2 = ip1 + 1; /* zeigt so auf 00004712h! */

Hinweis:
Bei der Zeigerarithmetik muss darauf geachtet werden, dass der resultierende Zeiger auf eine Adresse zeigt, die innerhalb des Programmsegments oder innerhalb eines reservierten Speicherbereichs liegt. Ansonsten kommt es schnell zu einem Speicherzugriffsfehler!

8.5. Zeiger und Arrays

Zeiger und Arrays haben vieles gemeinsam. Tatsächlich setzen die meisten Compiler alle Array-Befehle in Zeiger-Befehle um. Der Name eines Arrays (also der Variablenname) ohne eckigen Klammern und Indexangaben gibt die Startadresse des Arrays zurück, d.h. die Adresse des ersten Elements. Er ist damit wie ein Zeiger einsetzbar. Im Gegensatz zu einem richtigen Zeiger kann ihm allerdings keine andere Adresse zugeordnet werden; er ist kein L-Wert (siehe Kapitel Variablen Abschnitt L-Werte und R-Werte).

Beispiel:

int *IntZeiger = NULL; /* Zeiger auf int */
int IntArray[5];       /* Array von int */

IntZeiger = IntArray;  /* Zeiger auf Array-Startadresse */
IntArray[0] = 5;       /* ist identisch mit folgender Zeile */
*IntZeiger = 5;


Es kann aber nicht nur auf die Startadresse des Arrays mit Zeigern zugegriffen werden, sondern auch auf jedes andere Element des Arrays. Dazu wird die sogenannte Zeiger-Arithmetik verwendet. Der Begriff Zeiger-Arithmetik bedeutet das Addieren und Subtrahieren von ganzen Zahlen zu bzw. von einem Zeiger unter Berücksichtigung des Speicherbedarfs des Datentyps, auf den der Zeiger zeigt. Zum besseren Verständnis wird das obige Beispiel erweitert.

Beispiel:

int *IntZeiger = NULL; /* Zeiger auf int */
int IntArray[5];       /* Array von int */

IntZeiger = IntArray;  /* Zeiger auf Array-Startadresse */
IntArray[0] = 5;       /* ist identisch mit folgender Zeile */
*IntZeiger = 5;
IntArray[1] = 4;       /* ist identisch mit folgender Zeile */
*(IntZeiger + 1) = 4;


In der letzen Zeile wird zu dem Wert des Zeigers noch eine 1 dazuaddiert. Damit zeigt dieser Zeiger nicht auf die nächste Speicheradresse, sondern auf das nächste Element im Array (alle Elemente eines Arrays - auch mehrdimensionale - liegen direkt hintereinander im Speicher). Im Beispiel liegt diese Adresse nicht eine sondern 4 Speicheradressen weiter, da der Datentyp int einen Speicherbedarf von 4 Byte hat. Das bedeutet, dass bei der Zeiger-Arithmetik der Speicherbedarf des Datentyps, auf den der Zeiger zeigt, bei der Addition und Subtraktion berücksichtigt wird.

 Array  Zeiger  Adresse (hex.)  Inhalt
 IntArray[0] *IntZeiger -->  00004711h  5
 00004712h
 00004713h
 00004714h
 IntArray[1] *(IntZeiger + 1) -->  00004715h  4
 00004716h
 00004717h
 00004718h

Mit Hilfe des Inkrement-Operators (++) kann diese letzte Zeile des obigen Beispiels auch folgendermaßen geschrieben werden:

*(++IntZeiger) = 4;

Allerdings zeigt jetzt der Zeiger nicht mehr auf die Startadresse des Arrays, da das Inkrementieren den Zeiger selbst verändert hat. Im Beispiel zeigt der Zeiger nun nicht auf die Adresse 00004712h sondern auf die Adresse 00004715h.

8.6. Zeiger und Zeichenketten

Da Zeichenketten ein Spezialfall von Arrays sind, gilt der ganze vorige Abschnitt auch für Zeichenketten. Hier sollen noch einige Beispiele zur Verwendung von Zeigern und Zeichenketten zur Verdeutlichung vorgestellt werden.

Beispiel:

/* Textlänge ermitteln: */
char Text[] = "Dies ist ein Textarray!";
char *TextZeiger = Text;
int TextLaenge = 0;

while (*TextZeiger++)
   TextLaenge++;
printf("Textlänge: %i",TextLaenge);


In der Bedingung der while-Schleife wird das erste Zeichen in der Zeichenkette (dahin zeigt der Textzeiger) geprüft. Ist dieses wahr (also ungleich Null und damit ungleich dem Nullzeichen für Textende), wird die Schleife ausgeführt. Zuvor wird noch der Zeiger auf das nächste Zeichen gesetzt (also der Zeiger inkrementiert). In der Schleife wird die Variable TextLaenge um eins erhöht. Nun wird in der Bedingung das zweite Zeichen geprüft usw. Ist das Textende erreicht, ist das Zeichen, auf das der Zeiger zeigt, das Nullzeichen, das den ASCII-Wert Null hat. Null bedeutet aber auch falsch, womit die while-Schleife nun abgebrochen wird. In der Variablen TextLaenge steht damit die Anzahl der Zeichen in der Zeichenkette.

Beispiel:

/* Text kopieren: */
char Text1[] = "Dieser Text soll kopiert werden!";
char Text2[35],*TextZeiger1 = Text1,*TextZeiger2 = Text2;

while (*TextZeiger2++ = *TextZeiger1++)
   ;
printf("kopierter Text: %s",Text2);


In dieser Schleife wird eine Zeichenkette kopiert. Als Bedingung wird die Zuweisung des ersten Zeichens der Originalzeichenkette Text1 nach Text2 durchgeführt. Anschließend werden beide Zeiger ums eins erhöht; sie zeigen damit auf das nächste Zeichen. Das Ergebnis der Zuweisung ist das kopierte Zeichen selber und wird jetzt als Bedingung geprüft. Ist es wahr (also ungleich Null und damit ungleich dem Nullzeichen), wird die Schleife durchgeführt (Leeranweisung). Dann wird das nächste Zeichen kopiert usw. Ist das Textende erreicht, ist das Zeichen, auf das der Zeiger zeigt, das Nullzeichen. Auch dieses wird kopiert. Dann werden die Zeiger ums eins erhöht und das kopierte Zeichen geprüft. Das Nullzeichen hat den ASCII-Wert Null und dadurch ist die Bedingung falsch; die Schleife wird abgebrochen. Das Beispiel kopiert also den vollständigen Text einschließlich des Nullzeichens; die Zeiger zeigen anschließend auf das erste Zeichen nach dem Nullzeichen!

8.7. Zeiger und Strukturen

Mit Zeigern kann auf alle Objekte gezeigt werden, so auch auf Strukturen (struct). Dabei verändert sich die Schreibweise, wenn mit Zeigern auf Felder einer Struktur zugegriffen wird. Anstelle des Punktes zwischen Strukturnamen und Feld wird nun ein Pfeil - bestehend aus Minuszeichen und Größer-als-Zeichen (->) - verwendet. Das folgende Beispiel verdeutlicht dieses.

Beispiel:

struct Buch
{
   char Titel[100];
   char Autor[100];
   char ISBN[20];
   char Standort[10];
   float Preis;
} Buecher[50];     /* 50mal struct Buch */
struct Buch *BuchZeiger;

BuchZeiger = Buecher;       /* zeigt auf's 1. Array-Element */
Buecher[0].Preis = 9.99;    /* ist identisch mit */
(*BuchZeiger).Preis = 9.99; /* ist identisch mit */
BuchZeiger->Preis = 9.99;


Hier gilt es, ganz genau hinzusehen und einen Teilausdruck nach dem anderen auszuwerten, um zu verstehen, was tatsächlich passiert. Ansonsten ändert sich beim Umgang mit den Strukturen gar nichts.

8.8. Unveränderbare Zeiger

Ein Zeiger kann unveränderbar sein (d.h. er kann auf keine andere Adresse zeigen) oder auf eine unveränderbare Variable zeigen - oder beides; je nachdem, an welcher Stelle das Schlüsselwort const verwendet wird.

Beispiel:

int       i1 = 5;
int const i2 = 3;

// veränderbarer Zeiger auf veränderbare Variable:
int       *       ip1 = &i1;

// veränderbarer Zeiger auf unveränderbare Variable:
int const *       ip2 = &i2;

// unveränderbarer Zeiger auf veränderbare Variable:
int       * const ip3 = &i1;

// unveränderbarer Zeiger auf unveränderbare Variable:
int const * const ip4 = &i2;

*(ip1++); // erlaubt!
(*ip1)++; // erlaubt!
*(ip2++); // erlaubt!
(*ip2)++; // Fehler - unveränderbare Variable!
*(ip3++); // Fehler - unveränderbarer Zeiger!
(*ip3)++; // erlaubt!
*(ip4++); // Fehler - unveränderbarer Zeiger!
(*ip4)++; // Fehler - unveränderbare Variable!

Dabei muss eine Variable, auf die ein Zeiger auf unveränderbare Variable (z.B. int const *) zeigt, nicht unbedingt unveränderbar sein. Dies mag vielleicht unsinnig erscheinen, kann aber durchaus sinnvoll sein.

Beispiel:

int i = 5;     // veränderbare Variable
int *ip1 = &i; // Zeiger auf veränderbare Variable
int const * ip2 = &i; // Zeiger auf
                      // unveränderbare Variable

i = 7;     // ok
*ip1 = 9;  // ok
*ip2 = 11; // Fehler, da ip2 auf
           // unveränderbare Variable zeigt!

Obwohl alle drei Zuweisungen auf die gleiche (veränderbare) Speicheradresse zugreifen, hat der Zeiger ip2 keine Veränderungsberechtigung. Man sagt auch: Der Zeiger ip1 bietet eine veränderbare Ansicht (im engl: non-constant view) und der Zeiger ip2 eine unveränderbare Ansicht (im engl. constant view).

Anders herum funktioniert es übrigens nicht: Ein Zeiger auf eine veränderbare Variable kann nicht auf eine unveränderbare Variable zeigen.

Beispiel:

int const i = 5;
int * ip = &i; // Fehler, da i unveränderbar ist!

8.9. Zeiger auf Zeiger

Ein Zeiger kann auch auf einen anderen Zeiger zeigen.

Beispiel:

kap08_02.c

01 #include <stdio.h>
02
03 int main()
04 {
05    int Wert = 1234;
06    int *Zeiger_auf_Wert = &Wert;
07    int **Zeiger_auf_Zeiger = &Zeiger_auf_Wert;
08
09    printf("  Wert              = %i\n", Wert);
10    printf(" *Zeiger_auf_Wert   = %i\n", *Zeiger_auf_Wert);
11    printf("**Zeiger_auf_Zeiger = %i\n", **Zeiger_auf_Zeiger);
12
13    return 0;
14 }

Ein Zeiger auf einen Zeiger wird durch einen doppelten Variablenoperator (**) geschaffen. Diesem Zeiger wird die Adresse eines anderen Zeigers zugewiesen, dem wiederum die Adresse einer Variablen zugewiesen wurde. Das angegebene Beispiel gibt folgendes aus:

  Wert              = 1234
 *Zeiger_auf_Wert   = 1234
**Zeiger_auf_Zeiger = 1234


Dies kann beliebig fortgesetzt werden: Es wäre also möglich, einen Zeiger auf Zeiger auf Zeiger auf Zeiger usw. zu erzeugen, aber es wird keine sinnvolle Anwendung dafür geben.



Voriges Kapitel: 7. Strukturierte Datentypen