C-Tutorial (C oder C++, Vorwort, Installation, Kapitel 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)

Zeiger

11.1. Zeiger definieren, Verweis- und Adressoperator

Um effiziente Programme schreiben zu können, ist es oftmals notwendig, nicht mit den Daten selbst, d.h. mit Kopien der Daten, sondern mit Verweisen auf diese zu arbeiten. Wenn größere Datenmengen erst kopiert werden müssen, wird dabei oft Rechenleistung verschenkt. Ein schönes Beispiel sind Arrays, die ebenso wie Variablen als Parameter an eine Funktion übergeben werden.

Bei großen Arrays wäre dies eine unnütz große Datenmenge, die kopiert und lokal - in der Funktion - gespeichert werden müsste. Eine leichtere Möglichkeit ist, nur einen Verweis auf das Anfangselement, den Anfang des Arrays zu übergeben. Einen solchen Verweis nennt man Zeiger. Dieser verweist (zeigt) auf den Ort, d.h. die Adresse im Speicher, an dem die Daten liegen (bzw. beginnen).

Zunächst werden wir eine einfache Zeigervariable erstellen. Diese speichert eine Adresse im Arbeitsspeicher. Jeder Zeiger hat hierbei auch einen Typ. Da an der Stelle, auf die der Zeiger verweist, Daten liegen, muss klar sein, wie diese gelesen bzw. geschrieben werden sollen. Da der Zeiger nur auf die Anfangsadresse verweist, muss bekannt sein, wo die Endadresse ist, sonst würden fremde Speicherbereiche überschrieben werden.

Soll ein Zeiger beispielsweise auf einen char-Wert zeigen, muss der Zeiger vom Typ char * sein (mit * am Ende!). Ebenso müsste ein int * - Zeiger definiert werden, soll auf einen int-Wert gezeigt werden. Man spricht hierbei auch oft von einem Zeiger auf <Typ>, einem Zeiger auf int, Zeiger auf float, usw.

Eine Zeigervariable benötigt immer gleich viel Speicher. Das ist so viel, wie nötig ist, um eine Adresse abzubilden. Mit einem beliebigen Zeigertypen können Sie das auf Ihrem System herausfinden, indem Sie - z.B. - das Ergebnis von sizeof(int*) ausgeben. Auf 64-Bit-Maschinen werden Sie hier wahrscheinlich 8 (Bytes) als Ergebnis erhalten.

Ein Zeiger auf int wird wie folgt deklariert:

int *p;

Undefiniert ist der Zeiger auch gleich "gefährlich", da nicht bekannt ist, auf welche Speicheradresse er zeigt! Das ist genauso, wie lokale Variablen einen beliebigen Wert enthalten können. Daher müssen Zeiger unbedingt initialisiert werden, was nichts anderes bedeutet, als dass dem Zeiger ein Wert - die Speicheradresse - zugewiesen wird. Es ist allerdings auch möglich, den Zeiger ins Nichts zeigen zu lassen:

int *p = 0;

Sie können etwa eine Variable dorthin zeigen lassen, wo eine andere Variable ihren Wert abspeichert. Hat man etwa die int-Variable a und möchte, dass pa (wie: pointer auf a, ein Zeiger auf int) darauf zeigt, so wäre folgende Anweisung notwendig: (Initialisierung bei der Deklaration)

int *pa = &a;

Die Variable a sei in diesem Beispiel vom Typ int und somit eine normale Variable. a wurde zuvor deklariert und der Compiler hat für diese Variable ausreichend Speicher reserviert (meist 4 Bytes). Nun lässt sich mittels Speicheradresse jedes der 4 Bytes adressieren, 4 Adressen gibt es also. Nun passiert im letzten Beispiel Folgendes: Der Adressoperator & sorgt dafür, dass die Startadresse (die, die das erste Byte anspricht) vom entsprechenden Operanden (das ist hier die Variable a) geliefert wird. Diese wird dem Zeiger pa zugewiesen. Das hat zur Folge, dass pa von nun an auf diese Adresse zeigt.

Natürlich können Sie einem Zeiger eine Adresse nicht nur bei dessen Deklaration zuzuweisen, sondern auch später:

pa = &a;

