Anfang   zurück   weiter

Zeiger

Zeiger (Pointer) sind ein wichtiger Bestandteil der Programmiersprache C. Sie stellen eine flexible und mächtige Methode zur Manipulation von Daten in Programmmen dar.

Was ist nun ein Zeiger? Der Speicher eines Computers besteht aus einer Folge von einzelnen Speicherstellen, die jede durch eine eindeutige Adresse identifiziert wird. Die Speicheradressen eines Rechners reichen von 0 bis zu einer maximalen Adresse, die von der Grösse des eingebauten Speichers abhängt. Wird ein Programm ausgeführt, belegen der Programmcode und die Daten einen Teil dieses Systemspeichers. Wird in einem C Programm eine Variable deklariert, so wird vom Compiler ein Speicherbereich mit einer eindeutigen Adresse zur Speicherung der Variablen reserviert. Benutzt das Programm diese Variable, so wird automatisch die richtige Speicheradresse angesprochen, aber der Programmierer muß sich nicht speziell damit beschäftigen. Das heißt vom Compiler wird die Adresse des für die Variable reservierten Speicherbereichs mit dem Variablennamen assoziiert.

Die Adresse einer Variablen ist also eine Zahl und kann wie jede beliebige andere Zahl in C behandelt werden. Ist die Adresse einer Variablen bekannt, kann also eine zweite Variable erstellt werden, in die die Adresse der ersten Variablen abgelegt wird. Die Deklaration eines Zeigers auf eine Variable lautet allgemein:

type   *ptrname;

oder für verschiedene Variablentypen
  char   *chr;   /* chr ist ein Zeiger auf eine Character Variable */
  int   *p_ganz, ganz;
 
  /* *p_ganz ist ein Zeiger auf int,
  ganz ist eine Integervariable */
  float   *p_real, real;
 
  /* *p_real ist ein Zeiger auf real
  real ist eine float Variable */
Der Operator * wird indirection operator oder Inhaltsoperator bezeichnet.

Benützung von Zeigern
Um einer Zeigervariablen die Adresse einer Variablen zuzuordnen, wird der Adressoperator & verwendet. Enthält die Zeigervariable ptr die Adresse der Variablen var, so verweist *ptr und var auf dieselbe Größe. Mit ptr und &var wird die Adresse der Variablen var bezeichnet. Ein Zeiger ohne Inhaltsoperator verweist also auf die Adresse der Variablen auf die er zeigt.

Ein Beispiel zum grundlegende Gebrauch von Zeigern:

/* ptr_demo.c
   Demonstriert den Gebrauch von Zeigern. */

#include <stdio.h>

/* Deklariere und initialisiere eine Integervariable */

int var = 1;

/* Deklariere einen Zeiger auf eine Integervariable */

int *ptr;

int main(void)
{
  /* Initialisiere ptr um auf var zu zeigen */

  ptr = &var;  /* ptr enthält also nun die Adresse von var */

  /* greife direkt und indirekt auf var zu */

  printf("\nDirekter Zugriff  : var = %d", var);
  printf("\nIndirekter Zugriff: var = %d\n", *ptr);

  /* Gebe die Adresse der Variablen var auf zwei Arten aus*/

  printf("\nDie Adresse von var (&var) = %p", &var);
  printf("\nDie Adresse von var (ptr)  = %p\n", ptr);
  return 0;
}

Die Werte der Adressen der Variablen sind von System zu System verschieden und hängen von der Belegung des Speichers durch die verschiedenen Systemprogramme ab.

Bei der Verwendung von Zeigern ist es ganz wichtig nicht darauf zu vergessen, daß vor der Benutzung eines Zeigers dieser zu initialisieren ist. Die Verwendung eines nicht initialisierten Zeigers kann verheerende Folgen haben.

In der bisherigen Beschreibung wurde nicht berücksichtigt, daß Variablen verschieden viel Platz im Speicher belegen. Zeiger auf verschiedene Variablentypen zeigen im Speicher immer auf das erste benutzte Byte einer Variablen. Da aber jeder Zeiger mit einem bestimmten Typ deklariert wurde, "weiss" der Compiler auf wieviel Speicherplatz die einzelenen Zeigertypen weisen.

