In diesem Kapitel erfahren Sie etwas über das vielleicht wichtigste und vielseitigste Thema in der Programmiersprache C. Dabei handelt es sich allerdings auch um ein Thema, welches oft auf Anhieb nicht gleich verstanden wird. Aber ist einem Zeigerarithmetik erst mal klar, erscheinen die Hürden der Fortgeschrittenen Kapitel nicht mehr so hoch. C C++ C/C++ Zeiger Pointer void Arrays Zeigerarithmetik Derefernzierung call by reference Zeiger - Pointer void Arrays Zeigerarithmetik Derefernzierung call by reference Kapitel 15: Zeiger (Pointer)

In diesem Kapitel geht es um das vielleicht wichtigste und vielseitigste Thema in der Programmiersprache C. Dabei handelt es sich allerdings um ein Thema, das oft nicht auf Anhieb verstanden wird. Aber ist die Zeigerarithmetik erst einmal klar, erscheinen die Hürden der fortgeschrittenen Kapitel nicht mehr so hoch. Also: Ist das Thema Zeiger kein Problem mehr, ist es auch die Programmiersprache C nicht mehr.

Im Grunde sind Zeiger aber gar nicht so kompliziert, wie diese oft dargestellt werden. Zeiger sind im Prinzip nichts anderes als ganz normale Variablen, die statt Datenobjekten wie Zahlen, Zeichen oder Strukturen eben Adressen eines bestimmten Speicherbereichs beinhalten.

Es stellt sich die Frage, was Sie mit Zeigern auf Adressen so alles machen können. Hierzu ein kleiner Überblick der Anwendungsgebiete von Zeigern:

Auf den nächsten Seiten werden erst einmal die Grundlagen der Zeiger (häufig auch Pointer genannt) durchgearbeitet. Im Laufe des Buchs werden dann die zuvor genannten Punkte besprochen. Ich empfehle Ihnen, sich für das vorliegende Kapitel viel Zeit zu nehmen. Es stellt auf jeden Fall die Grundlage für den Fortgang des Buchs und Ihre Karriere als C-Programmierer dar.

15.1. Zeiger deklarieren            zurück  Ein Kapitel tiefer  zum Inhaltsverzeichnis

Die Deklaration eines Zeigers hat die folgende Syntax:

Datentyp *zeigervariable; 

Der Datentyp des Zeigers muss vom selben Datentyp wie der sein, auf den er zeigt (referenziert).

Hinweis
 

Wenn ich im Weiteren von "auf etwas zeigen" spreche, ist damit natürlich gemeint, dass auf einen bestimmten Speicherbereich (eine Adresse im Arbeitsspeicher) referenziert wird.

Das Sternchen vor zeigervariable kennzeichnet den Datentyp als Zeiger. Im Fachjargon heißt dieser Operator Indirektionsoperator. Die Position für das Sternchen befindet sich zwischen dem Datentyp und dem Zeigernamen. Beispiel:

int *zeiger1;
int* zeiger2;
char *zeiger3;
char* zeiger4;
float *zeiger5;

Hier sehen Sie zwei verschiedene Schreibweisen, wobei beide richtig sind; es hat sich aber folgende eingebürgert:

int *zeiger1;
int *zeiger2;
int *zeiger3;

Mit dieser Schreibweise wird der gemachte Fehler deutlicher:

int *zeiger1 ,zeiger2;

Hier wurde nur ein Zeiger deklariert, was auch recht schnell zu sehen ist. Bei der folgenden Schreibweise ist dieser Fehler nicht mehr so eindeutig zu erkennen:

int* zeiger1 ,zeiger2;

Hier könnte man fälschlicherweise annehmen, es seien zwei Zeiger deklariert worden. Am besten verwenden Sie also die übliche Schreibweise. Damit können Sie sich einige Probleme ersparen. Das Sternchen (*) wird Dereferenzierungsoperator genannt.

15.2. Zeiger initialisieren            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

