C-Programmierung: Zeiger

Aus Wikibooks
Wechseln zu: Navigation, Suche

Eine Variable wurde bisher immer direkt über ihren Namen angesprochen. Um zwei Zahlen zu addieren, wurde beispielsweise der Wert einem Variablennamen zugewiesen:

 summe = 5 + 7;

Eine Variable wird intern im Rechner allerdings immer über eine Adresse angesprochen (außer die Variable befindet sich bereits in einem Prozessorregister). Alle Speicherzellen innerhalb des Arbeitsspeichers erhalten eine eindeutige Adresse. Immer wenn der Prozessor einen Wert aus dem RAM liest oder schreibt, schickt er diese über den Systembus an den Arbeitsspeicher.

Eine Variable kann in C auch direkt über die Adresse angesprochen werden. Eine Adresse liefert der &-Operator (auch als Adressoperator bezeichnet). Diesen Adressoperator kennen Sie bereits von der scanf-Anweisung:

 scanf("%i", &a);

Wo diese Variable abgelegt wurde, lässt sich mit einer printf Anweisung herausfinden:

 printf("%p\n", &a);

Der Wert kann sich je nach Betriebssystem, Plattform und sogar von Aufruf zu Aufruf unterscheiden. Der Platzhalter %p steht für das Wort Zeiger (engl.: pointer).

Eine Zeigervariable dient dazu, ein Objekt (z.B. eine Variable) über ihre Adresse anzusprechen. Im Gegensatz zu einer "normalen" Variable, erhält eine Zeigervariable keinen Wert, sondern eine Adresse.

Beispiel[Bearbeiten]

Im folgenden Programm wird die Zeigervariable a deklariert:

  1. #include <stdio.h>
    
  2.  
    
  3. int main(void)
    
  4. {
    
  5.   int *a, b;
    
  6.  
    
  7.   b = 17;
    
  8.   a = &b;
    
  9.   printf("Inhalt der Variablen b:    %i\n", b);
    
  10.   printf("Inhalt des Speichers der Adresse auf die a zeigt:    %i\n", *a);
    
  11.   printf("Adresse der Variablen b:   %p\n", &b);
    
  12.   printf("Adresse auf die die Zeigervariable a verweist:   %p\n", (void *)a);
    
  13.   /* Aber */
    
  14.   printf("Adresse der Zeigervariable a: %p\n", &a);
    
  15.   return 0;
    
  16. }
    
Abb. 1 - Das (vereinfachte) Schema zeigt wie das Beispielprogramm arbeitet. Der Zeiger a zeigt auf die Variable b. Die Speicherstelle des Zeigers a besitzt lediglich die Adresse von b (im Beispiel 1462). Hinweis: Die Adressen für die Speicherzellen sind erfunden und dienen lediglich der besseren Illustration.

In Zeile 5 wird die Zeigervariable a deklariert. Dabei wird aber kein eigener Speicherbereich für die Variable a selbst bereitgestellt, sondern ein Speicherbereich für die Adresse! Außerdem wird eine Integervariable des Typs int deklariert. Bitte beachten Sie, dass die Anweisung

 int* a, b;

einen Zeiger auf die Integer-Variable a und nicht die Integer-Variable b deklariert (b ist also kein Zeiger!). Deswegen sollte man sich angewöhnen, den Stern zum Variablennamen und nicht zum Datentyp zu schreiben:

 int *a, b;

Diese Schreibweise verringert die Verwechslungsgefahr deutlich.

Nach der Deklaration hat die Zeigervariable a einen nicht definierten Inhalt. Die Anweisung a=&b in Zeile 8 weist a deshalb eine neue Adresse zu. Damit zeigt die Variable a nun auf die Variable b.

Die printf-Anweisung gibt den Wert der Variable aus, auf die der Zeiger verweist. Da ihr die Adresse von b zugewiesen wurde, wird die Zahl 17 ausgegeben.

Ob Sie auf den Inhalt der Adresse auf den die Zeigervariable verweist oder auf die Adresse auf den die Zeigervariable verweist zugreifen, hängt vom * - Operator ab:

  • *a = greift auf den Inhalt der Zeigervariable zu. Der *-Operator wird auch als Inhalts- oder Dereferenzierungs-Operator bezeichnet.
  • a = greift auf die Adresse, auf die die Zeigervariable verweist, zu.

