next up previous contents
Next: Zeiger auf void Up: Zeiger, Zeiger-Feld-Dualität, und Referenzen Previous: Zeiger, Zeiger-Feld-Dualität, und Referenzen

Zeiger

Vom Standpunkt des Computers betrachtet ist jede erreichbare Speicherzelle im (virtuellen) Speicher durch eine eindeutige Zahl gekennzeichnet (Teile des virtuellen Adressbereiches können auf Harddisk gehalten sein, andere können im RAM liegen, wieder andere, gegenwärtig unbenutzte, müssen vom Betriebssystem gar nicht repräsentiert sein). Auf regulären PC's liegen diese Adressen im Wertebereich normaler int Variablen, auf anderen Architekturen können sie darüber hinaus gehen. C/C++ bietet Möglichkeiten diese ``Namen'' oder Adressen der Speicherstellen für Programmierzwecke nutzbar zu machen. An Übersichtlichkeit gewinnt das Programm dadurch, daß man die Adressen selbst benennen darf. Das Programmsegment
  int    i=5;
  double a=12.34;
  char   *c1="hello";   // declaration of an array of char
  int    j=0;
erzeugt beispielsweise die in Abb. [*] abgebildeten Speichereinträge (linke Spalte). Nur die Speichereinträge benötigen wirklich Platz im Computer die Daten enthalten. Die Adressen bzw. Namen belegen nicht mehr Platz wenn man im Programm anstelle von i den weniger kryptischen Namen counter verwenden würde.
Abbildung: Speicherbelegung (schematisch) mit realem Speichereintrag (Links), einer willkürlich gewählten, Computer-internen Adresse (Mitte) und dem benutzerdefinierten Variablennamen (Rechts).
\begin{figure}\centerline{\epsfig{file=FIGS/memory1.eps,width=7cm}}\end{figure}

Im allgemeinen besteht leider keine Garantie, daß hintereinander definierte Variablen auch direkt nacheinander im Speicher liegen (Ausnahme: Felder). Die Variable c1 vom Typ char enthält einen Verweis auf einen ganz anderen Speicherbereich, in dem die gespeicherten Zeichen abgelegt sind. Die Definition von *c1 reserviert im Speicher einen Bereich, der genügt um das Wort "hello" abzulegen. Auf die einzelnen Elemente 'h', 'e', 'l', 'l' und 'o' kann man allerdings auch mit der Formulierung c[0], c[1], c[2], c[3], und c[4] direkt zugreifen. Man beachte dabei daß Numerierungen in C++ immer mit 0 beginnen.

Wenn z.B. nötig wird Daten in eine geordnete Liste schnell einzufügen oder zu löschen, so ist diese Operation nicht durch die Anordnung der Zeichen in einem derartigen linearen Feld zu erreichen. Ein lineares Feld ist dadurch ausgezeichnet, daß alle Daten in aufeinanderfolgenden Speicherbereichen liegen (die genaue Lokalisierung wird vom Computer übernommen). Sowohl das Einfügen als auch das Entfernen einer Zahl aus diesem Feld erfordert das Umkopieren aller ``oberhalb'' des modifizierten Elementes liegenden Daten, im Mittel also etwa halbsoviele Operationen wie das Feld Elemente aufweist. Dieser Aufwand ist nur bei kleinen Feldern akzeptabel. Eine Teillösung dieses Problems liegt in einer verketteten Liste, in der jedes Element noch die Information erhält ``wo'' das jeweils nächste im Speicher liegt (d.h. welches der Name der Speicherzelle ist, die den Wert des Elementes speichert). Die tatsächliche Position des Elementes spielt keine Rolle, so daß immer nur die Speicherzellennamen manipuliert werden müssen. Um zum Beispiel ein Element $ x$ zu entfernen, muß nur der Namenseintrag des Vorgängers von $ x$ in der Liste so geändert werden, daß er nicht mehr auf $ x$ sondern auf dessen Nachfolger zeigt. Kopieren anderer Daten ist nicht mehr nötig, allerdings benötigt eine verkettete Liste evtl. mehr Speicher als eine lineare Liste, da für eine Zahl sowohl ihr Wert als auch eine Adresse abgelegt werden muß.

Abbildung: Benutzung von Zeigern am Beispiel einer verketteten Liste. Oben sieht man die Liste vor Entfernen eines Elementes. Unten ist es nicht in der Liste vorhanden.
\begin{figure}\centerline{\epsfig{file=FIGS/list.eps,width=10cm}}\end{figure}