Hier beginnt eine gefährliche Operation. Wird im Programm ein Zeiger verwendet, der zuvor nicht initialisiert wurde, kann dies zu schwerwiegenden Fehlern führen - gar bis zum Absturz eines Betriebssystems (bei 16-Bit-Systemen). Die Gefahr ist, dass bei einem Zeiger, der nicht mit einer gültigen Adresse initialisiert wurde und auf den jetzt zurückgegriffen werden soll, stattdessen einfach auf irgendeine Adresse im Arbeitsspeicher zurückgriffen wird. Wenn sich in diesem Speicherbereich wichtige Daten oder Programme bei der Ausführung befinden, kommt es logischerweise zu Problemen.

Um das Prinzip der Zeiger zu verstehen, müssen Sie nochmals zurück zu den normalen Datentypen springen. Beispielsweise zu folgender Initialisierung:

int x=5; 

Durch diese Initialisierung ergibt sich im Arbeitsspeicher folgendes Bild:

Abbildung 15.1: Darstellung einer Variablen im Arbeitsspeicher
Abbildung 15.1: Darstellung einer Variablen im Arbeitsspeicher

Die Adresse ist eine erfundene Adresse im Arbeitsspeicher, auf die Sie keinen Einfluss haben. Diese wird vom System beim Start des Programms vergeben. Damit der Rechner weiß, von wo er den Wert einer Variablen auslesen soll, wird eine Adresse benötigt. Ebenso sieht es mit der Initialisierung einer Variablen aus, falls dieser ein Wert zugewiesen wird. Der Name einer Variablen ist der Name, den Sie bei der Deklaration selbst festgelegt haben. Der Wert 5 wurde zu Beginn des Programms definiert. Dieser Block oben hat eine Speichergröße von vier Bytes (int = vier Byte oder, auf 16-Bit-Systemen, zwei Bytes).

Hundertprozentig stimmt diese Analyse eines Datentyps nicht. Es gibt noch einige weitere Attribute, die ein Datentyp besitzt. Hier die nicht genannten Attribute:

Benötigen Sie die Adresse einer Variablen im Arbeitsspeicher, dann kann diese mit dem Formatzeichen %p und dem Adressoperator & abgefragt und ausgegeben werden:

#include <stdio.h>

int main()
{
   int x=5;
   printf("Die Adresse von x ist %p \n",&x);
   return 0;
}

In diesem Beispiel wurde mithilfe des Adressoperators und dem Formatzeichen %p die aktuelle Speicheradresse der Variable x ausgegeben.

Jetzt ist auch klar, warum scanf() eine Fehlermeldung ausgibt, wenn kein Adressoperator mit angegeben wird:

scanf("%d",x);  /* Wohin damit ...??? */ 

Das wäre dasselbe, als wenn der Postbote einen Brief zustellen soll, auf dem sich keine Anschrift befindet. Der Brief wird niemals sein Ziel erreichen. Genauso läuft es in Ihrem PC ab, egal ob Sie jetzt ein Computerspiel spielen oder ein Textverarbeitungsprogramm verwenden. Jedes Speicherobjekt, das Sie definieren, hat eine Adresse, einen Namen und eine bestimmte Speichergröße (je nach Datentyp). Der Wert ist der einzige dieser vier Angaben, der zur Laufzeit festgelegt oder verändert werden kann.

Wie kann jetzt einem Zeiger die Adresse einer Variablen übergeben werden? Dies soll das folgende Beispiel demonstrieren:

#include <stdio.h>

int main()
{
   int abfrage;
   int Kapitel1 = 5;
   int Kapitel2 = 60;
   int Kapitel3 = 166;
   int Nachtrag = 233;
   int *Verzeichnis; /* Zeiger */

   do{
      printf("\tINDEXREGISTER VOM BUCH\n");
      printf("\t*******************************\n\n");
      printf("\t-1- Kapitel 1\n");
      printf("\t-2- Kapitel 2\n");
      printf("\t-3- Kapitel 3\n");
      printf("\t-4- Nachtrag\n");
      printf("\t-5- Ende\n");
      printf("\n");
      printf("\tAuswahl : ");
      scanf("%d",&abfrage);
      printf("\tKapitel %d finden Sie auf ",abfrage);

      switch(abfrage)
         {
            case 1 :  Verzeichnis=&Kapitel1;
                      printf("Seite %d\n",*Verzeichnis);
                      break;
            case 2 :  Verzeichnis=&Kapitel2;
                      printf("Seite %d\n",*Verzeichnis);
                      break;
            case 3 :  Verzeichnis=&Kapitel3;
                      printf("Seite %d\n",*Verzeichnis);
                      break;
            case 4 :  Verzeichnis=&Nachtrag;
                      printf("Seite %d\n",*Verzeichnis);
                      break;
            default:  printf("Seite ???\n");
                      break;
         }
      }while(abfrage<5);
   return 0;
}