Es fällt auf, dass der Stern * bei der Zeigervariablen nicht angegeben wurde. * bezeichnet man als Verweisoperator. Wird * weggelassen, z.B. pa, repräsentiert der Zeiger seine Adresse! Das bedeutet, dass man durch Weglassen des *-Operators die Adresse verändern bzw. setzen kann. Die Initialisierung sieht hier wie eine Ausnahme aus, ist aber in Wirklichkeit nur der Datentyp. Anders angeschrieben:

int* pa = &a;

Ob Sie den * zum Variablennamen oder zum Typ schreiben, bleibt Ihnen überlassen. Eine Diskussion erspare ich mir an dieser Stelle.

Gibt man das Verweisoperator mit an, z.B. pa*, so greift man nicht auf die Adresse zu, sondern auf den Bereich im Arbeitsspeicher, auf den der Zeiger zeigt. Sie können dann den Wert auslesen oder verändern. Man spricht hier von einer Dereferenzierung. Anhand des Zeigertyps weiß der Compiler, was zusammen gehört, auf wieviele Bytes also zugegriffen werden darf.

Sehen Sie sich einmal das nächste Beispiel an:

#include <stdio.h>
 
int main()
{
  int a, b, *pa, *pb;   /* zwei Variablen und zwei Zeiger deklarieren */
 
  pa = &a;   /* zeigt nun auf a */
  pb = &b;    /* zeigt nun auf b */
 
  printf ("Geben Sie zwei Zahlen ein: ");
 
  scanf ("%d %d", pa, pb);   /* kein &-Operator! */
 
  printf ("Zahlen werden wieder ausgegeben:\n");
 
  printf ("Zugriff über die Variablen ...\n");
  printf ("a = %d, b = %d\n", a, b);   /* altbekannt; einfach den Wert von a und b ausgeben */
 
  printf ("Zugriff durch Dereferenzierung der Zeiger ...\n");
  printf ("a = %d, b = %d\n", *pa, *pb);   /* Wichtig: Verweisoperator * nötig! */
 
  return 0;
}

Am Anfang der main()-Funktion werden die beiden int-Variablen a und b deklariert. Wie üblich werden beide durch Beistriche getrennt, um zu vermeiden, dass der Datentyp mehrmals angegeben werden muss. Nach b folgt die Deklaration zweier Zeiger auf int, ebenso durch Beistriche getrennt. Der Stern * steht vor den Bezeichnern. Es wurden somit zwei int-Variablen sowie zwei Zeiger auf int deklariert.

Die nächsten beiden Zeilen legen die Adressen fest, auf die die Zeiger verweisen sollen. Bei den Zeigervariablen darf KEIN * stehen, da auf die Zeigervariable selbst zugegriffen wird. Der Zeiger speichert dann in seinen - häufig - 8 Bytes die Adresse der anderen Variable. Mit dem Operator & werden die Adressen von a und b herausgefunden.

Nun wird es interessant. Mit scanf() werden zwei Werte eingelesen. Bisher haben wir bei scanf() immer ein & vor die Variablen gestellt. Das liegt daran, dass scanf() eine ADRESSE verlangt. Bei Zeigern repräsentiert der Zeigername aber bereits die Adresse, und somit entfällt hier der Adressoperator.

Wenn Sie hier & dennoch vor dem Zeiger angeben, erfahren Sie, wo der Zeiger selbst gespeichert ist! Also an welcher Stelle im Speicher die Zeigervariable die Adresse der anderen Variable speichert.

Nach dem Einlesen werden die Werte zweimal ausgeben. Zunächst durch Zugriff auf die Variablen, dann durch Dereferenzieren der Zeiger mit dem *-Operator. Es dürfte nicht verwundern, warum beide Male - richtigerweise - die gleichen Werte ausgegeben werden..

Ein weiteres Beispiel:

#include <stdio.h>
 
int main()
{
  int a = 100, *pa = &a;   /* a wird mit 100 initialisiert, pa zeigt gleich auf a */
 
  printf ("a = %d\n", a);
  printf ("*pa = %d\n", *pa);
 
  printf ("&a = %x (hexadezimal)\n", &a);
  printf ("pa = %x (hexadezimal)\n", pa);   /* "Wert" des Zeigers, also wohin zeigt pa? */
  printf ("&pa = %x (hexadezimal)\n",&pa);   /* Wo liegt der Zeiger selbst im Speicher? */
 
  printf ("Zeiger wird dereferenziert, Wert verändert ...\n");
  *pa = 200;   /* Ueber den Zeiger auf a zugreifen und dessen Wert veraendern */
  printf ("Neuer Wert von a: %d\n", a);
 
  return 0;
}