Werden mit folgenden Anweisungen unterschiedliche Variablen deklariert und initialisiert,

int vint = 12345;
char vchar = 90;
float vfloat = 9876.5432;

und deklariert und initialisiert man im folgenden Zeiger auf diese Variablen,

int *p_vint = &vint;
char *p_vchar = &vchar;
float *p_vfloat = &vfloat;

so kann man oben gesagtes leicht durch Ausgabe der Inhalte der Zeigervariablen, und Bestimmung der Variablengrösse, durch Anwendung des Operators sizeof() auf die Variablen, zeigen.

Pointers und Arrays

Zeiger können schon bei einfachen Variablen nützlich sein, jedoch bei Arrays sind sie es um vieles mehr. Es gibt einen speziellen Zusammenhang zwischen Arrays und Pointern. Tatsächlich benutzt man Zeiger, wenn man eine Komponente eines Arrays über seinen Index anspricht.

Ein Array Name ohne Klammern ist ein Zeiger auf das Anfangsarrayelement. Wurde z.B. ein Array mit array daten[] deklariert, so ist daten die Adresse des nullten Array Elementes. Das heißt daten ist äquivalent zu &daten[0]. Die Ralation (daten==&daten[0]) liefert in C als Ergebnis den Wert true. Zu beachten ist, daß der Name des Arrays eine Zeigerkonstante ist, also während des Programmes nicht verändert werden kann. Man kann aber sehr wohl eine Zeigervariable deklarieren und initialisieren, daß sie auf das Array zeigt.

int array[100], *p_array;
/* weitere Anweisungen */
p_array=array;

Diese Anweisung initialisiert die Zeigervariable p_array mit der Adresse des Anfangselementes von array[]. Da p_array eine Zeigervariable ist, kann sie auch verändert werden, und z.B. auf ein anderes Element des Arrays zeigen. Um das zu erreichen muß der Zeiger entsprechend erhöht werden. Für ein Typ int array heißt das also um 2 bei einem Typ float array entsprechend um 4. Um aufeinanderfolgende Elemente eines Arrays von bestimmtem Typ anzusprechen, muß der Pointer um sizeof(datatype) erhöht werden.

/* point_1.c
   Demonstriert den Zusammenhang zw. Adressen und
   Arrayelementen von verschiedenen Datentypen.
 */
 
#include <stdio.h>
 
/* Deklariere drei Arrays und eine Zaehler Variable. */
int i[10], n;
float f[10];
double d[10];
 
int main(void)
{
  /* Tabellen Kopf */
  printf("\nElement\t\tInteger\t\tFloat\t\tDouble\n");
  printf("=============================");
  printf("=============================\n");
 
  /* Gib die Addressen der Array Elemente aus. */
  for (n = 0; n < 10; n++)
  printf("Element %d:\t%p\t%p\t%p\n", n, &i[n], &f[n], &d[n]);
  printf("=============================");
  printf("=============================\n");
  return 0;
}

Zeiger Arithmetik

Hat man einen Zeiger der auf das Anfangselement eines Arrays zeigt, so muß um ein nachfolgendes Element des Arrays anzusprechen der Zeiger um die Größe des im Array abgelegten Datentyps erhöht werden. Dazu verwendet man die Zeiger Arithmetik. Die Zeiger Arithmetik ist relativ einfach und macht die Verwendung von Pointern im Programm leichter. Im wesentlichen gibt es drei Formen:

Inkrementieren und dekrementiern von Zeigern erfolgt mit den in C gebräuchlichen Abkürzungen. Ist z.B. ptr_to_int ein Zeiger auf ein Element eines Arrays vom Typ int so bewirkt ptr_to_int++, daß danach ptr_to_int auf das nächste Element des Arrays zeigt. Analoges gilt für einen Zeiger ptr_to_float der auf ein Element eines float Arrays verweist. Nach Ausführung von ptr_to_float++ zeigt ptr_to_float auf das folgende Element. Dasselbe gilt für Erhöhungen um mehr als 1. ptr_to_int += 4 zeigt also auf 4 Elemente weiter. Die Erniedrigung der Zeiger erfolgt mit dem -- und dem -= Operator.