Abbildung 15.2: Programm zur Verwendnung der Zeiger in Aktion
Abbildung 15.2: Programm zur Verwendnung der Zeiger in Aktion

Der Zeiger des Programms:

int *Verzeichnis; 

Hiermit wurde ein Zeiger deklariert mit dem Namen Verzeichnis. Bis zur switch-Verzweigung so weit nichts Neues. Aber dann in der ersten case-Anweisung:

Verzeichnis=&Kapitel1; 

Damit wird dem Zeiger Verzeichnis die Adresse der Variablen Kapitel1 übergeben. Dies können Sie am Adressoperator & erkennen, der sich vor der Variablen Kapitel1 befindet. Sollte der Adressoperator vor der Variablen Kapitel1 vergessen werden, wird der Compiler das Programm nicht übersetzen, da ein Zeiger eine Adresse und nicht den Wert einer Variablen haben will.

Zu diesem Beispiel folgt ein kleiner Ausschnitt, der verdeutlicht, was im Speicher alles geschieht:

int Kapitel1 = 5;
…
int *Verzeichnis;

Abbildung 15.3: Darstellung im Arbeitsspeicher
Abbildung 15.3: Darstellung im Arbeitsspeicher

Zunächst erfolgt die Adressübergabe der Variable Kapitel1 an den Zeiger Verzeichnis mit dem Adressoperator:

Verzeichnis=&Kapitel1;

Abbildung 15.4: Der Zeiger verweist hier auf die Adresse der Variable Kapitel1
Abbildung 15.4: Der Zeiger verweist hier auf die Adresse der Variable Kapitel1

Daran lässt sich erkennen, wie der Zeiger Verzeichnis die Adresse der Variable Kapitel1 enthält. Ein wenig anders sieht es dann hiermit aus:

printf("Seite %d\n",*Verzeichnis);

Hier kommt zum ersten Mal der Dereferenzierungsoperator (*) ins Spiel. Dieser dereferenziert den Wert der Adresse, mit welcher der Zeiger zuvor mit

Verzeichnis=&Kapitel1;

initialisiert wurde. Lassen Sie bei der Ausgabe einfach einmal den Dereferenzierungsoperator weg:

printf("Seite %d\n",Verzeichnis);   /* ohne '*'  */

Übersetzen Sie dieses Programm erneut und lassen Sie sich das Verzeichnis von Kapitel1 ausgeben. Es wird irgendeine Zahl ausgegeben, nur nicht die Zahl 5. Warum? Eine Umänderung der Zeile

printf("Seite %d\n",*Verzeichnis);

in

printf("Adressen %p %p\n",*Verzeichnis, Kapitel1);

zeigt mehr. Jetzt soll wieder das erste Kapitel bei der Abfrage verwendet werden. Danach müssten beide Male dieselben Adressen ausgegeben werden. Mit Verzeichnis=&Kapitel1 wurde doch nur die Adresse übergeben. Und im Zeiger selbst befindet sich auch nur die Adresse von Kapitel1. Ohne den Dereferenzierungsoperator ist der Zeiger hier nutzlos. Nur mit diesem können Sie auf den Inhalt einer Variablen mithilfe eines Zeigers zugreifen.

Wenn Sie den Zeiger jetzt auf Kapitel3 (Verzeichnis=&Kapitel3) verweisen lassen, ergibt sich folgender Stand im Arbeitsspeicher:

Abbildung 15.5: Zeiger verweist jetzt auf die Adresse von Kapitel3
Abbildung 15.5: Zeiger verweist jetzt auf die Adresse von Kapitel3



Merke
 

Wird der Indirektionsoperator vorangestellt, erkennen Sie, dass nicht auf den Zeiger zurückgegriffen werden soll, sondern auf das Datenobjekt, dessen Anfangsadresse sich im Zeiger befindet.

Zum besseren Verständnis folgt dazu ein weiteres Programm, welches die Verwendung von Zeigern detaillierter darstellen soll. Ein lehrreiches Beispiel, bei dem es sich lohnt, es zu studieren:

#include <stdio.h>

int main()
{
   int x=5;
   int *y;

   printf("Adresse x=%p, Wert x=%d\n",&x,x);

   /* Führt bei manchen Systemen zum Programmabsturz
      ggf. auskommentieren */
   printf("Adresse *y=%p, Wert *y=%d(unsinn)\n",&y,*y);

   printf("\ny=&x;\n\n");
   y=&x;              /*y hat jetzt die Adresse von x*/
   printf("Adresse x=%p, Wert x=%d\n",&x,x);
   printf("Adresse *y=%p, Wert *y=%d\n",&y,*y);
   printf("\nAdresse auf die y zeigt ist %p\n",y);
   printf("und das ist die Adresse von x = %p\n",&x);
   printf("\nACHTUNG!!!\n\n");
   *y=10;
   printf("*y=10\n\n");
   printf("Adresse x=%p, Wert x=%d\n",&x,x);
   printf("Adresse *y=%p, Wert *y=%d\n",&y,*y);
   printf("\nAdresse auf die y zeigt ist %p\n",y);
   printf("weiterhin die von x (%p)\n",&x);
   return 0;
}

Abbildung 15.6: Ausgabe des Programms unter Linux
Abbildung 15.6: Ausgabe des Programms unter Linux

Folgende Zeile dürfte Ihnen bei diesem Programm aufgefallen sein:

*y=10;

Hiermit wird der Wert der Variablen x dereferenziert. Mit dieser Dereferenzierung kann jederzeit auf den Wert der Variablen x zugegriffen werden. Dadurch kann mithilfe eines Zeigers der Inhalt der Variablen verändert werden, und zwar so, als würden Sie direkt darauf zugreifen. Hierzu der Verlauf bildlich:

int x=5;
int *y;

Abbildung 15.7: Speicheraddressierung der Variablen x und des Zeigers y
Abbildung 15.7: Speicheraddressierung der Variablen x und des Zeigers y

y=&x;

Abbildung 15.8: Zeiger y verweist jetzt auf die Adresse von Variable x
Abbildung 15.8: Zeiger y verweist jetzt auf die Adresse von Variable x

*y=10;

Abbildung 15.9: Dereferenzierung der Variablen x
Abbildung 15.9: Dereferenzierung der Variablen x

Somit gilt: Wenn Sie mit dem Dereferenzierungsoperator den Wert einer Variablen auslesen können, dann kann damit auch die Variable verändert werden. Das Wichtigste ist, dass Sie verstehen, dass einem Zeiger kein Wert übergeben wird, sondern eine Adresse (ich wiederhole mich), um anschließend mit dem Wert dieser Adresse zu arbeiten.

Aber Achtung, nachstehendes Programm könnte böse Folgen haben:

#include <stdio.h>

int main()
{
   int *y;
   *y=10;
   printf("Der Wert von *y ist %d\n",*y);
   return 0;
}

Dem Zeiger y wurde hier zuvor keine gültige Adresse zugewiesen. Dies bedeutet, dem Zeiger y steht beim Start des Programms eine Adresse zur Verfügung, die durch ein zufälliges Bitmuster vom Linker erzeugt wurde. Das Programm kann theoretisch sogar korrekt ablaufen. Irgendwann kann (wird) es jedoch ein vollkommen falsches Ergebnis zurückliefern. Auch könnte es sein, dass auf einen Speicherbereich zugegriffen wird, der bereits Daten beinhaltet. Dies könnte zu erheblichen Problemen bei der Programmausführung bis hin zum Absturz führen.

Solche Fehler können Sie vermeiden, indem Sie einen nicht verwendeten Zeiger mit dem Wert NULL initialisieren und vor der Verwendung des Zeigers eine Überprüfung auf NULL durchführen. Der Wert oder genauer der Zeiger NULL ist eine Konstante, welche mit dem Wert 0 definiert ist:

#define NULL (void *)0 

Einfach ausgedrückt handelt es sich bei diesem NULL-Zeiger um einen Ich-zeige-auf-keine-gültige-Adresse-Wert. Hier das Erwähnte in der Praxis:

#include <stdio.h>