Die Ausgabe zeigt, dass die Adresse von a - die durch &a geliefert wird - mit der, auf die der Zeiger zeigt (Wert von pa), übereinstimmt. Denn so soll es auch sein! Neu ist an diesem Beispiel nur die Ausgabe des Ortes, wo der Zeiger selbst gespeichert ist, was wir durch &pa erfahren. &pa ist übrigens ein Konstanter Zeiger, da der Wert immer gleich bleibt. Der Gegensatz dazu sind Zeigervariablen, wie Sie sie jetzt schon kennen, die auf alles Mögliche zeigen können und während des Programmablaufs veränderbar sind. Abschließend wird der Wert von a indirekt über den dereferenzierten Zeiger verändert und wieder ausgegeben.

11.2. Zeiger als Funktionsparameter, Call by Reference

Sehr häufig werden Zeiger als Parameter von Funktionen verwendet. Dazu möchte ich noch einmal auf scanf() zurückkommen. scanf() hat bekanntlich die Aufgabe, Werte einzulesen und in Variablen zu schreiben. Um so etwas zu erreichen, gibt es nur zwei Möglichkeiten: Entweder übergibt man den eingelesenen Wert als Rückgabewert oder man verwendet Zeiger.

Die erste Möglichkeit hätte den Nachteil, dass immer nur ein Wert auf einmal eingelesen werden könnte, denn es gibt nur einen Rückgabewert. Mehrere Werte wären möglich, wenn alle vom selben Typ sind. Dann könnte ein Array zurückgeliefert werden.

Es kommt aber aus gutem Grund die zweite Variante zum Einsatz. Es soll nämlich der Wert derjenigen Variable geändert werden, die angegeben wurde. scanf() verwendet Zeiger als Parameter. Indem die Zeiger dereferenziert werden, wird auf die Werte der externen Variablen zugegriffen und diese gesetzt.

Kämen in scanf() "normale" Parameter zum Einsatz, wie Sie sie aus Kapitel 10 (Funktionen) kennen, würde das nicht funktionieren. Die Parameter-Variablen von Funktionen können zwar verändert werden (sind normale lokale Variablen für die Funktion), das hat aber keinen Einfluss (keine Seiteneffekte, die hier aber erwünscht sind) auf Variablen, die außerhalb der Funktion deklariert wurden.

Das nächste Beispiel demonstriert dies. Achten Sie auf den Funktionsprototyp (auf den Funktionskopf)!

#include <stdio.h>
 
void speichern(int *variable, int wert);   /* Funktionsprototyp */
 
int main()
{
  int a, b, *pb = &b;
 
  speichern (&a, 100);   /* Adresse von a und 100 übergeben */
  speichern (pb, 200);   /* pb zeigt auf b */
 
  printf ("a = %d, b = %d\n", a, b);
 
  return 0;
}
 
/* speichern() setzt die referenzierte Variable
   auf den angegebenen Wert;
   hat den gleichen Effekt wie eine Zuweisung
*/
void speichern (int *variable, int wert)
{
  *variable = wert;   /* Zeiger dereferenzieren und den Wert zuweisen */
}

Zunächst fällt der Prototyp der Funktion auf. Der erste Parameter ist int *variable. Das heißt, dass als erster Parameter eine Adresse übergeben werden muss. Entweder wird hier eine Variable mit & davor angegeben, oder der Name der Zeigervariable ohne *.

Über den dereferenzierten Zeiger (*variable) wird auf die "externe" Variable direkt zugegriffen. Man nennt diese Methode Call by Reference. Das Gegenteil, Call by Value, wo Werte kopiert werden, kennen Sie bereits aus Kapitel 10.

Der zweite Parameter (wert) ist ein normaler Funktionsparameter. Im Beispiel dient er dazu, der Funktion zu sagen, welchen Wert sie speichern soll. Hier erfolgt die Übergabe by Value, es wird also kopiert. Zwar hätten wir hier auch einen Zeiger verwenden können, das hätte aber den Nachteil gehabt, dass dann immer eine Variable angegeben werden müsste. Einen konstanten Wert - wie 100 - direkt anzugeben, wäre dann nicht möglich.

