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

Inhaltsverzeichnis
Vorwort
Vorwort des Gutachters
1 Einstieg in C
2 Das erste Programm
3 Grundlagen
4 Formatierte Ein-/Ausgabe mit »scanf()« und »printf()«
5 Basisdatentypen
6 Operatoren
7 Typumwandlung
8 Kontrollstrukturen
9 Funktionen
10 Präprozessor-Direktiven
11 Arrays
12 Zeiger (Pointer)
13 Kommandozeilenargumente
14 Dynamische Speicherverwaltung
15 Strukturen
16 Ein-/Ausgabe-Funktionen
17 Attribute von Dateien und das Arbeiten mit Verzeichnissen (nicht ANSI C)
18 Arbeiten mit variabel langen Argumentlisten – <stdarg.h>
19 Zeitroutinen
20 Weitere Headerdateien und ihre Funktionen (ANSI C)
21 Dynamische Datenstrukturen
22 Algorithmen
23 CGI mit C
24 MySQL und C
25 Netzwerkprogrammierung und Cross–Plattform-Entwicklung
26 Paralleles Rechnen
27 Sicheres Programmieren
28 Wie geht’s jetzt weiter?
A Operatoren
B Die C-Standard-Bibliothek
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
C von A bis Z von Jürgen Wolf
Das umfassende Handbuch
Buch: C von A bis Z

C von A bis Z
3., aktualisierte und erweiterte Auflage, geb., mit CD und Referenzkarte
1.190 S., 39,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1411-7
Pfeil 12 Zeiger (Pointer)
Pfeil 12.1 Zeiger deklarieren
Pfeil 12.2 Zeiger initialisieren
Pfeil 12.2.1 Speichergröße von Zeigern
Pfeil 12.3 Zeigerarithmetik
Pfeil 12.4 Zeiger, die auf andere Zeiger verweisen
Pfeil 12.4.1 Subtraktion zweier Zeiger
Pfeil 12.5 Typensicherung bei der Dereferenzierung
Pfeil 12.6 Zeiger als Funktionsparameter (call–by–reference)
Pfeil 12.6.1 Zeiger als Rückgabewert
Pfeil 12.7 Array und Zeiger
Pfeil 12.8 Zeiger auf Strings
Pfeil 12.8.1 Zeiger auf konstante Objekte (Read-only-Zeiger)
Pfeil 12.9 Zeiger auf Zeiger und Stringtabellen
Pfeil 12.9.1 Stringtabellen
Pfeil 12.10 Zeiger auf Funktionen
Pfeil 12.11 void-Zeiger
Pfeil 12.12 Äquivalenz zwischen Zeigern und Arrays
Pfeil 12.13 Der »restrict«-Zeiger


Rheinwerk Computing - Zum Seitenanfang

12.2 Zeiger initialisieren Zur nächsten ÜberschriftZur vorigen Überschrift

Hier beginnt eine gefährliche Operation. Wird im Programm ein Zeiger verwendet, der zuvor nicht initialisiert wurde, kann dies zu schwerwiegenden Fehlern führen – sogar 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 12.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 Bytes oder, auf 16-Bit-Systemen, zwei Bytes).

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

  • Wann bekommt die Variable ihren Speicherplatz zugeordnet? (Das ist abhängig vom Schlüsselwort static oder auto.)
  • Wie lange bleibt der Speicherort dieser Variablen gültig?
  • 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.)
  • 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 des Formatzeichens %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 ist:

int *Verzeichnis;

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

Verzeichnis =& Kapitel1;

Abbildung 12.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. Falls Sie den Adressoperator vor der Variablen Kapitel1 vergessen, 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 12.3 Darstellung im Arbeitsspeicher

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

Verzeichnis =& Kapitel1;

Abbildung 12.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 Indirektionsoperator (*) ins Spiel. Dieser dereferenziert den Wert der Adresse, mit der der Zeiger zuvor mit

Verzeichnis =& Kapitel1;

initialisiert wurde. Lassen Sie bei der Ausgabe einfach einmal den Indirektionsoperator 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 Operator 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 12.5 Der 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, das die Verwendung von Zeigern detaillierter darstellen soll. Es ist ein lehrreiches Beispiel, und es lohnt sich, 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 Adresse von x (%p)\n", &x);
   return EXIT_SUCCESS;
}

Abbildung 12.6 Die 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. Die folgenden Abbildungen verdeutlichen den Verlauf.

int x = 5;
int *y;

Abbildung 12.7 Speicheradressierung der Variablen »x« und des Zeigers »y«

y =& x;

Abbildung 12.8 Der Zeiger »y« verweist jetzt auf die Adresse von Variable »x«.

*y = 10;

Abbildung 12.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, das folgende 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, dass dem Zeiger y beim Start des Programms eine Adresse zur Verfügung steht, 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, die 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 gehe ich in einem anderen Abschnitt (Abschnitt 14.3, »Das NULL-Mysterium«) nochmals etwas genauer ein. Allerdings empfehle ich Ihnen, hierzu auch die deutsche FAQ der de.comp.lang.c (http://www.dclc-faq.de/inhalt.htm) zu lesen. Hier wurde dem Thema NULL-Zeiger ein ganzes Kapitel gewidmet, da besonders Anfänger NULL zu sehr vergleichen – 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 sehen Sie 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.1, »Rangfolge der Operatoren«). Wenn Sie diese Klammerung vergessen, kann das fatale Folgen haben.


Rheinwerk Computing - Zum Seitenanfang

12.2.1 Speichergröße von Zeigern topZur vorigen Überschrift

Ich komme bei der Speicherverwaltung zwar nochmals genauer auf dieses Thema zurück, aber es soll hier schon einmal kurz erwähnt werden. Die Größe eines Zeigers hängt nicht von dem Datentyp ab, auf den dieser verweist. Das ist 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;
}

Zeiger auf 64-Bit-Architekturen

Auf 64-Bit-Architekturen mit einem 64-Bit-Betriebssystem und dem LP64-Typenmodell ist ein Zeiger (wie auch der Datentyp long) üblicherweise auch 64 Bit breit und somit 8 Bytes.


Sicherlich stellt sich die Frage, warum man Zeiger dann überhaupt typisieren sollte, wenn der Speicherverbrauch immer gleich ist. Dies ist ziemlich wichtig in C. Nur dadurch lässt sich in C die Zeigerarithmetik realisieren. Denn nur durch das Wissen um die Speichergröße des assoziierten Typs kann die Adresse des Vorgänger- oder Nachfolgeelementes berechnet werden. Darüber hinaus ermöglicht die Typisierung von Zeigern dem Compiler, Verletzungen der Typkompatibilität zu erkennen.


Hinweis

Es gibt in der Tat Zeiger, die keinem Typ zugeordnet sind (void-Zeiger; siehe Abschnitt 12.11, »void-Zeiger«). Diese Zeiger können allerdings nicht dereferenziert, inkrementiert oder dekrementiert werden.




Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  
  Zum Katalog
Zum Katalog: C von A bis Z

 C von A bis Z
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: C/C++






 C/C++


Zum Katalog: Einstieg in C






 Einstieg in C


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2009
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.


[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de