Programmieren in C


von Prof. Jürgen Plate

6 Zeiger (Pointer)

6.1 Grundlagen Zeiger

Zeiger sind Variablen und benötigen wie alle anderen Variablen Speicherplatz. Ein Zeiger ist eine Variable, die die Adresse eines (beliebigen) anderen Objektes enthält. Man kann auf dieses Objekt indirekt über einen Zeiger zugreifen. Diese Speicheradressen sind entweder Adressen von anderen Variablen oder Adressen von Funktionen.

Der Name "Zeiger" (engl. "Pointer") kommt von der Vorstellung, daß Zeiger auf eine andere Variable zeigen.

Verwendung von Zeigern in C:

Grundoperationen mit Zeigern:

Deklaration einer Zeigervariablen, die auf eine Variable des angegebenen Typs zeigt:

int *pc;

Eigentlich sollte man ja int* pc schreiben, denn gemeint ist ein Pointer auf int, der den Name pc hat. Aber seit Anbeginn der Sprache C wird das Sternchen direkt vor den Variablennamen gesetzt (dem Compiler ist es übrigens egal).

Zeiger zeigen immer auf Objekte eines bestimmten Typs. Sie müssen daher deklariert werden. Auch in der Zeigerdefinition wird der Operator * verwendet. Zeigerdefinitionen kann man als Muster verstehen:

int *px;          /* px ist Zeiger auf int */
char *zs;         /* zs ist Zeiger auf char */
int x, y;
px = &x;      /* px = (Speicher-)Adresse der Variablen x */
y = *px;          /* y = Wert der Variablen x */
Zeiger sind also an bestimmte Objekttypen gebunden. Ein Zeiger auf int kann also beispielsweise nur Adressen von int-Variablen aufnehmen. Eine Ausnahme bildet der "Generic Pointer", der auf ein beliebiges Objekt zeigen kann.

void *pc;

In Verbindung mit Zeigern werden hauptsächlich zwei zueinander inverse Operatoren benutzt:

  1. Der Adreßoperator &, der angewendet auf ein Objekt, die Adresse dieses Objekts liefert.

    pc = &c;

    & kann auf Variablen und Arrayelemente angewendet werden, nicht aber auf Arraynamen selbst (Warum? Ein Arrayname hat keine Adresse, er ist eine Adresse!). Ebenso haben natürlich Variablen in der Speicherklasse register keine Adressen.

    Beispiele für die Anwendung des Adreßoperators und das Speichern der Adresse in einem Zeiger:

    px = &x;     /* px erhält als Wert die Adresse von x */
    pf = &f[5];  /* pf erhält als Wert die Adresse des
                    6. Elementes von f */
    
  2. Der Inhaltsoperator *, der angewendet auf einen Zeiger das Objekt liefert, das unter dieser Adresse abgelegt ist.

    c = *pc;
    *pc = 5;

    Beispiel:

    y = *px;         /* y erhält den Wert des Objektes,
                        dessen Adresse in px steht */
    px = &x;         /* px "zeigt" nun auf x */
    y = *px;         /* y = x; */
    
Die folgenden Programmbeispiele zeigen den Gebrauch dieser beiden Operatoren:
 
#include <stdio.h>

int main(void)
  {
  int x = 1, y = 2, z[10];
  int *ip;               /* ip ist ein Zeiger auf int */

  ip = &x;               /* ip zeigt nun auf x */
  printf("ip: %d\n",ip);
  y = *ip;               /* y ist nun gleich 1 */
  printf(" y: %d\n", y);
  *ip = 0;               /* x ist nun gleich 0 */
  printf(" x: %d\n", x);
  ip = &z[0];        /* ip zeigt nun auf z[0] */
  printf("ip: %d\n", ip);
  }



#include <stdio.h>

int main(void)
  {
  int zahl;
  int *zeiger;

  /* Laß die Zeigervariable auf die Variable zeigen */
  zeiger=&zahl;

  /* Setze den Wert der Variablen mit Hilfe der Zeiger-
  variablen */
  *zeiger = 5;

  /* Gib zur Kontrolle den Wert aus */
  printf("Der folgende Wert sollte 5 sein: %d\n",zahl);

  return(0);
}

Die Deklaration des Zeigers ip in int *ip; besagt, daß der Ausdruck *ip vom Datentyp int ist, bzw. ip auf den Datentyp int zeigt. In der Zuweisung ip = &x; wird der Zeigervariablen ip die Adresse von x zugewiesen; man sagt auch "ip zeigt auf x". Damit hat *ip denselben Wert wie x, nämlich 1, der in der Zuweisung y = *ip; der Variablen y zugewiesen wird und somit den ursprünglichen Wert 2 überschreibt. Durch die Zuweisung *ip = 0; erhält auch x den Wert 0, wie man mit printf bestätigen kann. Durch die Zuweisung ip = &z[0]; zeigt der Zeiger ip auf das Anfangselement des Feldes z.

Einige Grundregeln:

Die Kombination *Zeiger kann in Ausdrücken überall dort auftreten, wo auch das Objekt, auf das der Zeiger zeigt, selbst stehen könnte:
y = *px + 10;
y = *px + *px;
printf("%d\n", *px);
*px = 0;
py = px;    /* falls py auch Zeiger auf int */

Bei der Verwendung des Operators * muß man die Operatorrangfolge und -assoziativität genau beachten. Dies erscheint zunächst etwas schwierig, da dieser Operator ungewohnt ist. Hier einige Beispiele mit dem * Operator und anderen Operatoren:

y = *px + 1;    /* Inhalt von px plus 1                    */
y = *(px+1);    /* Inhalt der Adresse px+1                 */
*px += 1;       /* Inhalt von px = Inhalt von px plus 1    */
(*px)++;        /* Inhalt von px inkrementieren            */
*px++;          /* wie *(px++); (Assoziativität) 
                   Inhalt der Adresse px; px = px plus 1   */
*++px;          /* Inhalt der Adresse px+1; px = px plus 1 */