Die Verwendung der Pointer Arithmetik zeigt folgendes Programmbeispiel:
/* point_2.c
   Demonstriert die Zeiger Arithmetik um 
   Array Elemente mit Zeigern anzusprechen. */

#include <stdio.h>
#define MAX 10

/* Deklariere und initialisiere ein Integer Array. */
int i_array[MAX] = { 0,1,2,3,4,5,6,7,8,9 };

/* Deklariere einen pointer auf int und eine int Variable. */
int *i_ptr, count;

/* Deklariere und initialisiere ein float Array. */
float f_array[MAX] = { .0, .1, .2, .3, .4, .5, .6, .7, .8, .9 };

/* Deklariere einen pointer auf float. */
float *f_ptr;

int main(void)
{
  /* Initialisiere die Pointer. */
  i_ptr = i_array;
  f_ptr = f_array;

  /* Gebe die Array Elemente aus. */
  printf("\n\nElement\tInteger\tFloat");
  for (count = 0; count < MAX; count++)
  printf("\n%i\t%d\t%f", count, *i_ptr++, *f_ptr++);
  return 0;
}

Als weitere Zeiger Manipulationsmöglichkeiten existieren noch das "differencing", mit dem der Abstand zweier Elemente im Speicher bestimmt werden kann : ptr1 - ptr2 und der Vergleich von Zeigern : ptr1 < ptr2. Dabei ist natürlich nur ein Vergleich zwischen Zeigern gleichen Typs erlaubt. Zeigt also ptr1 und ptr2 auf Elemente deselben Typs eine Arrays, so ist der Vergleich

ptr1 < ptr2
wahr, wenn ptr1 auf ein früheres Element des Arrays verweist als ptr2.

! Achtung !

Bei der Verwendung von Zeigern sollte ein besonders schwerer Fehler vermieden werden: Nämlich die Verwendung eines nichtinitialiserten Zeigers. Die Anweisung

int *ptr;

deklariert einen Zeiger vom Typ int. Dieser Zeiger ist aber noch nicht initialisert, zeigt also noch auf nichts, oder exakter irgendwohin im Speicher. Wird ein nichtinitialiserter Zeiger in einer Zuweisungs - Anweisung wie z.B.:

*ptr = 12;

verwendet, so wird der Wert 12 irgendwo im Speicher abgelegt. Das kann im Programmteil des aktuellen Programmes sein, aber auch an einer Stelle im Speicher, wo sich etwa ein Systemprogramm befindet.

Arrayindizes und Zeiger

Da ein Arrayname ohne Klammern ein Zeiger auf das Anfangselement des Arrays ist, kann man daher auch mittels des Inhaltsoperators angewendet auf den Arraynamen auf die verschiedenen Elemente des Arrays zugreifen. *array ist also das Anfangselement array[0], *(array + 1) das darauffolgende. Folgende Relationen erhalten also als Ergebnis den Wert true:

*(array) == array[0]
*(array + 1) == array[1]
*(array + 2) == array[2]
....
*(array + n) == array[n]

Man beachte dabei die Klammerung, da der Inhaltsoperator als unärer Operator Vorrang vor der Addition besitzt. Es würde *array + 1 zu array[0] + 1 äquvalent sein, also etwas anderes ergeben als beabsichtigt.

Übergabe von Arrays an Funktionen

