C Forever

C : Interna - Pointer (Zeiger)

Man liest immer wieder, Zeiger seien der größte Schrecken vieler C-Programmierer.

Einerseits stimmt das. Wenn man das Prinzip und Möglichkeiten nicht verstanden hat, wird die Sache durchaus kompliziert - und bisweilen gefährlich.

Andererseits sind Zeiger, mit Wissen und Bedacht angewendet, ein sehr mächtiges Werkzeug. Ganz egal, was und worauf man C programmiert, früher oder später kommt man nicht mehr um Zeiger herum.
Ich habe den Umgang mit Zeigern ganz am Anfang meiner Informatikerkarriere in PASCAL gelernt und war davon sofort begeistert.
Die Möglichkeiten der C-Zeiger gehen aber noch deutlich weiter und machen dadurch Sachen möglich, die sich anders nicht oder nur sehr schwer lösen lassen.

Zeiger - was ist das eigentlich

Eigentlich ist das ganz einfach : ein Zeiger ist eine Variable, die die Adresse einer Speicherstelle enthält.
So weit, so gut. Und was kann man damit anfangen? Unendlich viel!

Eine Zeigervariable braucht als erstes mal einen Typ, der angibt, auf was der Zeiger eigentlich zeigt. Also hier mal das übliche Beispiel:

char c;
char * p_c;

Die erste Zeile deklariert eine Variable vom Typ char, die genau ein Byte belegt. Die zweite Zeile deklariert einen Zeiger auf etwas vom Typ char, der genau soviel Bytes belegt wie das entsprechende System für eine Adresse verwendet. Zunächst sind beide Variablen undefiniert, was insbesondere bei dem Zeiger zu Verwirrungen führen kann. Denn die übliche Abfrage, ob ein Zeiger definiert ist, lautet meistens :

if( p_c )
    // mache etwas mit dem Zeiger
else
    // initialisiere den Zeiger

Da solche Deklarationen ja des Öfteren in Funktionen vorkommen, kann man sich nicht auf den Null-Wert verlassen, weil die Variable auf dem (vorher schon benutzen) Stack liegt. Deswegen ist es meistens sinnvoller, die Deklaration mit einer Definition zu verknüpfen :

char * p_c = 0;

Das kostet kaum Platz und Ausführungszeit, hilft aber ungemein, später "seltsame" und meistens schwer aufzufindende Fehler zu vermeiden.

Jetzt haben wir also einen Zeiger, der zunächst mal ins Nirvana oder auf die Adresse 0 zeigt - noch nicht besonders nützlich.
Also lassen wir ihn auf die Variable c zeigen :

p_c = &c;

Das ist immer noch nicht viel Sinnvolles, denn wie kommen wir jetzt über den Zeiger an den Inhalt der Variable c?
Eigentlich ganz einfach - aber von der Syntax her eher etwas verwirrend. In der Deklaration steht der Stern ja auch vor der Zeigervariable, ohne irgendwelchen Inhalt herzugeben.
Wird der Stern außerhalb der Deklaration, also überall sonst im Programm, verwendet, kommt der Inhalt des Speichers, auf den der Pointer zeigt, zurück.
Im Informatiker-Sprech :
p_c refezenziert c
*p_c dereferenziert c

char d;
d = *p_c;

Jetzt könnten wir mit dem Zeiger etwas anfangen.

Zur Vereinfachung nehme ich jetzt zwei Integervariablen und einen Zeiger auf Speicherplatz vom Typ int. Zuerst werden wie gehabt die Variablen dekalriert, dann der Zeiger deklariert und mit 0 initialisiert.

int i;
int j;
int * p_int = 0;
 
i = 123;                // die Variable i enthält jetzt den Wert 123
j = 456;                // die Variable j enthält jetzt den Wert 456
p_int = &i;             // der Zeiger verweist auf die Variable i
printf("%d", *p_int);   // Ausgabe : 123
*p_int = 789;           // die Variable i enthält jetzt den Wert 789
printf("%d", i);        // Ausgabe : 789
p_int = &j;             // der Zeiger verweist auf die Variable j
printf("%d", *p_int);   // Ausgabe : 456