Besonders wichtig:

  1. * und & haben höhere Priorität als arithmetische Operatoren.
  2. Werden * und ++ direkt hintereinander verwendet, wird der Ausdruck von rechts nach links abgearbeitet.

Ein weiteres Beispiel, das mit Pointern spielt:

#include <stdio.h>

main() 
  {
  int *pt1, *pt2;
  int var1 = 10;
  int var2 = 20;

  pt1  = &var1;       /* pt1 zeigt nun auf var1      */
  pt2  = pt1;         /* pt2 zeigt nun auch auf var1 */
                      /*                             */
  *pt1 = *pt1 + 1;    /* --> var1 = var1 + 1;        */
                      /*                             */
  pt1  = &var2;       /* pt1 zeigt nun auf var2      */
  (*pt1)++;           /* --> var2 = var2 + 1;        */
  *pt2 = 15;          /* --> var1 = 15;              */
                      /*                             */
  pt1  = &var1;       /* pt1 zeigt nun auf var1      */
  pt2  = &var2;       /* pt2 zeigt nun auf var2      */
  *pt2 = *pt1;        /* --> var2 = var1;            */
  *pt2 = 30;          /* --> var2 = 30;              */
                      /*                             */
  pt2  = pt1;         /* pt2 zeigt nun auch auf var1 */
  *pt2 = 30;          /* --> var1 = 30;              */
  }

Zeiger haben nur dann sinnvolle Werte, wenn sie die Adresse eines Objektes oder NULL enthalten. NULL ist eine globale symbolische Konstante, die in der Standardbibliothek definiert ist und überall als NULL-Zeiger benutzt werden kann. Für den Zeigerwert NULL ist garantiert, daß er nirgends hinzeigt. NULL ist definiert als eine Adresse mit dem Wert 0; Sie sollten auch immer NULL verwenden, niemals den Zahlenwert 0, denn es ist nicht sicher, ob die Länge einer Integer-Variablen auch der Länge einer Adresse entspricht. Früher war dies oft der Fall, weshalb sich manchmal in alten Programmen noch die Definition #define NULL 0 findet. Bei modernen Computersystemen geht das aber schief!

6.2 Zeiger und Funktionsargumente

Argumente vom Zeigertyp eröffnen die Möglichkeit, aus einer Funktion heraus Objekte in der rufenden Funktion anzusprechen und zu verändern. Die Funktion swap() hat dies gezeigt.

Beispiel:


/* Funktion zum Vertauschen ihrer Argumente */
/* 1. Versuch, FALSCH, da Werte nur innerhalb */
/* von swap getauscht werden */
void swap(int x,int y);  
  {
  int temp;
  temp = x;
  x = y;
  y = temp;
  }

int main(void)
  {
  int a = 1, b = 2;
  swap(a,b);
  printf("a=%d, b=%d\n",a,b); /* Ueberraschung! */
                              /* Ausgabe: a=1, b=2 */
  return(0);
  }

  
/* 2. Versuch, RICHTIG, da Objekte, auf die die */
/* Adressen zeigen, ausgetauscht werden */
void swap(int *px,int *py);
  {
  int temp;
  temp = *px;
  *px = *py;
  *py = temp;
  }

int main(void)
  {
  int a = 1, b = 2;
  swap(&a,&b);        /* Adressen übergeben */
  printf("a=%d, b=%d\n",a,b); /* Diesmal klappt es: */
                              /* Ausgabe: a=2, b=1 */
  return(0);
  }

Im anschließenden Beispiel zur Ermittlung des betragsgrößten Elementes einer n*n-Matrix mit Hilfe der Funktion maxval kann das Ergebnis auch ohne return Anweisung übergeben werden. Im Beispiel geschieht das über die Zeigervariable pamax. Beim Aufruf der Funktion maxval wird &amax, die Adresse von amax, als Argument übergeben. Die Zeigervariable pamax von maxval zeigt dann auf amax, d. h. der Parameter *pamax hat denselben Wert wie amax.
Die Berechnung in der Funktion maxval erfolgt mit *pamax. Der von der Funktion maxval ermittelte Wert, auf den *pamax zeigt, kann in der aufrufenden Funktion main als Variable amax verwendet werden.

   
/* Betragsgroesstes Element einer n*n Matrix */
/* Rueckgabe mittels einer Zeigervariablen  */
#include <stdio.h>
#include <math.h>

#define NMAX 5 

int maxval(double a[NMAX][NMAX], int n, double *pamax); 

int main(void)
  {
  double a[NMAX][NMAX] = { {   1.3,   2.6,  -8.7,  23.4,  12.0 },
                            { 777.0,   5.0,   3.1, -45.0,   0.1 },
                            {   0.01, 33.7,  11.4,  -1.0,  99.9 },
                            {   1.0,  -2.0,   3.0,  -4.0,   1.1 },
                            { 567.9, -222.0,  2.2,  3.14, -12.4 } };
  double amax; 
  int i, j, n;

  n = NMAX;
  printf("Eingabedaten:\n");
  for(i=0; i<n; i++) 
    {
    for(j=0; j<n; j++)
      printf("%f ", a[i][j]);
    printf("\n");
    }
  maxval(a,n,&amax);
  printf("betragsgroesstes Element= %f\n", amax);
  }

void maxval(double a[NMAX][NMAX], int n, double *pamax)
  {
  int i,j;
  *pamax = 0.0;
  for(i=0; i<n; i++)
    {
    for(j=0; j<n; j++)
      {
      if(fabs(a[i][j]) >  *pamax) 
        *pamax = fabs(a[i][j]);
      }
    }
  }
   

6.3 Zeigerarithmetik

Es können mit Zeigern bestimmte arithmetische Operationen und Vergleiche durchgeführt werden. Es sind natürlich nur solche Operationen erlaubt, die zu sinnvollen Ergebnissen führen. Zu Zeigern dürfen ganzzahlige Werte addiert und es dürfen ganzzahlige Werte subtrahiert werden. Zeiger dürfen in- und dekrementiert werden und sie dürfen voneinander subtrahiert werden (Dies ist i. a. nur sinnvoll, wenn beide Zeiger auf Elemente des gleichen Objektes (z. B. ein Array) zeigen.).