Um den Umgang mit solchen Speicherzellennamen unabhängig vom jeweiligen Rechner zu gestalten, hält C/C++ den Zeiger/Pointer-Typ bereit. Ein Zeiger kann auf einen beliebigen Datentyp verweisen. Dieser Typ kann auch wieder ein Zeiger sein. Zur Deklaration benutzt man einen *. Um die Addresse einer Variablen zu erhalten, verwendet man den & Operator. Die Deklarationen im Programmsegment

// Declaration part 
  int     i = 5;        // initialisation
  int  *p_i = &i;       // p_i is a pointer to an int, 
                        //   here the variable i
  int **pp_i = (&p_i);  // pointer to pointer to int 

// How to use * and & legally in the program
    *p_i = 1;            // set i to 1, p_i remains unchanged
  **pp_i = 2;            // set i to 2, p_i remains unchanged
   int j = 7;            // declare another integer j
   *pp_i = &j;           // change p_i to point on j, *p_i != i
     p_i = &i;           // let p_i point to i again
führen zu der in Abb. [*] abgebildeten Speicherbelegung.
Abbildung: Speicherbelegung: Bedeutung der Spalten wie in Abb. [*].
\begin{figure}\centerline{\epsfig{file=FIGS/memory2.eps,width=7cm}}\end{figure}

Um den Wert einer Speicherstelle, auf die ein Zeiger verweist, zu ändern oder in einer Zuweisung oder Rechnung zu benutzen, benutzen wir ebenfalls den * Operator.

  *p_i = 27;               // i is 27 now 
  *p_i = 2 * (*p_i) + 5;   // equivalent to:  i = 2 * i + 5;

Konstante Pointer werden durch das Setzen von const rechts vom Pointersymbol * deklariert. Sie müssen bereits bei der Deklaration initialisert werden. Jede weitere Zuweisung auf den Pointer wird vom Compiler als Fehler erkannt.

  const int *cp_i = &i;                  // ok, i cannot be changed
                                         //     through use of cp_i
                                         //     cp_i can be changed
  const char newline = '\n';
  const char * const p_nl = &newline;    // const pointer to const char
                                         // note: p_nl = '0'; -> error!

Der * Operator hat damit zwei Bedeutungen. Steht er in einer Variablendeklaration, d.h. bei einem Typ, so kennzeichnet er einen Pointer. Vor einer Variablen (vom Typ Pointer) bewirkt er den Zugriff auf den Inhalt der Speicherstelle auf die der Pointer zeigt.

Analog hat der & Operator zwei Bedeutungen. Bei einer Variablendeklaration, d.h bei einem Typ kennzeichnet er eine Referenz. Vor einer Variablen ist es der sog. adress of Operator, d.h. er liefert die Adresse der Variablen.

Mit Zeigern kann Arithmetik getrieben werden; im obigen Beipiel läßt z.B. p_i = p_i + 5 den Zeiger um 5 Speicherstellen für int weiterwandern. In dieser Operation benötigt der Compiler die Information, wie groß der Datentyp ist, auf den verwiesen wird, um richtig inkrementieren zu können. Ebenfalls kann die Differenz von Pointern gebildet werden, wobei das Ergebnis ein integer ist. Ein definiertes Ergebnis hat diese Operation jedoch nur, wenn die beiden pointer in ein und dasselbe Datenfeld verweisen, s.u.. Wenn der Wert einer Speicherstelle benötigt wird, die man durch Fortschalten des Pointers um n Speicherstellen erhält, so gibt es dafür eine kurze Notation mittels []:

  int p[30];           // declare a field of int
  *(p_i + 25)  = 15;   // the value of the 25th entry (past the one 
                       // pointed to by p_i) is set to 15 
  p_i[25]      = 15;   // equivalent ...

Die Sprache garantiert, daß 0 niemals als Adresse eines wirklichen Datenelementes vorkommt. Dieser Wert läßt sich also verwenden, um irgendwie geartete Fehler anzuzeigen, die bei einer Operation mit Pointern aufgetreten sind.



Unterabschnitte
next up previous contents
Next: Zeiger auf void Up: Zeiger, Zeiger-Feld-Dualität, und Referenzen Previous: Zeiger, Zeiger-Feld-Dualität, und Referenzen
© R.Hilfer et al., ICA-1, Univ. Stuttgart
28.6.2002