Will man aber die Adresse der Zeigervariable selbst haben, so muss man den & Operator wählen. Also so: &a.

Ein Zeiger darf nur auf eine Variable verweisen, die denselben Datentyp hat. Ein Zeiger vom Typ int kann also nicht auf eine Variable mit dem Typ float verweisen. Den Grund hierfür werden Sie im nächsten Kapitel kennen lernen. Nur so viel vorab: Der Variablentyp hat nichts mit der Breite der Adresse zu tun. Diese ist systemabhängig immer gleich. Bei einer 16 Bit CPU ist die Adresse 2 Byte, bei einer 32 Bit CPU 4 Byte und bei einer 64 Bit CPU 8 Byte breit - unabhängig davon, ob die Zeigervariable als char, int, float oder double deklariert wurde.

Zeigerarithmetik[Bearbeiten]

Es ist möglich, Zeiger zu erhöhen und damit einen anderen Speicherbereich anzusprechen, z. B.:

  1. #include <stdio.h>
    
  2.  
    
  3. int main()
    
  4. {
    
  5.   int x = 5;
    
  6.   int *i = &x;
    
  7.   printf("Speicheradresse %p enthält %i\n", (void *)i, *i);
    
  8.   i++; // nächste Adresse lesen
    
  9.   printf("Speicheradresse %p enthält %i\n", (void *)i, *i);  
    
  10.   return 0;
    
  11. }
    

i++ erhöht hier nicht den Inhalt (*i), sondern die Adresse des Zeigers (i). Man sieht aufgrund der Ausgabe auch leicht, wie groß ein int auf dem System ist, auf dem das Programm kompiliert wurde. Im folgenden handelt es sich um ein 32-bit-System (Differenz der beiden Speicheradressen 4 Byte = 32 Bit):

Speicheradresse 134524936 enthält 5
Speicheradresse 134524940 enthält 0

Um nun den Wert im Speicher, nicht den Zeiger, zu erhöhen, wird *i++ nichts nützen. Das ist so, weil der Dereferenzierungsoperator * die niedrigere Priorität hat als das Postinkrement (i++). Um den beabsichtigten Effekt zu erzielen, schreibt man (*i)++, oder auch ++*i. Im Zweifelsfall und auch um die Les- und Wartbarkeit zu erhöhen sind Klammern eine gute Wahl.

Zeiger auf Funktionen[Bearbeiten]

Zeiger können nicht nur auf Variablen, sondern auch auf Funktionen verweisen, da Funktionen nichts anderes als Code im Speicher sind. Ein Zeiger auf eine Funktion erhält also die Adresse des Codes.

Mit dem folgenden Ausdruck wird ein Zeiger auf eine Funktion definiert:

 int (*f) (float);

Diese Schreibweise erscheint zunächst etwas ungewöhnlich. Bei genauem Hinsehen gibt es aber nur einen Unterschied zwischen einer normalen Funktionsdefinition und der Zeigerschreibweise: Anstelle des Namens der Funktion tritt der Zeiger. Der Variablentyp int ist der Rückgabetyp und float der an die Funktion übergebene Parameter. Die Klammer um den Zeiger darf nicht entfernt werden, da der Klammeroperator () eine höhere Priorität als der Dereferenzierungsoperator * hat.

Wie bei einer Zeigervariable kann ein Zeiger auf eine Funktion nur eine Adresse aufnehmen. Wir müssen dem Zeiger also noch eine Adresse zuweisen:

 int (*f) (float);
 int func(float);
 f = func;

Die Schreibweise (f = func) ist gleich mit (f = &func), da die Adresse der Funktion im Funktionsnamen steht. Der Lesbarkeit halber sollte man nicht auf den Adressoperator(&) verzichten.

Die Funktion können wir über den Zeiger nun wie gewohnt aufrufen:

 (*f)(35.925);

oder

 f(35.925);