Man kann Zeiger mittels der Operatoren >, >=, <, <=, != und == miteinander vergleichen. Wie bei der Zeigersubtraktion ist das aber i. a. nur dann sinnvoll, wenn beide Zeiger auf Elemente des gleichen Arrays zeigen. Eine Ausnahme bildet hier der Zeigerwert NULL. denn viele Bibliotheksfunktionen liefern im Fehlerfall einen NULL-Zeiger zurück.

Alle anderen denkbaren arithmetischen und logischen Operationen (Addition von Zeigern, Multiplikation, Division, Shifts oder Verwendung von logischen Operatoren, sowie Addition und Subtraktion von float oder double Werten) sind mit Zeigern nicht erlaubt.

Es ist sinnvoll, Zeigervariablen mit NULL zu initialisieren, d. h. Zeigern, die keinen definierten Wert enthalten, wird der Wert NULL zugewiesen.

Wie funktioniert nun aber die Zeigerarithmetik? Sei ptr ein Zeiger und N eine ganze Zahl, dann bezeichnet ptr + N das n-te Objekt im Anschluß an das Objekt, auf das ptr gerade zeigt. Es wird also nicht der Wert N zu ptr direkt addiert, sondern N wird vorher mit der Typlänge des Typs, auf den ptr zeigt, multipliziert. Dieser Typ wird aus der Deklaration von ptr bestimmt.

Beispiel-Unterprogramm: Länge der Zeichenkette s


int strlen(char *s)    
  {
  char *p = s;         /* Pointer zeigt auf das erste Element */
  while (*p != '\0')  /* Solange der String nicht zuende ist */
    p++;               /* Pointer incrementieren */
  return (p-s);        /* Laenge = aktueller Wert - Anfangswert */
  }

Beispiel: 2 Versionen von strcmp: Vergleich zweier Strings s und t. Die Funktion liefert als Ergebnis: Ergebnis < 0 wenn s kleiner t ist,
Ergebnis = 0 wenn beide Strings identisch sind, Ergebnis > 0 wenn s > t ist

/* Version mit Arrays */
int strcmp(char s[],char t[]) 
  {
  int i;
  i = 0;
  while (s[i] == t[i])
    if (s[i++] == '\0')
      return (0);
  return (s[i] - t[i]);
  }

/* Version mit Pointern */
int strcmp(char *s,char *t)
  {
  for ( ; *s == *t; s++,t++)
    if (*s == '\0')
      return (0);
  return (*s - *t);
  }

Folgende Operationen sind möglich, sollten jedoch mit Vorsicht verwendet werden.

pc = (int*)10 . . . . . Zuweisen der Adresse 10 an die Zeigervariable pc
x = *(int*)10 . . . . . Zuweisen des Inhalts der Adresse 10 an die Variable x
Mit diesen Operationen kann man auf beliebige Speicherplätze zugreifen. Nützlich sind solche Befehle bei systemnaher Programmierung, z. B. bei der systemnahen Programmierung von Mikrocontollern.

6.4 Zeiger und Array

In C besteht zwischen Zeigern und Feldern eine ausgeprägte Korrespondenz. Jede Operation, die durch die Indizierung von Feldelementen formuliert werden kann, kann auch mit Zeigern ausgedrückt werden. Die Zeigerversion wird im allgemeinen schneller sein; sie ist aber am Anfang schwerer zu verstehen.

a ist ein Integer-Array und pa ein Zeiger auf den Datentyp int, deklariert durch

   int a[10];         /* Array mit 10 Elementen */
   int *pa;           /* Zeiger auf int */
Zur Erinnerung:
a hat 10 Elemente, wobei a[0] das erste und a[9] das letzte Element ist. a[i], das i-te Elemnt ist genau i Positionen vom Anfang a[0] entfernt.

Die Zuweisung

   
   pa = &a[0];
bewirkt, daß pa auf das 0-te Element von a zeigt, d. h. daß pa die Adresse von a[0] enthält. Statt der obigen Zuweisung kann auch die folgende
   pa = a;
verwendet werden, wo auf der rechten Seite der Feldname a steht. Die einzelnen Elemente des Feldes a können auf drei verschiedene Weisen angesprochen werden:
   a[i]       i-tes Feldelement
   *(pa+i)    Pointer pa + i * (Laenge eines Elementes von a)
   *(a+i)     Arrayanfang + i * (Laenge eines Elementes von a)
a ist also in C äquivalent zu &a[0]. So kann man statt pa = &a[0] auch schreiben pa = a. Die Verwandtschaft zwischen Arrays und Pointern kann beim Programmieren recht praktisch und effizient sein, führt aber auch zu unsauberem Stil und unverständlichen Programmen.

Das folgende Beispiel soll die Äquivalenz von Arrays und Pointern etwas verdeutlichen:

#include <stdio.h>

main() 
  {
  float messwerte[12];  /* Array mit 12 float-Komponenten */
  float *zeiger;        /* Zeiger auf ein float */

  /* 2 aequivalente Zuweisungen an die erste Arraykomponente */
  messwerte[0] = 1.23;
  *messwerte   = 1.23;

  /* 2 aequivalente Zuweisungen an die dritte Arraykomponente */
  messwerte[2]     = 4.56;
  *(messwerte + 2) = 4.56;

  /* 2 aequivalente Zuweisungen: Der Zeiger zeigt auf die 
     erste Arraykomponente */
  zeiger = messwerte;
  zeiger = &messwerte[0];

  /* 2 aequivalente Zuweisungen: Der Zeiger zeigt auf die 
     dritte Arraykomponente */
  zeiger = &messwerte[2];
  zeiger = messwerte + 2;

  messwerte = zeiger; /* Unzulaessig, weil 'messwerte' 
                       eine Zeigerkonstante ist     */
  }
Noch ein Beispiel für die Äquivalenz von Arrays und Pointern:

