10.3 Zugriff auf den Inhalt von Zeigern
Sie verwenden den Indirektionsoperator *, um mit einem Zeiger direkt auf die Werte eines Speicherobjekts zuzugreifen, das Sie zuvor mit dem Adressoperator referenziert haben. Der direkte Zugriff auf den Wert eines Speicherobjekts mit dem Indirektionsoperator (oder auch Verweisoperator) wird häufig auch als Dereferenzierung bezeichnet. Sie können damit über einen Zeiger die Werte, auf die er verweist, nicht nur auslesen, sondern auch ändern. Hier ein einfaches Beispiel:
00 // Kapitel10/dereferenzierung.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(void) {
04 int *iptr;
05 int ival = 255;
06 iptr = &ival;
07 // Wert ausgeben
08 printf("*iptr : %d\n", *iptr);
09 printf(" ival : %d\n", ival);
10 // ival neuen Wert zuweisen
11 *iptr = 128;
12 // Wert ausgeben
13 printf("*iptr : %d\n", *iptr);
14 printf(" ival : %d\n", ival);
15 return EXIT_SUCCESS;
16 }
Das Programm gibt bei der Ausführung Folgendes aus:
*iptr : 255
ival : 255
*iptr : 128
ival : 128
In Zeile (08) wurde der Indirektionsoperator * mit dem Zeiger iptr verwendet. Im Gegensatz zu iptr ohne Indirektionsoperator, der die Adresse des Speicherobjekts ival enthält, können Sie mit dem Indirektionsoperator direkt auf den Wert von ival zugreifen. In Zeile (11) wurde daher zum Beweis ein neuer Wert an ival über den Zeiger iptr zugewiesen, wie die Ausgabe in den Zeilen (13) und (14) zeigt. Das bedeutet also, dass alle gültigen Integer-Werte, die Sie über den Indirektionsoperator * und den Zeiger iptr zuweisen, eigentlich die Variable ival betreffen. Abbildung 10.2 zeigt den Vorgang nochmals bildlich.
Stern bei der Zeigerdeklaration und dem Indirektionsoperator
An dieser Stelle soll ein häufiger Irrtum beseitigt werden. Der Stern bei der Deklaration eines Zeigers ist nicht mit dem Indirektionsoperator für die Dereferenzierung zu verwechseln, der auch die Form eines Sterns hat und eine doppelt indirekte Adressierung im Maschinencode erzeugt. Genau deshalb haben wir auch in den vorangehenden Kapiteln so deutlich Deklaration und Initialisierung voneinander getrennt.
Dasselbe funktioniert natürlich auch anders herum. Sie können einer normalen Variable über den Indirektionsoperator den Wert einer Variable zuweisen, deren Adresse der Zeiger enthält. Es folgt ein einfaches Listing hierzu:
00 // Kapitel10/dereferenzierung_2.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(void) {
04 float *fptr;
05 float fval1 = 123.123f, fval2;
06 // fptr die Adresse von fval1 übergeben
07 fptr =& fval1;
08 // fval2 erhält den Wert von fval1
09 fval2 = *fptr;
10 printf("fval2: %.3f\n", fval2);
11 return EXIT_SUCCESS;
12 }
In Zeile (07) erhält der Zeiger fptr die Adresse von fval1. In Zeile (09) wird der Wert dieser Variable über den Zeiger fptr mithilfe des Indirektionsoperators der Variable fval2 zugewiesen. Nun zeigen fval1 und fval2 auf dieselbe Adresse! Das heißt natürlich auch, dass wenn Sie den Wert von fval1 ändern, sich damit gleichzeitig der Wert von fval2 ändert. Probieren Sie dies ruhig aus, und Sie sehen, dass Zeiger wirklich nur Verweise in der Variablentabelle sind.
10.3.1 Der NULL-Zeiger
Den Indirektionsoperator dürfen Sie natürlich nur verwenden, wenn der Zeiger eine gültige Adresse enthält. Wurde dem Zeiger keine gültige Adresse zugewiesen und wird der Indirektionsoperator trotzdem verwendet, wird das Programm vermutlich aufgrund einer Speicherzugriffsverletzung (segmentation fault) abstürzen oder im schlimmsten Fall sogar weiterlaufen und unvorhergesehene Dinge auslösen.
In der Praxis können Sie viele Fehler vermeiden, wenn Sie einen (vorerst) nicht verwendeten Zeiger zunächst mit NULL belegen, also gleich bei der Definition initialisieren und einen Zeiger vor jeder Verwendung überprüfen. NULL ist ein Zeiger, der keine gültige Adresse verwendet, sondern meist einfach auf die Adresse 0 zeigt. Globale Zeiger oder Zeiger, die mit static gekennzeichnet wurden, werden automatisch mit dem NULL-Zeiger initialisiert. Lokale Zeiger in einem Anweisungsblock enthalten dagegen ein beliebiges und somit undefiniertes Bitmuster.
Es folgt ein Beispiel mit dem NULL-Zeiger. Mit diesem können Sie eine Speicherzugriffsverletzung vermeiden:
00 // Kapitel10/null_zeiger.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(void) {
04 int *iptr = NULL; // Zeiger mit NULL initialisiert
05 // ... mehr Code
06 if( iptr == NULL ) {
07 printf("Zeiger hat keine gueltige Adresse\n");
08 return EXIT_FAILURE;
09 }
10 // iptr hat eine gültige Adresse ...
11 return EXIT_SUCCESS;
12 }
In Zeile (04) wurde dem Zeiger iptr bei der Initialisierung gleich der NULL-Zeiger zugewiesen. Einen Zeiger sollten Sie immer auf eine gültige Adresse hin prüfen. Dies geschieht in Zeile (06). Bei einem Fehler wird das Programm beendet.
Der NULL-Zeiger
Der NULL-Zeiger ist ein vordefinierter Zeiger, für gewöhnlich mit einem Wert auf die Adresse 0. Diese Adresse gibt es meist nicht, und deshalb ist sie für Zeiger reserviert, die einfach nichts enthalten. In C werden dagegen Variablen und Funktionen immer an bestimmten Speicherplätzen abgelegt, deren Adresse nicht 0 ist. Daher ist ein Zeiger auf die Adresse 0 ein Zeiger auf ein nicht definiertes Speicherobjekt. Die Konstante NULL ist in der Header-Datei stddef.h definiert.
10.3.2 Deklaration, Adressierung und Dereferenzierung von Zeigern
Sie dürften nun schon gemerkt haben, warum das Thema Zeiger etwas komplexer ist. Schwierig ist allerdings eher selten das Verständnis der Zeiger selbst, die ja letztendlich nur mit Adressen operieren, sondern die richtige Verwendung des Adressoperators und des Indirektionsoperators. Daher folgt hier eine Übersicht über den Zugriff und die Dereferenzierung von Zeigern:
int *iptr = NULL; // Zeiger mit NULL initialisiert
int ival1=0, ival2=0;
// Initialisierung: Zeiger erhält Adresse von ival1
iptr = &ival1;
// Dereferenzierung mit dem Indirektionsoperator
// ival erhält den Wert 123
*iptr = 123;
// ival2 erhält denselben Wert wie ival1
ival2 = *iptr;
// ival erhält die Summe aus ival2 + 100;
*iptr = ival2+100;
// Zeiger erhält die Adresse von ival2
iptr = &ival2;
printf("%d\n" , *iptr); // gibt Wert von ival2 aus
printf("%p\n" , iptr); // gibt die Adresse von ival2 aus
printf("%p\n" , &ival2); // gibt die Adresse von ival2 aus
printf("%p\n" , &iptr); // gibt die Adresse von iptr aus