int main()
{
   int *y=NULL; /* Zeiger mit NULL initialisieren */
   if(y == NULL)
      printf("Der Zeiger besitzt keine gültige Adresse\n");
   else
      *y=10;
   return 0;
}

Ein Tipp zu einer sichereren Überprüfung von:

if(y == NULL) 

Es kann dabei schnell passieren, dass Sie statt einer Überprüfung auf NULL den Zeiger mit NULL initialisieren:

if(y = NULL)  /* Fehler */

Mit folgender Überprüfung kann Ihnen dieser Fehler nicht mehr unterlaufen:

if(NULL == y) 

Denn sollten Sie NULL den Zeiger y zuweisen wollen, wird der Compiler das Programm nicht übersetzen, da dies rein syntaktisch falsch ist.

Natürlich geht dies auch umgekehrt. Sie können einer normalen Variablen auch den Wert eines Zeigers übergeben, auf den dieser zeigt. Beispiel:

#include <stdio.h>

int main()
{
   int *ptr;
   int var=10, tmp;

   /* ptr zeigt auf Adresse von var1 */
   ptr=&var;
   /* Variable tmp bekommt Wert, welchen ptr dereferenziert */
   tmp = *ptr;  /* tmp=10 */
   /* Viele Zeilen Code */
   *ptr=100;   /* Inhalt von var wird verändert var==100 */
   /* Wieder viele Zeilen später */
   if(var > 50) /* Ist var größer als 50? */
      var=tmp;   /* ...dann doch lieber wieder den alten Wert */
   printf("var=%d\t*ptr=%d\n",var, *ptr); /* var=10   *ptr=10 */
   return 0;
}

Wichtig ist allerdings dabei, dass Sie den Dereferenzierungsoperator verwenden. Denn dieser dereferenziert den Wert, auf den der Zeiger zeigt:

tmp = *ptr;  /* tmp=10 */

Sollten Sie den Dereferenzierungsoperator vergessen, lässt sich das Programm ohnehin nicht übersetzen, denn es würde ja versucht werden, der Variablen tmp eine Adresse zu übergeben.

Hier ein schneller Überblick zum Zugriff und zur Dereferenzierung von Zeigern:

/* Deklaration */
int *ptr;
int var, var2;

/* Initialisieren: ptr bekommt die Adresse von var */
ptr=&var;

/* Dereferenzierung : var bekommt den Wert 100 zugewiesen */
*ptr=100;

/* var2 mit dem selben Wert wie var initialisieren */
var2 = *ptr

*ptr+=100;     /* Dereferenzierung: var wird um 100 erhöht     */
*ptr++;        /* Dereferenzierung: var hat jetzt den Wert 201 */
*ptr--;        /* var hat wieder den Wert 200 */
ptr=&var2;     /* var bekommt den Wert von var2 (==100)*/

printf("%d",*ptr);    /* gibt Wert von var aus */
printf("%p",&ptr);    /* gibt Adresse von ptr aus */
printf("%p",ptr)      /* gibt Adresse von var aus */

15.2.1 Speichergröße von Zeigern
Ich komme bei der Speicherverwaltung zwar nochmals genauer zu diesem Thema zurück, aber es soll hier schon einmal kurz erwähnt werden. Die Größe eines Zeigers ist nicht abhängig vom Datentyp, auf den dieser verweist. Das ist ja auch schließlich nicht notwendig, denn Zeiger sollen ja keine Werte, sondern Adressen speichern. Und zur Speicherung von Adressen werden in der Regel zwei oder vier Bytes benötigt. Der Beweis:

#include <stdio.h>

int main()
{
   char   *v;
   int    *w;
   float  *x;
   double *y;

   printf("%d\t %d\t %d\t %d\n",sizeof(v),sizeof(w),
                                sizeof(x), sizeof(y));
   return 0;
}

15.3. Zeigerarithmetik            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

Folgende Rechenoperationen können mit einem Zeiger und auf deren Adresse verwendet werden:

Wenn Sie Folgendes eingeben würden:

int *ptr;
int wert;

ptr=&wert;
ptr+=10;

Auf welche Adresse zeigt der Zeiger ptr? 10 Bytes von der Variablen wert entfernt? Nein, ein Zeiger wird immer um den Wert der Größe des Datentyps erhöht bzw. erniedrigt. Auf einem 32-Bit-System würde der Zeiger auf eine Stelle verweisen, die 40 Bytes von der Anfangsadresse der Variablen wert entfernt ist.