void arraytest(char x[])
  { /* Ersetzt im Array x alle 'e"' durch ".", 
       Array wird als Parameter uebergeben */
  int z;

  for (z = 0; x[z] != '\0'; z++)
    if (x[z] == 'e') x[z] = '.';
  } 


void pointertest(char *px)
  { /* Ersetzt im Array x alle 'e"' durch ".",
       ein Pointer wird als Parameter uebergeben */
  int z;

  for (z = 0; *(px+z) != '\0'; z++)
    if (*(px+z) == 'e') *(px+z) = '.';
  } 

Beide Unterprogramme können mit einem Array als Parameter aufgerufen werden und liefern das gleiche Ergebnis.

Im folgenden Programmbeispiel werden die drei Möglichkeiten in der Anweisung printf einander gegenübergestellt:

   
/* Zeiger und Felder */
#include <stdio.h>
int main(void)
  {
  int i, a[10] = {12, 23, 34, 35, 36, 37, 44, 46, 48, 50} ;
  int *pa;  

  pa = a;  /* oder: pa = &a[0]; */
  for (i=0; i<10; i++)
    printf("a[%d]= %d\t *(pa+%d)= %d\t *(a+%d)= %d\n", 
              i, a[i], i, *(pa+i), i, *(a+i));
  }
Die Korrespondenz von Indizieren und Zeigerarithmetik ist daher sehr eng: Die Schreibweise pa + i bedeutet die Adresse des i-ten Objekts hinter demjenigen, auf welches pa zeigt. Es gilt:

Jeder Array-Index-Ausdruck kann auch als Pointer-Offset-Ausdruck formuliert werden und umgekehrt.

Der Ausdruck pa + i bedeutet nicht, daß zu pa der Wert i addiert wird, sondern i legt fest, wie oft die Länge des Objekt-Typs von pa addiert werden muß.

Es besteht jedoch ein gravierender Unterschied zwischen Array-Namen und Zeigern:

Beispiel zur Verwandschaft zwischen Zeigern und Arrays: Äquivalente Formulierungen der Funktion strcopy()


void strcopy(char strl[] ,char str2[]) 
  {
  int i=0;
  while((str1[i]=str2[i]) != '\0')
    i++;
  }

void strcopy(char *strl ,char *str2) 
  {
  int i=0;
  while((str1[i]=str2[i]) != '\0')
	i++;
  }

void strcopy(char *strl ,char *str2) 
  {
  int i=0;
  while((*(str1+i)= *(str2+i)) != '\0')
	i++;
  }


void strcopy(char *strl ,char *str2) 
  {
  while((*str1 = *str2) != '\0') 
    {
	str1++;
	str2++;
	}
  }

void strcopy(char *strl ,char *str2) 
  {
  while (*str1++ = *str2++);
  }

Beispiel: Zeichenketten und Pointer:

void main(void)
  {
  int   i = 0;
  char  zeichenKette1[]   = "Hier stehen ganz viele Zeichen",
        zeichenKette2[32] = {'D','i','e',' ','z','w','e','i','t','e',
                             ' ','Z','e','i','c','h','e','n','k','e',
                             't','t','e','\0'},
       *zeichenKette3     = "Heiter geht's weiter!",
       *zeichenPtr        = NULL;                           
                         
  printf("String 1: %s\n",zeichenKette1);
  printf("String 2: %s\n",zeichenKette2);
  printf("String 3: %s\n\n",zeichenKette3);

  printf("Zeichen 5 aus String 1: %c\n",zeichenKette1[5]);
  printf("String 1 ab Zeichen 5: %s\n\n", &zeichenKette1[5]);

  zeichenPtr = zeichenKette2;

  printf("ZeichenPtr     zeigt auf: %s\n",zeichenPtr);
  printf("ZeichenPtr + 4 zeigt auf: %s\n",zeichenPtr+4);

 for(i = 0; i < 24; i++)
   {
   printf("%c",*(zeichenPtr+i));
   }
 printf("\n\n");
 }

Ausgabe:

String 1: Hier stehen ganz viele Zeichen
String 2: Die zweite Zeichenkette
String 3: Heiter geht's weiter!

Zeichen 5 aus String 1: s
String 1 ab Zeichen 5: stehen ganz viele Zeichen

ZeichenPtr     zeigt auf: Die zweite Zeichenkette
ZeichenPtr + 4 zeigt auf: zweite Zeichenkette
Die zweite Zeichenkette

Im nächsten Programmbeispiel werden drei Versionen einer Funktion vom Datentyp int einander gegenübergestellt, die zur Ermittlung der Länge einer Zeichenkette dienen.

   
#include <stdio.h>

int strlen0(char s[]);
int strlen1(char *s);
int strlen2(char *s);

int main(void) /* Laenge einer Zeichenkette ermitteln */
  {
  char name[51];

  printf("Zeichenkette mit max. 50 Zeichen eingeben:\n");
  scanf("%s",name);
  printf(" name: %s\n",name);
  printf("Laenge von name mit strlen0: %d\n",strlen0(name));
  printf("Laenge von name mit strlen1: %d\n",strlen1(name));
  printf("Laenge von name mit strlen2: %d\n",strlen2(name));
  }

int strlen0(char s[])  /* liefert die Laenge von s */
   {
   int i;
   for (i = 0; s[i] != '\0'; i++)
     ;
   return i;
   }  

int strlen1(char *s)  /* 1.Zeigerversion */
   {
   int i;
   for (i = 0; *s != '\0'; s++, i++)
     ;
   return i;
   }
     
int strlen2(char *s)  /* 2.Zeigerversion */
   {
   char *p;
   for (p = s; *p != '\0'; p++)
     ;
   return p - s;
   }
Die Funktion strlen0 verwendet keine Zeiger und hat als Parameter ein Feld vom Datentyp char unbestimmter Länge. In der for Anweisung der Funktion strlen0 wird die Laufvariable i ausgehend vom Anfangswert Null solange inkrementiert, wie das i-te Zeichen der beim Aufruf übergebenen Zeichenkette vom Nullzeichen '\0' am Ende der Zeichenkette verschieden ist. Der Wert von i, für den s[i] gleich '\0' gilt, wird nicht mehr inkrementiert und über die return Anweisung als Länge der Zeichenkette an die rufende Funktion main zurückgegeben.

