Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 << zurück
C von A bis Z von Jürgen Wolf
Das umfassende Handbuch für Linux, Unix und Windows
– 2., aktualisierte und erweiterte Auflage 2006
Buch: C von A bis Z

C von A bis Z
1.116 S., mit CD, Referenzkarte, 39,90 Euro
Galileo Computing
ISBN 3-89842-643-2
gp Kapitel 14 Zeiger (Pointer)
  gp 14.1 Zeiger deklarieren
  gp 14.2 Zeiger initialisieren
    gp 14.2.1 Speichergröße von Zeigern
  gp 14.3 Zeigerarithmetik
  gp 14.4 Zeiger, die auf andere Zeiger verweisen
    gp 14.4.1 Subtraktion zweier Zeiger
  gp 14.5 Typensicherung bei der Dereferenzierung
  gp 14.6 Zeiger als Funktionsparameter (call–by–reference)
    gp 14.6.1 Zeiger als Rückgabewert
  gp 14.7 Array und Zeiger
  gp 14.8 Zeiger auf Strings
    gp 14.8.1 Zeiger auf konstante Objekte (Read-only-Zeiger)
  gp 14.9 Zeiger auf Zeiger und Stringtabellen
    gp 14.9.1 Stringtabellen
  gp 14.10 Zeiger auf Funktionen
  gp 14.11 void-Zeiger
  gp 14.12 Äquivalenz zwischen Zeigern und Arrays


Galileo Computing - Zum Seitenanfang

14.2 Zeiger initialisieren  downtop

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ückgegriffen 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
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.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:

gp  Wann bekommt die Variable ihren Speicherplatz zugeordnet? (Das ist abhängig vom Schlüsselwort static oder auto.)
gp  Wie lange bleibt der Speicherort dieser Variablen gültig?
gp  Wer kann diesen Wert ändern bzw. abrufen? (Das ist abhängig vom Gültigkeitsbereich und von der Sichtbarkeit der Variablen (global, lokal, Schlüsselwort const.)
gp  Wann wird die Variable gespeichert? (Das ist abhängig vom Schlüsselwort volatile.)

Dies dient allerdings hier nur zur Information, denn die Dinge sollten jetzt nicht komplizierter gemacht werden als sie sind.

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

/* ptr1.c */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
   int x = 5;
   printf("Die Adresse von x ist %p \n",&x);
   return EXIT_SUCCESS;
}

In diesem Beispiel wurde mithilfe des Adressoperators und dem Formatzeichen %p die aktuelle Speicheradresse der Variablen 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:

/* ptr2.c */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
   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 EXIT_SUCCESS;
}

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;

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.2   Programm zur Verwendung der Zeiger in Aktion

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 Kapitel2 = 60;
int Kapitel3 = 166;
int Nachtrag = 233;
int *Verzeichnis;

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.3   Darstellung im Arbeitsspeicher

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

Verzeichnis =& Kapitel1;

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.4   Der Zeiger verweist hier auf die Adresse der Variablen Kapitel1

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

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

Hier kommt zum ersten Mal der Indirektionssoperator (*) 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 Indirektionssoperator 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 Indirektionsoperator 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
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.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:

/* ptr3.c */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
   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 hat jetzt die Adresse von x */
   y =& 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 EXIT_SUCCESS;
}

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.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
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.7   Speicheradressierung der Variablen x und des Zeigers y

y =& x;

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.8   Zeiger y verweist jetzt auf die Adresse von Variable x

*y = 10;

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.9   Dereferenzierung der Variablen x

Somit gilt: Wenn Sie mit dem Indirektionsoperator 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:

/* ptr4.c */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
   int *y;
   *y=10;
   printf("Der Wert von *y ist %d\n", *y);
   return EXIT_SUCCESS;
}

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 NULL initialisieren und vor der Verwendung des Zeigers eine Überprüfung auf NULL durchführen. Der Wert oder genauer der Zeiger NULL ist meistens 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.


Hinweis   Auf NULL soll in einem späteren Kapitel nochmals etwas genauer eingegangen werden. Allerdings empfehle ich Ihnen hierzu auch die deutsche FAQ von der de.comp.lang.c zu lesen. Hier wurde dem Thema NULL-Zeiger ein ganzes Kapitel gewidmet, da besonders Anfänger NULL zu sehr ver-

gleichen – was aber nicht immer so sein muss. Theoretisch muss ein NULL-Zeiger nämlich kein Zeiger auf null sein, sondern kann auch eben nur als 0 definiert werden.


Hier das Erwähnte in der Praxis:

/* ptr5.c */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
   int *y=NULL;    /* Zeiger mit NULL initialisieren */
   if(y == NULL) {
      printf("Der Zeiger besitzt keine gültige Adresse\n");
      return EXIT_FAILURE;
   }
   else
      *y = 10;
   return EXIT_SUCCESS;
}

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:

/* ptr6.c */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
   int *ptr;
   int var=10, tmp;
   /* ptr zeigt auf Adresse von var */
   ptr =& var;
   /* Variable tmp bekommt den Wert, den ptr dereferenziert */
   tmp = *ptr;       /* tmp=10 */
   *ptr = 100;       /* Inhalt von var wird verändert var=100 */
   if(var > 50)      /* Ist var größer als 50 ... ? */
      var = tmp;       /* ... wieder den alten Wert */
   printf("var=%d\t*ptr=%d\n",var, *ptr); /* var=10   *ptr=10 */
   return EXIT_SUCCESS;
}

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

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

Sollten Sie den Indirektionsoperator 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 demselben 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;     // ptr zeigt auf var2
printf("%d", *ptr);    // gibt Wert von var2 aus
printf("%p", &ptr);    // gibt Adresse von ptr aus
printf("%p", ptr);     // gibt Adresse von var2 aus

Sicherlich sind Ihnen im Beispiel auch die Klammern bei den Zeilen

(*ptr)++;      // Dereferenzierung: var hat jetzt den Wert 201
(*ptr)--;      // var hat wieder den Wert 200

aufgefallen. Diese waren nötig, da die Operatoren ++ und -- einen höheren Rang haben als der Indirektionsoperator (*) (siehe Anhang A »Rangfolge der Operatoren«). Ein Vergessen dieser Klammerung kann fatale Folgen haben.


Galileo Computing - Zum Seitenanfang

14.2.1 Speichergröße von Zeigertoptop

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:

/* ptr7.c */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
   char   *v;
   int    *w;
   float  *x;
   double *y;
   void   *z;
   printf("%d\t %d\t %d\t %d\t %d \n",
      sizeof(v),sizeof(w), sizeof(x), sizeof(y), sizeof(z));
   return EXIT_SUCCESS;
}
 << zurück
  
  Zum Katalog
Zum Katalog: C von A bis Z
C von A bis Z
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Shell-Programmierung






 Shell-Programmierung


Zum Katalog: Linux-UNIX-Programmierung






 Linux-UNIX-Programmierung


Zum Katalog: C/C++






 C/C++


Zum Katalog: UML 2.0






 UML 2.0


Zum Katalog: Reguläre Ausdrücke






 Reguläre Ausdrücke


Zum Katalog: Linux






 Linux


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de