Das restliche Beispiel ist schnell erklärt. Die Funktion speichern() weist der dereferenzierten - der äußeren - Variable den Wert zu. In main() werden dann a und b zur Kontrolle ausgegeben.

11.3. Zeiger und Arrays

Erinnern Sie sich an folgendes Beispiel aus Kapitel 9.2:

#include <stdio.h>
 
int main()
{
  char eingabe[255];
 
  printf ("Text eingeben:\n");
  scanf ("%s", eingabe);     /* KEIN & vor eingabe! */
 
  printf ("Der Text war:\n%s\n",eingabe);  /* ausgeben */
 
  return 0;
}

Vielleicht haben Sie sich gefragt, wieso der Adressoperator & beim Einlesen des Strings in ein Array weggelassen wurde. Wenn Sie dieses Kapitel aufmerksam gelesen haben, müssten Sie folgerichtig argumentieren können, dass der Name des Arrays dessen Anfangsadresse widerspiegelt.

Das gilt allerdings nur dann, wenn kein bestimmtes Element des Arrays mit [ ] angegeben wurde. Wenn Sie mit

int feld[10];

ein Array vom Typ int mit 10 Elementen deklariert haben, dann bezeichnet feld die Start-Adresse des ersten Elementes. Um die Adresse zu erhalten, können Sie auch - wie bekannt - &feld[0] angegeben. Das ist gleichwertig zu feld.

Alle Elemente eines Arrays liegen im Speicher hintereinander. Bei einem char-Array liegt das 3. Element zwei Bytes von der Start-Adresse entfernt. Ein Element weiter wäre das 2. Element, zwei Elemente (bei char 2 Bytes entfernt) weiter ist also das 3. Element.

Durch Addition oder Subtraktion ist es möglich, im Speicher quasi "zu springen", sog. Zeigerarithmetik. Um von einem char-Element zum nächsten zu gelangen, muss zur Adresse 1 hinzuaddiert werden. Die Schlussfolgerung, bei int müsse man dann 2 oder 4 Bytes addieren, ist aber (zum Glück!) falsch. Sonst wäre der Quellcode nicht portabel, wenn auf einem anderen System int eine andere Größe hat. Darum gilt allgemein: Addiert man 1, wird immer um die Größe des Datentyps weitergesprungen!

Ein Beispiel soll das verdeutlichen:

#include <stdio.h>
 
int main()
{
  int feld[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, i;
 
  for (i = 0; i < 10; i++) 
    printf ("Element %d = %d\n", i, *(feld + i) );
 
  return 0;
}

Neu ist in diesem Beispiel:

*(feld + i)

Durch Addition eines Wertes wird erreicht, dass um i Felder weitergesprungen wird. Da wir allerdings auf den Wert und nicht auf die Adresse zugreifen wollen, muss ein * vor der Adresse stehen. Die Adresse bildet sich aus feld + i und muss aus diesem Grund in Klammer stehen.

Es sind nicht alle arithmetischen Operationen mit Zeigern möglich, sondern nur solche, die "Sinn machen". So können Sie etwa nicht zwei Zeiger addieren, was auch keinen Sinn macht. Adresse + Adresse = ? Wenn das den größtmöglichen Wert nicht bereits überschreitet, ergäbe das irgendeine zufällige Adresse.

Abschließend möchte ich noch einen besonderen Zeiger vorstellen: Den Zeiger auf char. Mit ihm lassen sich Strings speichern. Er zeigt auf einen bestimmten Bereich im Speicher, der ab dieser Stelle mit Zeichen (je 1-Byte-Blöcke, die als ASCII-Zeichen interpretiert werden) gefüllt wird. Das Ende des Strings bildet ein char mit dem ASCII-Code 0, das sich durch die Escape-Sequenz \0 bilden lässt. Da ein solcher Zeiger ohne explizites Reservieren von Speicher zu unvorhersehbaren Ergebnissen führen kann, möchte ich dieses Thema erst im Kapitel 12 über Speicherverwaltung fortführen.

Das Thema Zeiger wurde hier keineswegs erschöpfend behandelt. Vor allem viele Anwendungsmöglichkeiten sind Ihnen noch unbekannt. Dazu zählen zahlreiche Algorithmen, die Zeiger verwenden.

Vorheriges Kapitel Nächstes Kapitel