Die Funktionen strlen1 und strlen2 haben jeweils einen Zeiger auf den Datentyp char als Parameter. Wenn beim Aufruf von strlen1 eine Zeichenkette als Argument übergeben wird, wird die Adresse des ersten Elements übergeben, so daß der Zeiger s auf das erste Element der übergebenen Zeichenkette zeigt. In der for Anweisung von strlen1 wird geprüft, ob das Objekt, auf welches s zeigt, vom Nullzeichen am Ende der Zeichenkette verschieden ist. Ist dies nicht der Fall, werden der Zeiger s und die Laufvariable i inkrementiert. Durch die Inkrementierung von s weist diese Zeigervariable auf das nächste Element der beim Aufruf übergebenen Zeichenkette. Wenn aber der Zeiger s auf das Nullzeichen am Ende der Zeichenkette zeigt, findet keine Inkrementierung von s und i mehr statt. Der Wert von i wird durch die return Anweisung an main zurückgegeben.

In der Funktion strlen2 wird p als Zeiger auf den Datentyp char vereinbart. Im ersten Ausdruck in der for Anweisung wird p so initialisiert, daß p auf das erste Zeichen der als Argument übergebenen Zeichenkette zeigt. In der Schleifenbedingung wird geprüft, ob das Nullzeichen am Ende der Zeichenkette erreicht ist. Ist dies noch nicht der Fall, wird der Zeiger p inkrementiert, so daß er auf das nächste Element der Zeichenkette zeigt. Ist das Nullzeichen aber erreicht, findet eine Inkremetierung von p nicht mehr statt. Die Zeigerdifferenz p-s, die als Ergebnis zurückgegeben wird, gibt die Anzahl der Inkrementierungen von p an, also die Länge der Zeichenkette.

Das folgende Beispiel zeigt, wie man ein Array von Zeigern initialisieren kann:

char *month_name(int n)
  {
  static char *name[] = {  /* Array von Zeigern */
    "falscher Monat",      /* Ein String ist ein char-Array */
    "Januar",              /* und daher durch seine */
    "Februar",             /* Anfangsadresse charakterisiert */
    "Maerz",
    "April",
    "Mai",
    "Juni",
    "Juli",
    "August",
    "September",
    "Oktober",
    "November",
    "Dezember"
    };
 
  return ((n < 1 || n > 12) ? name[0] : name[n]);
  }

Beispiel: Sortieren von n eingegebenen int-Werten
An die Sortierfunktion ssort werden nur zwei Zeiger übergeben, die auf das erste und letzte Element des Feldes zeigen. Innerhalb von ssort wird auch nur mit Zeigern gearbeitet.


#include <stdio.h>
#define MAX 100

void ausgabe(char *text, int *feld, int m);
void ssort(int *first, int *last);

int main(void)
  {	
  int feld[MAX];
  int m, anz;

  printf("Eingabe von n int-Werten (unsortiert) - bis EOF\n");
  for (m = 0; m < MAX; m++) 
    { /* Einlesen bis EOF erreicht wurde
          oder bis MAX Werte eingelesen wurden */
    anz = scanf("%d", &feld[m]);
    if (anz == EOF) break;
	}	
  printf("\n");

  ausgabe("Unsortiert:",feld, m);

  ssort(&feld[0], &feld[m]);    
             /* oder kuerzer: ssort(feld, &feld[m]); */
  
  ausgabe("Sortiert:",feld, m);

  return(0);
  }

void ausgabe(char* text, int *feld, int m)
  { /* Array ausgeben */
  int j;

  printf("%s\n",text);
  for(j = 0; j < m; j++) 
    {
	printf(" %6d\n", feld[j]);
    }
  printf("\n");
  }
    
void ssort(	int *first, int *last)
  /* Sortieren durch direktes Einfuegen */
  {	
  int *i, *j, temp;

  for(i = first; i < last; i++) 
    {
    for(j = i-1; j >= first && *j > *(j+1); j--) 
      {	
      temp = *(j+1);
      *(j+1) = *j;
      *j = temp;
      }
    }
  }

Zeiger auf mehrdimensionale Arrays

Zur Erinnerung: bei statischer Speicherbelegung sind mehrdimensionale Arrays intern eindimensional angelegt, die Arrayzeilen liegen alle hintereinander. Definiert man beispielsweise

float matrix[N][M];

Dann berechnet sich die die Adresse von matrix[1][2] mit der Zeigerarithmetik zu:

Der letzte Ausdruck ist von der Form, wie er intern tatsächlich berechnet wird. Die Zeilenlänge M muß dem Compiler also explizit bekannt sein!

Alternative: Adressen zu den Zeilenanfängen können explizit abgespeichert werden.
Beispiel:

   int  aa[N][M], *a[N], i;
   for(i=0; i<N; i++)
      a[i] = aa[i];
ab dann nur noch Verwendung von a[k][l].

"echt 2-dimensional"; Adressen werden nicht berechnet, sondern nur gelesen; Zeilenlänge muß dazu nicht bekannt sein. Nachteil: es ist (geringfügig) mehr Speicherplatz nötig.

Zeiger auf Strukturen

Strukturpointer finden so häufig Verwendung, daß es dafür eine eigene Syntax gibt; bei gegebener Adresse auf eine Strukturvariable werden deren Elemente mit '->' angesprochen, in der Form:

Zeiger auf Strukturvariable ->Elementname

Beispiel: mit der Strukturdefinition

struct point 
  {
  double  px, py;
  int  farbe;
  }  punkt, sprueh[100];
sind folgende Aufrufe äquivalent:

punkt.pxentspricht(&punkt)->px
sprueh[20].pxentspricht(sprueh+20)->px

Nun wird noch ein Zeiger auf punkt definiert:

struct point *zeiger;
Mit der Anweisung zeiger = &punkt; kann man zeiger auf punkt zeigen lassen. Der Zugriff auf die Komponente px von punkt würde in reiner Pointerschreibweise lauten (erster Versuch):
irgendwas = *zeiger.px;
Diese Notation ist aber zweideutig. Bedeutet das jetzt "Das worauf zeiger zeigt, Komponente px" oder "Das worauf zeiger.px zeigt"? Diese Zweideutigkeit wird man los durch (zweiter Versuch):
irgendwas = (*zeiger).px;
Nun ist es eindeutig "Das worauf zeiger zeigt, Komponente px". Der Operator -> vereinfacht das nun nur noch und wir schreiben (Endfassung):
irgendwas = zeiger->>px;

Das folgende Beispiel beginnt mit der Deklaration eines namenlosen struct-Typs für Variablen, die Personaldaten von Angestellten (Name, Personalnummer, Gehalt) aufnehmen. Ausserdem werden drei struct-Variablen ang1, ang2, ang3 und eine Zeigervariable auf solche structs definiert:

#include <stdio.h>
#include <string.h>

main() 
  {
  struct personal 
    {
    char  name[20];
    int   pers_nr;
    float gehalt;
    };
  struct personal ang_1 = { "Gundel Gakeley", 3344, 5000.00 );
  struct personal ang_2, ang_3, *ang_zeiger;

  /* Uebertragung aller Werte von ang1 nach ang2 */
  ang_2 = ang_1;

  /* Setzen der Werte von ang3 durch Einzelzuweisungen */
  ang_3.pers_nr = 4733;
  ang_3.gehalt  = ang_1.gehalt + ang_2.gehalt;
  strcpy(ang_3.name,"Donald Duck");

  /* Zuweisung der Adresse von ang1 an ang_zeiger */
  ang_zeiger = &ang_1;

  /* Aenderung der Namenskomponente von ang_1 ueber Pointer */
  strcpy(ang_zeiger->name,"Dagobert Duck");
  }

6.5 Zeiger-Array

Zeiger-Arrays sind Arrays deren Komponenten Zeiger sind. Genauso wie man Vektoren aus den Grunddatentypen (char, int, float und double) bilden kann, kann man dies auch mit Zeigern tun. Ein Vektor von Zeigern wird so definiert:
    Grunddatentyp *Vektorname []
gelesen von rechts nach links (wegen des Vorranges von []): Vektor von Zeigern. Dagegen ist
    (*Vektorname) []
ein Zeiger auf einen Vektor!

Die Komponenten des Vektors können natürlich auch Adressen von Arrays sein. Damit ergibt sich eine Ähnlichkeit zu mehrdimensionalen Arrays.

Beispiel:

char *Z[3];    /* Definition für ein Array mit 3 Elementen, 
                  die jeweils Zeiger auf char-Typen sind. 
                  Z ist also ein Zeiger auf ein Array mit 
                  3 char-Zeigern */

char F1 [4],   /* Definition für 3   */
     F2 [4],   /* char-Arrays mit jeweils */
     F3 [4];   /* der Länge 4        */

Z[0] = F1;   /* Wertzuweisung an die char-Zeiger */
Z[1] = F2;
Z[2] = F3;
Die Situation stellt sich bildlich folgendermaßen dar:

Man sieht folgende Analogie zwischen Zeiger-Arrays und mehrdimensionalen Arrays:
Z+i ist ein Zeiger auf das (i+1)-te Element des Zeiger-Arrays Z
*(Z+i) = Z[i] ist das Array, auf das das (i+1)-te Element des Zeiger-Arrays zeigt, also ein Zeiger auf das erste Element dieses Arrays.
*(Z+i) + j = Z[i] + j ist ein Zeiger auf das (j+1)-te Element des Arrays, auf das das (i+1)-te Element des Zeiger-Arrays Z zeigt.
*(*(Z+i) + j) = *(Z[i] + j) =
= Z [i][j] = (*(pa+i)) [j]
ist das (j+1)-te Element des Arrays, auf das das (i+1)-te Element des Zeiger-Arrays Z zeigt.

Das obige Beispiel geht davon aus, dass die Elemente des Zeiger-Arrays alle auf Arrays gleicher Größe verweisen. Dies bedingt die große Ähnlichkeit zu mehrdimensionalen Arrays. Bei häufigen Anwendungen von Zeiger-Arrays besitzen die referenzierten Arrays unterschiedliche Längen, z.B. verschieden lange Strings.


/* Programm: ZEIGER-Array                           */
/* Aehnlichkeit und Unterschied zu mehrdim. Arrays  */

#include <stdio.h>

char TagFeld[][11] = 
  {  
  "Montag",     /* Dies ist ein 2-dimensionales Array */
  "Dienstag",   /* Die zweite Dimension ist fest 11,  */
  "Mittwoch",   /* die erste wird durch die Initialisierung */
  "Donnerstag", /* festgelegt mit 7 - für jeden String */
  "Freitag",    /* werden genau 11 Zeichen reserviert  */
  "Samstag",
  "Sonntag" 
  };

char *TagZeiger[] =    
  { 
  "Montag",     /* Dies ist ein Zeigerarray mit genau */
  "Dienstag",   /* 7 Zeigern als Elemente. Die Strings */
  "Mittwoch",   /* besitzen unterschiedliche Laenge */
  "Donnerstag",
  "Freitag",
  "Samstag",
  "Sonntag"
  };


int IndexLesen(int *,int *);
  
int main (void)  
  {
  int i, j;
  char Ca, Cb;

  while (IndexLesen(&i, &j) >= 0) 
    {
    /* Zugriff auf das 2-dimensionale Array */
    Ca = TagFeld[i][j];      
    /* Zugriff über das Zeiger-Array */
    Cb = TagZeiger[i][j];    

    printf("TagFeld [%2d][%2d]   = %c (= %2x hex)\n", i, j,Ca,Ca);
    printf("TagZeiger [%2d][%2d] = %c (= %2x hex)\n",i,j,Cb,Cb);
    }

  return(0);
  }