Hier ein vollständiges Beispielprogramm:

  1. #include <stdio.h>
    
  2.  
    
  3. int zfunc( )
    
  4. {
    
  5.     int var1 = 2009;
    
  6.     int var2 = 6;
    
  7.     int var3 = 8;
    
  8.  
    
  9.     printf( "Das heutige Datum lautet: %d.%d.%d\n", var3, var2, var1 );
    
  10.     return 0;
    
  11. }
    
  12.  
    
  13. int main( void )
    
  14. {
    
  15.     int var1 = 2010;
    
  16.     int var2 = 7;
    
  17.     int var3 = 9;
    
  18.     int ( *f )( );
    
  19.  
    
  20.     f = &zfunc;
    
  21.  
    
  22.     printf( "Ich freue mich schon auf den %d.%d.%d\n", var3, var2, var1 );
    
  23.     f( );
    
  24.     printf( "Die Adresse der Funktion im RAM lautet: %p\n", (void *)f );
    
  25.     return 0;
    
  26. }
    

void-Zeiger[Bearbeiten]

Der void-Zeiger ist zu jedem Datentyp kompatibel (Achtung, anders als in C++). Man spricht hierbei auch von einem untypisierten oder generischen Zeiger. Das geht so weit, dass man einen void Zeiger in jeden anderen Zeiger umwandeln kann, und zurück, ohne dass die Repräsentation des Zeigers Eigenschaften verliert. Ein solcher Zeiger wird beispielsweise bei der Bibliotheksfunktion malloc benutzt. Diese Funktion wird verwendet um eine bestimmte Menge an Speicher bereitzustellen, zurückgegeben wird die Anfangsadresse des allozierten Bereichs. Danach kann der Programmierer Daten beliebigen Typs dorthin schreiben und lesen. Daher ist Pointer-Typisierung irrelevant. Der Prototyp von malloc ist also folgender:

 void *malloc(size_t size);

Der Rückgabetyp void* ist hier notwendig, da ja nicht bekannt ist, welcher Zeigertyp (char*, int* usw.) zurückgegeben werden soll. Vielmehr ist es möglich, den Typ void* in jeden Zeigertyp zu "casten" (umzuwandeln, vgl. type-cast = Typumwandlung).

Der einzige Unterschied zu einem typisierten ("normalen") Zeiger ist, dass die Zeigerarithmetik schwer zu bewältigen ist, da dem Compiler der Speicherplatzverbrauch pro Variable nicht bekannt ist (wir werden darauf im nächsten Kapitel noch zu sprechen kommen) und man in diesem Fall sich selber darum kümmern muss, dass der void Pointer auf der richtigen Adresse zum Liegen kommt. Zum Beispiel mit Hilfe des sizeof Operator.

 int *intP;
 void *voidP;
 voidP = intP;         /* beide zeigen jetzt auf das gleiche Element */
 intP++;               /* zeigt nun auf das nächste Element */
 voidP += sizeof(int); /* zeigt jetzt auch auf das nächste int Element */

Unterschied zwischen Call by Value und Call by Reference[Bearbeiten]

Eine Funktion dient dazu, eine bestimmte Aufgabe zu erfüllen. Dazu können ihr Variablen übergeben werden oder sie kann einen Wert zurückgeben. Der Compiler übergibt diese Variable aber nicht direkt der Funktion, sondern fertigt eine Kopie davon an. Diese Art der Übergabe von Variablen wird als Call by Value bezeichnet.

Da nur eine Kopie angefertigt wird, gelten die übergebenen Werte nur innerhalb der Funktion selbst. Sobald die Funktion wieder verlassen wird, gehen alle diese Werte verloren. Das folgende Beispiel verdeutlicht dies:

  1. #include <stdio.h>
    
  2.  
    
  3. void func(int wert)
    
  4. {
    
  5.   wert += 5;
    
  6.   printf("%i\n", wert);
    
  7. }
    
  8.  
    
  9. int main()
    
  10. {
    
  11.   int zahl = 10;
    
  12.   printf("%i\n", zahl);
    
  13.   func(zahl);
    
  14.   printf("%i\n", zahl);
    
  15.   return 0;
    
  16. }
    

Das Programm erzeugt nach der Kompilierung die folgende Ausgabe auf dem Bildschirm:

10
15
10

Dies kommt dadurch zustande, dass die Funktion func nur eine Kopie der Variable wert erhält. Zu dieser Kopie addiert dann die Funktion func die Zahl 5. Nach dem Verlassen der Funktion geht der Inhalt der Variable wert verloren. Die letzte printf Anweisung in main gibt deshalb wieder die Zahl 10 aus.