Solch eine Erhöhung der Adresse ist ohnehin sehr gefährlich, da der Zeiger danach höchstwahrscheinlich nicht mehr auf einen reservierten Speicherbereich zeigt.

Des Weiteren sind bei Verwendung eines Zeigers natürlich auch die Vergleichsoperatoren <, ><, !=, ==, <= und => erlaubt. Die Verwendung ist aber hierbei nur sinnvoll, wenn die Zeiger auf Elemente eines Arrays zeigen.

15.4. Zeiger, die auf andere Zeiger verweisen            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

Zeiger können letztendlich auch auf andere Zeiger verweisen:

int *ptr1;
int *ptr2;

ptr1=ptr2;

In diesem Fall zeigen beide Zeiger auf dieselbe Adresse. Dies ist ein wenig verwirrend. Daher folgt ein kleines Beispiel dazu:

#include <stdio.h>

int main()
{
   int wert=10;
   int *ptr1;
   int *ptr2;

   ptr1=&wert;
   ptr2=ptr1;

   printf("Wert=%d Adresse in *ptr1=%p\n",*ptr1,ptr1);
   printf("Adresse von *ptr1=%p\n",&ptr1);

   printf("Wert=%d Adresse in *ptr2=%p\n",*ptr2,ptr2);
   printf("Adresse von *ptr2=%p\n",&ptr2);
   printf("Adresse von int wert=%p\n",&wert);
   return 0;
}

Bei diesem Programm verweisen beide Zeiger auf denselben Wert (genauer auf dieselbe Adresse), nämlich die Adresse der Variablen wert. Mit ptr2=ptr1 bekommt ptr2 dieselbe Adresse zugewiesen, auf die schon ptr1 zeigt. Auffällig ist hierbei auch, dass kein Adressoperator verwendet wird. Dieser wird nicht benötigt, da der Wert eines Zeigers schon eine Adresse ist, und ein Zeiger auch einen Wert als Adresse erwartet.

Abbildung 15.10: Ein Zeiger bekommt die Adresse eines anderen Zeigers
Abbildung 15.10: Ein Zeiger bekommt die Adresse eines anderen Zeigers

Würde jetzt

*ptr1=11;

im Programm eingefügt, würde der Wert der Variablen wert auf 11 geändert, und somit wäre die Ausgabe des Programms 11. Schreiben Sie

wert=20; 

werden auch die beiden Zeiger (mit Verwendung des Dereferenzierungsoperators) 20 ausgeben, da die Zeiger ja weiterhin auf die Speicheradresse der Variablen wert zeigen.

15.4.1 Subtraktion zweier Zeiger
In der Standard-Headerdatei <stddef.h> befindet sich ein primitiver Datentyp ptrdiff_t, welcher meist mit int deklariert ist. Dieser wird verwendet, um das Ergebnis aus der Subtraktion zweier Zeiger zurückzugeben. Diese Subtraktion wird verwendet, um zu berechnen, wie weit zwei Zeiger zum Beispiel in einem Vektorelement voneinander entfernt sind. (Näheres über den Zusammenhang von Arrays bzw. Strings und Zeigern können Sie einige Seiten weiter unten lesen.) Hier ein einfaches Listing:

#include <stdio.h>
#include <stddef.h>  /* für ptrdiff_t */

int main()
{
   char *ptr1, *ptr2;
   ptrdiff_t diff; /* Primitiver Datentyp */
   char string[] = {"Hallo Welt\n"};

   ptr2=string;  /* ptr2 auf Anfangsadresse von string */
   /* ptr1 6 Bytes weiter von der Adresse ptr2 platzieren */
   ptr1 = ptr2+6;
   /* Wie weit liegen beide Zeiger voneinander entfernt */
   diff = ptr1-ptr2;
   printf("Differenz der beiden Zeiger : %d Bytes\n",diff); /*6*/
   printf("%s",ptr1);  /* Welt */
   printf("%s",ptr2);  /* Hallo Welt */
   return 0;
}

Mit der Zeile

ptr1 = ptr2+6; 