int IndexLesen(int *I, int *J) 
  {
  printf("\nZeile:  "); scanf("%d",I);
  if(*I < 0) return -1;
  printf("\nSpalte: "); scanf("%d",J);
  if(*J < 0) return -1;
  return(1);
  }

Das Ergebnis eines Probelaufs sieht so aus:

Zeile:  0 
Spalte: 4
TagFeld [ 0][ 4]   = a (= 61 hex)
TagZeiger [ 0][ 4] = a (= 61 hex)

Zeile:  2 
Spalte: 9 
TagFeld [ 2][ 9]   =   (=  0 hex)    /* Wie laesst sich dieser Unterschied     */
TagZeiger [ 2][ 9] = D (= 44 hex)    /* erklaeren ?     */

Zeile:  -1  

Das folgende Beispiel zeigt, wie man einen Vektor von Zeigern initialisieren kann:

char *month_name(int n)       /* liefert Name des n. Monats */
  {
  static char *name[] = {     /* Vektor von Zeigern */
        "falscher Monat",     /* String ist ein char- */
        "Januar",             /* Vektor und daher durch */
        "Februar",            /* seine Anfangsadresse */
        "Maerz",              /* charakterisiert */
        "April",
        "Mai",
        "Juni",
        "Juli",
        "August",
        "September",
        "Oktober",
        "November",
        "Dezember"
        };
  return ((n < 1 || n > 12) ? name[0] : name[n]);
  }
Übrigens: Zeiger auf Zeiger sind eine äquivalente Formulierung für Vektoren von Zeigern:
int *iff[];       /* sind äquivalent, da iff die
int **iff;           Adresse des Vektors enthält */

6.6 Zeiger auf Funktionen

In C kann man nicht nur Zeiger auf Datentypen, sondern auch auf Funktionen definieren, die man dann wie Variablen verwenden kann. Damit wird es möglich, Funktionen an Funktionen zu übergeben. Der Zeiger enthät dann die Startadresse der Funktion im Arbeitsspeicher. Ein Zeiger auf eine Funktion wird folgendermaßen vereinbart:
Rückgabetyp (*Funktionsname) (Funktionsargumente)
Beachten Sie die Klammer um den Stern und den Funktionsnamen (*Funktionsname). Die runde Klammer ist zwingend erforderlich - vgl. Prioritäten!

Im Gegensatz dazu definiert *Funktionsname () eine Funktion, die einen Zeigerwert liefert.

Ein erstes Beispiel:

double (*ZFunc) (int i); 
ZFunc ist ein Zeiger auf eine Funktion, die einen int-Parameter erwartet und einen double-Wert zurückliefert.

Was kann man mit Funktionszeigern anstellen? Es ist möglich,

Die Wertzuweisung an eine Funktionszeigervariable erfolgt wie bei den Arrays, so ist auch bei Funktionen der Name allein als Zeiger auf die Funktion festgelegt. Es wird kein &-Operator angewendet, der Funktionsname ist eine Adreßkonstante! Beispiel:

int Funkt(int);           /*  Deklaration der Funktion Funkt() */
int (*ZFunkt) (int);      /* Definition der Funktionszeigervariablen */

ZFunkt = Funkt;           /* Zuweisung der Adresse der Funktion */
                          /* als Wert an den Zeiger */

Der Aufruf einer Funktion über einen Funktionszeiger erfolgt durch Dereferenzieren (mit *) des Zeigers und Angabe der aktuellen Parameterliste. Zum Beispiel:

Ergebnis = (*ZFunkt)(2*i); 

Es stellt sich die Frage, wozu sowas nötig sein sollte. Man kann auf diese Weise allgemein verwendbare Funktionen programmieren. Angenommen, sie wollen eine Funktion schreiben, die den Graphen einer beliebigen Funktion zeichnet. Ohne Funktionszeiger müßte die Funktion jeweils im Quellcode eingefügt und dann das Programm neu compiliert und ausgeführt werden. Natürlich kann man sowas auch automatisieren. Ein Programm öffnet eine Quelldatei, modifiziert sie, startet die Compilierung und führt schließlich das erzeugte Programm aus. Das Ganze ist aber recht kompliziert. Einfacher und eleganter ist es da sicher, an die Grafikfunktion einfach einen Zeiger auf eine beliebige Funktion zu übergeben.

Ein erstes Beispiel:

#include <stdio.h>

int i;
int Funzel();             /*  Deklaration der Funktion */
int (*ZFunkt) ();         /* Definition des Funktionszeigers */

int (*druck) (char *format,...); /* ja, das geht :-) */

int main(void)
  {
  ZFunkt = Funzel;        /* Zuweisung der Funktionsadresse */
  i = (*ZFunkt)();        /* Funktion ausfuehren */

  /* Stimmen die Adressen? */
  printf("Adresse Funzel(): %p, Adresse ZFunkt: %p\n", ZFunkt, Funzel);

  druck = printf;         /* Zuweisung der Funktionsadresse */
  /* druck ausfuehren: */
  (*druck) ("Sie mal einer da! I war uebrigens: %d\n", i);

  return(0);
  }

int Funzel()
  {
  printf("Funzel was here!\n");
  return(42);
  }
Die Ausgabe sieht etwa so aus:
Funzel was here!
Adresse Funzel(): 0x401120, Adresse ZFunkt: 0x401120
Sie mal einer da! I war uebrigens: 42

Natürlich sind auch Arrays von Funktionspointern möglich, Beispiel:

double (*trig[3])(double), x;

trig[0] = exp;
trig[1] = sin;
trig[2] = cos;

Die Aufrufe wie z. B. sin(x) und trig[1](x) sind dann äquivalent. Funktionen können so mit einem Index versehen werden.

Funktionspointer und Funktionvariable erlauben es, generische Funktionen zu schreiben. Als Beispiel diene hier die Bibliotheksfunktion qsort() aus <stdlib.h>:

void qsort(void *base, size_t nel, size_t width,
          int (*compar) (const void *, const void *));