Eine Lösung wurde bereits im Kapitel Funktionen angesprochen: Die Rückgabe über die Anweisung return . Diese hat allerdings den Nachteil, dass jeweils nur ein Wert zurückgegeben werden kann.

Ein gutes Beispiel dafür ist die swap() Funktion. Sie soll dazu dienen, zwei Variablen zu vertauschen. Die Funktion müsste in etwa folgendermaßen aussehen:

 void swap(int x, int y)
 {
   int tmp;
   tmp = x;
   x = y;
   y = tmp;
 }

Die Funktion ist zwar prinzipiell richtig, kann aber das Ergebnis nicht an die Hauptfunktion zurückgeben, da swap nur mit Kopien der Variablen x und y arbeitet.

Das Problem lässt sich lösen, indem nicht die Variable direkt, sondern - Sie ahnen es sicher schon - ein Zeiger auf die Variable der Funktion übergeben wird. Das richtige Programm sieht dann folgendermaßen aus:

  1. #include <stdio.h>
    
  2.  
    
  3. void swap(int *x, int *y)
    
  4. {
    
  5.   int tmp;
    
  6.   tmp = *x;
    
  7.   *x = *y;
    
  8.   *y = tmp;
    
  9. }
    
  10.  
    
  11. int main()
    
  12. {
    
  13.   int x = 2, y = 5;
    
  14.   printf("Variable x: %i, Variable y: %i\n", x, y);
    
  15.   swap(&x, &y);
    
  16.   printf("Variable x: %i, Variable y: %i\n", x, y);
    
  17.   return 0;
    
  18. }
    

In diesem Fall ist das Ergebnis richtig:

Variable x: 2, Variable y: 5 
Variable x: 5, Variable y: 2 

Das Programm ist nun richtig, da die Funktion swap nun nicht mit den Kopien der Variable x und y arbeitet, sondern mit den Originalen. In vielen Büchern wird ein solcher Aufruf auch als Call By Reference bezeichnet. Diese Bezeichnung ist aber nicht unproblematisch. Tatsächlich liegt auch hier ein Call By Value vor, allerdings wird nicht der Wert der Variablen sondern deren Adresse übergeben. C++ und auch einige andere Sprachen unterstützen ein echtes Call By Reference, C hingegen nicht.

Verwendung[Bearbeiten]

Sie stellen sich nun möglicherweise die Frage, welchen Nutzen man aus Zeigern zieht. Es macht den Anschein, dass wir, abgesehen vom Aufruf einer Funktion mit Call by Reference, bisher ganz gut ohne Zeiger auskamen. Andere Programmiersprachen scheinen sogar ganz auf Zeiger verzichten zu können. Dies ist aber ein Trugschluss: Häufig sind Zeiger nur gut versteckt, so dass nicht auf den ersten Blick erkennbar ist, dass sie verwendet werden. Beispielsweise arbeitet der Rechner bei Zeichenketten intern mit Zeigern, wie wir noch sehen werden. Auch das Kopieren, Durchsuchen oder Verändern von Datenfeldern ist ohne Zeiger nicht möglich.

Es gibt Anwendungsgebiete, die ohne Zeiger überhaupt nicht auskommen: Ein Beispiel hierfür sind Datenstrukturen wie beispielsweise verkettete Listen, die wir später noch kurz kennen lernen. Bei verketteten Listen werden die Daten in einem sogenannten Knoten gespeichert. Diese Knoten sind untereinander jeweils mit Zeigern verbunden. Dies hat den Vorteil, dass die Anzahl der Knoten und damit die Anzahl der zu speichernden Elemente dynamisch wachsen kann. Soll ein neues Element in die Liste eingefügt werden, so wird einfach ein neuer Knoten erzeugt und durch einen Zeiger mit der restlichen verketteten Liste verbunden. Es wäre zwar möglich, auch für verkettete Listen eine zeigerlose Variante zu implementieren, dadurch würde aber viel an Flexibilität verloren gehen. Auch bei vielen anderen Datenstrukturen und Algorithmen kommt man ohne Zeiger nicht aus. Einige Algorithmen lassen sich darüber hinaus mithilfe von Zeigern auch effizienter implementieren, so dass deren Ausführungszeit schneller als die Implementierung des selben Algorithmus ohne Zeiger ist.