Besonders wichtig ist die Verwendung von Zeigern, falls ein Array an eine Funktion übergeben werden soll. Die Argumente einer Funktion werden in C als Werte vom aufrufenden Programm an die Funktion übergeben. Es ist daher nicht möglich einfach ein Array an eine Funktion zu übergeben. Durch die Verwendung eines Pointers wird die Übergabe jedoch möglich. Ein Problem bleibt jedoch noch bestehen: Die Funktion besitzt nun zwar Zugriff auf das Array, hat aber noch keine Information über die Göße des Arrays. Es gibt zwei Möglichkeiten diese Information an die Funktion weiterzugeben:

  1. Man verwendet das letzte Arrayelement, um das Ende zu kennzeichnen. Dazu wird ein Array deklariert, das ein Element mehr als notwendig besitzt. Im letzten Arrayelement wird ein spezieller Wert abgespeichert, der wenn er gefunden wird, das Ende des Arrays anzeigt.
  2. Man übergibt an die Funktion einen weiteren Parameter, der die Information über die Göße des Arrays enthät. Der Funktion werden also zwei Argumente geliefert: ein Zeiger auf das Anfangselement und ein Ganzzahlwert, der die Anzahl der Elemente des Arrays angibt (Das zweite Argument könnte auch ein Zeiger auf das letzte Element des Arrays sein).
/* point_3.c
   Uebergabe eines Arrays an eine Funktion. */

#include <stdio.h>
#include <stdlib.h>

#define MAX 10

int array[MAX+1], count;
int largest(int x[]);

int main(void)
{
  /* Generiere MAX Zufallswerte 1 <= 10000 */
  for (count = 0; count < MAX; count++)
  {
    array[count] = 1 + rand() % 10000;
  }
  array[MAX]=0;

  /* Aufruf der funktion und Ausgabe des Ergebnisses. */
  printf("\n\nGroesster Wert = %d", largest(array));
  return 0;
}

/* Funktion largest() gibt den groessten Wert
   eines Integer Arrays zurueck. */

int largest(int x[])
{
  int count, biggest = 0X8000;

  for ( count = 0; x[count] != 0; count++)
  {
     if (x[count] > biggest)
        biggest = x[count];
  }
  return biggest;
}

Die zweite, der oben geschilderten Methoden, ist jedoch die flexiblere Form, um die Information über die Arraygröße zu erhalten.

Strings und Zeiger

Wie gezeigt, werden Strings in Characterarrays abgespeichert, wobei das Ende des Strings, welcher nicht das ganze Array belegen muß, durch \0 markiert ist. Da das Ende des Strings markiert ist, braucht zur Definition eines bestimmten Strings nur etwas auf dessen Anfang zu zeigen. Damit ist unmittelbar evident, daß der Arrayname allein zum Zugriff auf den String ausreicht.

Auf den Anfang eines Strings wird mittels eines Pointers auf eine Variable vom Typ char hingewiesen:

char *message;

definiert einen Zeiger auf eine Variable namens message vom Typ char, wobei dieser Zeiger im Moment noch auf nichts verweist. Wird die Definition aber abgeändert, so daß sie lautet:

char *message = "Jedes fertige Programm, das läuft, ist veraltet.";

wird der String   "Jedes fertige Programm, das läuft, ist veraltet."   irgendwo im Speicher abgelegt und der Zeiger message initialisiert, so daß er auf das erste Zeichen dieses Strings zeigt. Die vorige Deklaration ist zur folgenden äquvalent:

char message[] = "Jedes fertige Programm, das läuft, ist veraltet.";

Die beiden Notationen *message und message[] bezeichnen beide einen Zeiger. Auf diese Art kann bei der Übersetzung des Programmes Platz für eine Zeichenkette reserviert werden. Wird aber während des Programmablaufes ein variabler Speicherbedarf benötigt, so gibt es die Funktion malloc(), die diese Anforderung erfüllt.

malloc() und calloc() Funktion

Die Funktion malloc() ist eine von C's Funktionen zur Speicherplatz Reservierung (memory allocation). Sie ist in der Datei stdlib.h definiert. Beim Aufruf von malloc() wird der Funktion die Anzahl der Bytes, die zu reservieren sind, übergeben, und die Funktion liefert als Ergebnis die Anfangsadresse des reservierten Speicherblocks. malloc() gibt einen Zeiger vom Typ void zurück, der mit allen Datentypen in C kompatibel ist. Durch ein sogenanntes type casting kann der von der Funktion malloc() gelieferte Zeiger auf einen bestimmten Datentyp eingestellt werden. Dies zeigen die folgenden drei Beispiele.