qsort() sortiert ein Array base[0] bis base[nel-1] von Objekten der Größe width in aufsteigender Reihenfolge. Die Vergleichsfunktion compar() gibt einen negativen Wert zurück, wenn ihr erstes Argument kleiner ist als das zweite, Null wenn die Argumente gleich sind und einen positiven Wert, wenn das zweite größer als das erste ist.

In qsort() werden Vergleiche immer mit der Funktion compar() vorgenommen:

void qsort(void *base, size_t nel, size_t width,
          int (*compar) (const void *, const void *))
  {
    ...

  if ((*compar)(base[i], base[j]) < 0)
    swap(base, i, j);

    ...

  }
Mit der Wahl einer geeigneten Vergleichsfunktion können also Arrays beliebigen Typs sortiert werden. Beispiel:
#include <stdlib.h>
#include <string.h>

int agecompare(const void *i, const void *j);   /* Alter vergleichen ... */
int namecompare(const void *i, const void *j);  /* Namen vergleichen ... */

typedef struct
        {    
        char *name;
        int age;
        } person;
        
int main(void)
  {
  person a[ARRAYSIZE];

  ...                /* Initialisierungen ... */

  /* a nach Alter sortieren */
  qsort(a, ARRAYSIZE, sizeof(person), agecompare);
  
  ...
  
  /* a nach Namen sortieren */
  qsort(a, ARRAYSIZE, sizeof(person), namecompare);
  
  ...

  }

int agecompare(const void *i, const void *j)
  {
  int ii,jj;

  ii = ((person*)i)->age;
  jj = ((person*)j)->age;

  if ( ii > jj) return 1;
  if ( jj > ii) return -1;
  return(0);
  }

int namecompare(const void *i, const void *j)
  {
  char *ii, *jj;

  ii = ((person*)i)->name;
  jj = ((person*)j)->name;

  return strcmp(ii, jj);
  }

Programmierbeispiel: Integration nach Simpson

#include <stdio.h>
#include <math.h>

double dkreis (double dX);
double simpson (double dXu, double dXo, double dEps, 
                 double (*f)(double dVal));


/* oberer Halbkreis mit Radius 1 */
/* Flaeche ergibt Pi/2.          */
double dkreis (double dX)
  {
  double dR = 1.;
  double dY;

  if (dX >  1.)   return 0.;
  if (dX < -1.)   return 0.;
  dY = sqrt (dR*dR -dX*dX);
  return dY;
  }


double simpson( 
        double dXu;                 /* untere Grenze                */                   
        double dXo;                 /* obere  Grenze                */                   
        double dEps;                /* Genauigkeit                  */                   
        double (*f)(double dVal))  /* zu integrierende Funktion    */
  {
  int      i;
  int      nrun = 0;           /* Laufkenner               */
  double  dFl = 0.;           /* letzter Integralwert     */
  double  dFa = 0.;           /* aktueller Integralwert   */
  double  dH;                 /* Schrittweite             */
  double  dX;                 /* X-Wert                   */    
  double  dY;                 /* Funktionswert            */    
  double  dg;                 /* Gewicht                  */
  double  dErr;               /* Fehler                   */
  int      n = 2;              /* Inkrementierung          */    
    
  dFa = 0.;

  /* Schleife über alle Integrationen */
  while (1)
    {

    dFa = 0.;
    dH  = (dXo -dXu)/(double)n;  /* Schrittweite bestimmen   */
    for (i=0; i<=n; i++)
      {
      dX   = dXu +dH*(double)i;
      dY   = f (dX);
      if (i == 0 || i== n)  dg = 1.;
      else if (i%2 == 1)         dg = 4.;
      else if (i%2 == 0)         dg = 2.;
      else                       dg = 0.;
      dFa += dg*dY; 
      }
    dFa *= dH/3.;
    dErr = fabs((dFa -dFl)/dFa);
    if (nrun && dErr < dEps) break;
    n*=2;
    nrun++;
    dFl = dFa;
    }
  return dFa;
  }    


void main()
  {
  double dXo  =  1.;
  double dXu  = -1.;
  double dEps = 1.e-6;
  double dF;
    
  dF = simpson (dXu,dXo,dEps, dkreis );
  printf (" Xu: %8.4f, Xo: %8.4f, F: %12.8f Eps: %g\n",dXu,dXo,dF,dEps);
  }

6.7 Wiederholung: Argumente der Kommandozeile

Im allgemeinen hat diese Funktion main zwei Parameter: argc und argv, die Angaben über die Kommandoparameter beim Programmaufruf enthalten.

argc ist ein int-Parameter und enthält die Anzahl der Paramter der Kommandozeile einschließlich des Programmaufrufes selbst (hat also immer mindestens den Wert 1).

argv ist ein Vektor mit Zeigern auf Zeichenketten. Diese Zeichenketten enthalten die Aufrufparameter der Kommandozeile, wobei der 1. Parameter der Programmaufruf selbst ist. Letztes Argument ist die Vektorkomponente argv[argc - 1].

Beispiel: Programm echo gibt alle Aurgumente auf die Standardausgabe aus. Der Programmaufruf auf Betriebssystemebene sieht so aus:

echo Hier kommt das Echo!
Die Werte von argc und argv im aufgerufenen Programm lauten:
argc = 5
argv[0]: "echo"
argv[1]: "Hier"
argv[2]: "kommt"
argv[3]: "das"
argv[4]: "Echo!"

Beispielprogramm: 3 Versionen von echo

/* 1. Version */
main(int argc, char *argv[])
  {
  int i;
  for (i=1; i < argc; i++)
    printf("%s",argv[i]);
  printf("\n");
  }

/* 2.Version */
main(int argc, char *argv[])
  {
  while (--argc > 0)
    printf("%s",*++argv);
  printf("\n");
  }

/* 3.Version */
main(int argc, char **argv)
  {
  while (--argc > 0)
    printf("%s ",*++argv);
  printf("\n");
  }

Zum Inhaltsverzeichnis Zum nächsten Abschnitt


Copyright © FH München, FB 04, Prof. Jürgen Plate