lassen Sie den Zeiger ptr1 auf die Adresse string[6] zeigen. ptr2 zeigt hingegen weiterhin auf die Anfangsadresse des Strings (string[0]). Die Differenz dieser beiden Zeiger beträgt 6 Bytes, da diese so weit voneinander entfernt sind.

15.5. Typensicherung bei der Dereferenzierung            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

Zeiger sind in C streng typisiert. Sie können einen Zeiger vom Datentyp int nicht auf die Adresse eines double-Werts zeigen lassen, wie im folgenden Beispiel zu sehen ist:

#include <stdio.h>

int main()
{
   int *int_ptr;
   double double_wert=999.999;

   int_ptr=&double_wert;
   printf("*int_ptr=%d double=%f\n",*int_ptr, double_wert);
   return 0;
}

Die Ausgabe des Zeigers wird irgendwelchen Unsinn ergeben. Es ist aber auch möglich, die Typensicherung durch explizite Typenumwandlung oder über einen void-Zeiger zu umgehen. Aber dazu später mehr.

15.6. Zeiger als Funktionsparameter (call-by-reference)            zurück  Ein Kapitel höher  zum Inhaltsverzeichnis

Funktionen, die mit einem oder mehreren Parametern definiert werden und mit return einen Rückgabewert zurückliefern, wurden bereits verwendet (call-by-value). Der Nachteil dieser Methode ist, dass bei jedem Aufruf erst einmal alle Parameter kopiert werden müssen, sodass diese Variablen der Funktion anschließend als lokale Variablen zur Verfügung stehen. Beispielsweise folgendes Programm:

#include <stdio.h>
#define PI 3.141592f

float kreisflaeche(float wert)
{
   return (wert = wert * wert * PI);
}

int main()
{
   float radius, flaeche;
   printf("Berechnung einer Kreisfläche!!\n\n");
   printf("Bitte den Radius eingeben : ");
   scanf("%f",&radius);
   flaeche = kreisflaeche(radius);
   printf("\nDie Kreisfläche beträgt : %f\n",flaeche);
   return 0;
}

In solch einem Fall bietet es sich an, statt der Variablen radius einfach nur die Adresse der Variablen als Argument zu übergeben. Die Übergabe von Adressen als Argument einer Funktion wird call-by-reference genannt. Das Prinzip sehen Sie im abgeänderten Programmbeispiel:

#include <stdio.h>
#define PI 3.141592f

void kreisflaeche(float *wert)
{
   *wert = ( (*wert) * (*wert) * PI );
}

int main()
{
   float radius;
   printf("Berechnung einer Kreisfläche!!\n\n");
   printf("Bitte den Radius eingeben : ");
   scanf("%f",&radius);
   /*Adresse von radius als Argument an kreisflaeche() */
   kreisflaeche(&radius);
   printf("\nDie Kreisfläche beträgt : %f\n",radius);
   return 0;
}

Statt einer Variablen als Argument, wurde in diesem Beispiel einfach die Adresse der Variable radius übergeben. Bildlich können Sie sich das Prinzip so vorstellen:

float radius; /*Wir geben einfach mal 5.5 ein */ 

Abbildung 15.11: Variable radius bekommt den Wert 5.5
Abbildung 15.11: Variable radius bekommt den Wert 5.5

kreisflaeche(&radius);

Abbildung 15.12: Adresse als Funktionsparameter (call-by-reference)
Abbildung 15.12: Adresse als Funktionsparameter (call-by-reference)

*wert = (*wert) * (*wert) * PI;

Abbildung 15.13: In der Funktion wird mit der Referenz gerechnet
Abbildung 15.13: In der Funktion wird mit der Referenz gerechnet

In diesem Beispiel übergeben Sie mit dem Funktionsaufruf

kreisflaeche(&radius);

die Adresse der Variablen radius als Referenz an die Funktion

void kreisflaeche(float *wert) 

In der Funktion kreisflaeche() befindet sich als Parameter ein Zeiger namens wert vom Typ float. Der Zeiger wert in der Funktion kreisflaeche() bekommt durch den Funktionsaufruf kreisflache(&radius) die Adresse der Variablen radius zugewiesen. Jetzt, da der Zeiger in der Funktion die Adresse kennt, kann mit dem Dereferenzierungsoperator, welcher ja auf den Wert von radius zeigt, gerechnet werden:

*wert= (*wert) * (*wert) * PI;

Die Klammerung bei der Berechnung kann auch weggelassen werden. Sie dienen der besseren Übersicht. Ohne den Dereferenzierungsoperator würde lediglich mit einer Adresse gerechnet werden. Die meisten Compiler geben ohnehin eine Fehlermeldung aus.

Es wird Ihnen sicherlich aufgefallen sein, dass bei der Funktion keine Rückgabe mehr mit return erfolgt und der Rückgabetyp void ist. Das liegt daran, dass bei jeder Neuübersetzung des Programms jeder Variablen eine Adresse zugewiesen wird, die sich während der Laufzeit des Programms nicht mehr ändern lässt. Da die Funktion die Adresse der Variablen radius bekommt, wird auch in der Funktion der Wert dieser Variablen verändert. Weil hierbei mit der Variablen radius und dem Zeiger wert mit denselben Adressen gearbeitet wird, entfällt eine Rückgabe an den Ausrufer.

15.6.1 Zeiger als Rückgabewert
Natürlich ist es auch möglich, einen Zeiger als Rückgabewert einer Funktion zu deklarieren, so wie das bei vielen Funktionen der Standard-Bibliothek gemacht wird. Funktionen, die mit einem Zeiger als Rückgabetyp deklariert sind, geben logischerweise auch nur die Anfangsadresse des Rückgabetyps zurück. Die Syntax dazu sieht folgendermaßen aus:

Zeiger_Rückgabetyp *Funktionsname(Parameter)

Das Verfahren mit Zeigern als Rückgabewert von Funktionen wird häufig bei Strings oder Strukturen verwendet und ist eine effiziente Methode, Datenobjekte aus einer Funktion zurückzugeben. Speziell bei Strings ist dies die einzige Möglichkeit, eine ganze Zeichenkette aus einer Funktion zurückzugeben. Natürlich ist sie es nicht wirklich. Tatsächlich wird ja nur die Anfangsadresse, also das erste Zeichen an den Aufrufer zurückgegeben. Hierzu ein recht einfaches Beispiel:

#include <stdio.h>
#include <string.h>
#define MAX 255

char *eingabe(char *str)
{
   char input[MAX];
   printf("Bitte \"%s\" eingeben: ",str);
   fgets(input, MAX, stdin);
   return strtok(input, "\n");
}

int main()
{
   char *ptr;
   ptr = eingabe("Vorname");
   printf("Hallo %s\n",ptr);

   ptr = eingabe("Nachname");
   printf("%s, interssanter Nachname\n",ptr);
   return 0;
}

Der Funktion eingabe() wird hierbei als Argument die Adresse eines Strings übergeben. In der Funktion werden Sie aufgefordert, einen Namen einzugeben. Die Anfangsadresse des Strings geben Sie mit folgender Zeile zurück:

return strtok(input, "\n");

Die Funktion strtok() liefert ja selbst als Rückgabewert einen char-Zeiger zurück. Da die Funktion fgets() beim Einlesen von der Standardeingabe das Newline-Zeichen miteinliest, haben Sie hierbei gleich zwei Fliegen mit einer Klappe geschlagen. Das Newline-Zeichen wird mit strtok() entfernt, und die Funktion liefert auch gleich die Anfangsadresse des Strings input als Rückgabewert zurück, welchen Sie gleich mit return weiterverwenden. Sie hätten genauso gut erst einen Zeiger deklarieren, diesen auf input zeigen lassen und dann mittels return die Anfangsadresse zurückgeben können:

char *eingabe(char *str)
{
   char input[MAX];
   char *ptr;
   printf("Bitte \"%s\" eingeben: ",str);
   fgets(input, MAX, stdin);

   ptr = strtok(input, "\n");
   return ptr;
}

Oder sind Sie gar nicht daran interessiert, das Newline-Zeichen zu entfernen? Dann könnte die Funktion auch so aussehen:

char *eingabe(char *str)
{
   char input[MAX];
   char *ptr=input;
   printf("Bitte \"%s\" eingeben: ",str);
   fgets(input, MAX, stdin);

   return ptr;
}

Weiter mit 15.7. Array und Zeiger            zum Inhaltsverzeichnis