Beispiel 1:

#include <stdlib.h>
int main(void)
{
  /* reserviere Speicher fuer einen 100 Zeichen langen String */
  char *str;
  if ((str = (char *)malloc(100)) == NULL)
  {
    printf("\nNicht genuegend Speicherplatz!\n");
    exit(1);
  }
  printf("\nPlatz fuer Zeichenkette reserviert!\n");
  return 0;
}
  
Beispiel 2:

#include <stdlib.h>
int main(void)
{
  /* reserviere Speicher fuer ein Array von 50 Integers */
  int *numbers;
  if ((numbers = (int *)malloc(50 * sizeof(int))) == NULL)
  {
    printf("\nNicht genuegend Speicherplatz!\n");
    exit(1);
  }
  printf("\nPlatz fuer Integer Array reserviert!\n");
  return 0;
}

Im letzten Beispiel wird zur Reservierung des Speicherplatzes die Funktion calloc() eingesetzt, die sich von malloc() dadurch unterscheidet, daß sie zwei Parameter erwartet. Die Deklaration von calloc() lautet (siehe stdlib.h):

void *calloc(size_t nitems, size_t size)

calloc() liefert einen Zeiger auf genügend Speicher für einen Vektor von nitems Objekten der angegebenen Größe size. size_t ist z.B. in stdio.h als unsigned definiert (typedef unsigned size_t;).

Beispiel 3:

#include <stdlib.h>
int main(void)
{
  /* reserviere Speicher fuer ein Array von 10 Realzahlen */
  float *numbers;
  if ((numbers = (float *)calloc(10, sizeof(float))) == NULL)
  {
    printf("\nNicht genuegend Speicherplatz!\n");
    exit(1);
  }
  printf("\nPlatz fuer Realzahl Array reserviert!\n");
  return 0;
}

Bei der Benutzung von Funktionen, die Daten mittels eines Zeigers abspeichern, muß also immer sichergestellt sein, daß der Pointer auf einen reservierten Speicherbereich zeigt. Folgende Sequenz von Anweisungen

char *ptr;
gets(ptr);

speichert die Eingabe irgendwo ab, da der Zeiger zwar deklariert, aber nicht initialisiert wurde.

Die Funktion free()

Wurde mit den Funktionen malloc() und calloc() Speicherplatz reserviert, so sollte dieser auch wieder, nachdem er nicht mehr gebraucht wird, freigegeben werden. Dazu dient die Funktion free() der ein Zeiger auf einen, vorher mittels malloc() oder calloc() allozierten, Speicherbereich übergeben wird. Die Deklaration von free() lautet:

void free(void *ptr);

Die Funktion hat keine Rückgabeparamter, der übergebene Parameter void *ptr ist ein Zeiger auf den freizugebenden Speicherbereich, der als Ganzes freigegeben wird. Man sollte darauf achten einen Speicherbereich nicht mehrfach freizugeben, oder free() mit einem Nullpointer aufzurufen.

Hier ein Beispiel:
#include <stdlib.h>
#include <stdio.h>

#define MAX_ZAHL 10

int main(void)
{
  double *memptr,*hilf;
  int i;
  memptr = (double *) malloc(MAX_ZAHL * sizeof(double));
  hilf = memptr;
  if(memptr == NULL)
  {
    printf("\nNicht genuegend Speicher!");
    exit(1);
  }
  printf("\nSpeicher fuer %i double-Werte ok!\n\n", MAX_ZAHL);
  for(i = 1; i <= MAX_ZAHL; i++) *hilf++ = 3.14159 * i;
  for(i = 0; i < MAX_ZAHL; i++) printf("%10.5f\t", memptr[i]);
  free(memptr);
  printf("\nSpeicherplatz wieder freigegeben!\n");
return 0;
}


Funktionen