Das alles lernt man in jedem C-Kurs, es birgt kaum Fehlermöglichkeiten. Es erscheint aber auch nicht besonders nützlich.

Zeigerarithmetik

Zeigerarithmetik ist Ganzzahl-Arithmetik, nicht mehr.

Aber ein bisschen weniger :
Zeiger sind nie negativ, d.h. sie können nur positive Werte (inkl. der 0 !) annehmen. Die Länge einer Zeigervariablen entspricht i.A. der Größe einer Adresse im Speicherraum des verwendeten Prozessors.

Und ganz wichtig und gerne vergessen :
Die Schrittweite bei der Zeigerarithmetik ist meistens nicht ein Byte. Sie entspricht der Anzahl der Bytes, die eine Variable des Typs belegt, für den die Zeigervariable deklariert ist. Das kann bei großen Strukturen durchaus eine Menge Bytes ergeben.

Arrays / Strings - ein wenig Zeigerarithmetik

Deklarieren wir uns einfach mal ein Array mit 10 char-Elementen und spielen ein wenig damit.

int i;
char array[10];
char * p;
 
for( i = 0; i < 10; ++i )
    array[i] = i + '0';     // jetzt ist das Array mit den Ziffern 0 bis 9 gefüllt
 
p = &array[5];              // das ist einfach - p zeigt auf das sechste Element des Arrays
printf("%c", *p);           // Ausgabe : 5
 
p = array;                  // schon nicht mehr so durchsichtig :
                            // der Name eines Arrays wird in C als Zeiger auf das
                            // erste Element des Arrays interpretiert
printf("%c", *p);           // Ausgabe : 0
 
for( i = 0; i < 10; ++i )
    {
    printf("%c ", *p);      // Ausgabe : 0 1 2 3 4 5 6 7 8 9
    p = p + 1;              // Zeigerarithmetik !
    }
 
// p zeigt jetzt auf die erste Speicherstelle HINTER dem Array
// Ein Zugriff mit *p kann ohne Folgen bleiben, abgesehen von einem undefinierten Wert
// Ebenso kann er aber auch eine Speicherzugriffsverletzung und damit den Absturz unseres
//  Programms bewirken
 
for( i = 0; i < 10; ++i )
    {
    --p;                    // Zeigerarithmetik !
    printf("%c ", *p);      // Ausgabe : 9 8 7 6 5 4 3 2 1
    }

Was zeigt uns dieser Beispiel-Code?

  1. Arrays und Zeiger spielen gut zusammen.
    Der Name eines Arrays wird in C (auch) als Zeiger auf das erste Element des Arrays interpretiert.
    Zeigerarithmetik geht damit aber nicht - die genannte Zuordnung ist fix, der Typ des Namens ist Array vom Typ des Arrays! Macht man diesen Fehler, meckert der Compiler!
  2. Mit Zeigern kann man rechnen.
    Und so wunderbare Dinge damit veranstalten.
  3. Zeiger müssen nicht unbedingt auf einen vorher definierten Speicherbereich zeigen!
    An dieser Stelle muss der Programmierer das erste Mal bei der Verwendung von Zeigern aufpassen, was er tut.
    Daß ein Zeiger nicht dahin zeigt, wohin er zeigen soll, führt entweder zu einem unerwarteten Ergebnis oder zum Programmabsturz. Ersteres ist sehr schwer zu finden, wenn das Ergebnis nicht direkt aus dem Programmablauf ersichtlich bzw. nicht reproduzierbar ist. Zweiteres lässt sich, zumindest unter Linux, manchmal mit dem Debugger eingrenzen.
    Es ist aber immer besser, sich vorher über die korrekte Verwendung des Zeigers und die möglichen Fehler Gedanken zu machen.
    Die Überwachung der Zugriffsverletzung für das Array (out of bounds error) zur Laufzeit bringt im Zusammenhang mit dem Zugriff auf ein Arrayelement über einen Zeiger nichts, da der Zugriff nicht unter Verwendung einer Indexvariable und der Array-Indizierung erfolgt.

Wird fortgesetzt ...

© Uwe Jantzen 1.08.23