10.7 Zugriff auf Array-Elemente über Zeiger
Es gibt zwei Möglichkeiten, wie Sie auf ein Array-Element zugreifen können. Entweder gehen Sie den bereits bekannten Weg über den Index mit [], oder Sie verwenden hierzu die Zeiger in einer Zeiger-Versatz-Schreibweise.
Sehen Sie sich hierzu folgendes Listing an:
00 // Kapitel10/ptr_array.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define MAX 5
03 int main(void) {
04 int iarr[MAX] = { 12, 34, 56, 78, 90 };
05 int *iptr = NULL;
06 // iptr zeigt auf das erste Element von iarr
07 iptr = iarr;
08 printf("iarr[0] = %d\n", *iptr);
09 printf("iarr[2] = %d\n", *(iptr+2));
10 *(iptr+4) = 66; // iarr[4] = 66
11 *iptr = 99; // iarr[0] = 99
12 // alle auf einmal durchlaufen (2 Möglichkeiten)
13 int *iptr2 = iarr; // iptr2 auf den Anfang von iarr
14 for(int i=0; i < MAX; i++, iptr2++) {
15 printf("iarr[%d] = %d /", i, *(iptr+i));
16 printf(" %d\n", *iptr2); // so geht es auch
17 }
18 return EXIT_SUCCESS;
19 }
In Zeile (07) wird der Zeiger iptr mit der Adresse des ersten Elements von iarr initialisiert. Sicherlich fragen Sie sich, warum in Zeile
iptr = iarr;
anstatt
iptr = &iarr[0]
verwendet wurde. Hier gilt, dass der Name eines Arrays ohne Index automatisch eine Adresse auf das erste Element ist. Der Beweis, dass iptr auf das erste Element von iarr, genauer iarr[0], verweist, wird in Zeile (08) ausgegeben.
In Zeile (09) kommt eine Zeigerarithmetik mit der Addition einer Ganzzahl zum Einsatz, bei der der Zeiger bei einer Erhöhung um die Anzahl der Bytes verschoben wird, die der Größe des durch den Zeiger referenzierten Typs entspricht. Die Zeiger-Versatz-Schreibweise *(iptr+2) ist identisch mit iarr[2]. Mit beiden Versionen wird also auf den Inhalt des dritten Elements im Array iarr zugegriffen.
Spätestens jetzt dürften Sie auch erkennen, warum Sie Zeiger richtig typisieren müssen, denn ohne das Wissen um die Speichergröße des assoziierten Typs könnte das Vorgänger- bzw. Nachfolgerelement im Array beispielsweise über die Anweisung *(iptr+2) nicht wie in Zeile (09) berechnet werden. Im letzten Beispiel wird der Typ int verwendet, daher erfolgt die Erhöhung der Adresse mit iptr+n um die Größe des Typs, auf die der Zeiger verweist. Wird also ein Zeiger vom Typ int* um 1 erhöht, verweist er auf ein int-Objekt weiter, und nicht auf ein Byte weiter. Dasselbe gilt beispielsweise auch, wenn ein Zeiger vom Typ double*, der auf ein double zeigt, um 1 erhöht wird, sodass dieser Zeiger um die Anzahl der Bytes (=sizeof(double)) verschoben wird, die der Größe des durch den Zeiger referenzierten Typs entspricht.
Bitte Zeiger unterschiedlicher Datentypen nicht vermischen!
Wenn Sie z. B. einen double-Zeiger auf das int-Array verweisen lassen, was ja durchaus mit einem hier nicht ratsamen und gewaltsamen expliziten Cast erzwungen werden könnte, würde die Zeigerarithmetik nicht mehr richtig arbeiten: Es wird dann eine Erhöhung der Speicherzelle, auf die der Zeiger verweist, gemäß dem Zeiger und nicht dem Variablentyp durchgeführt. Wenn dann etwa sizeof(double) gleich 8 und ein sizeof(int) gleich 4 wäre, würde bei einer Erhöhung um 1 der Zeiger um 8 Bytes weiterlaufen. Daher sollte es jetzt einleuchten, wie auch in Abschnitt 10.6, »Zeigerarithmetik«, bereits erwähnt, dass Zeiger mit verschiedenen Datentypen nicht einander zugewiesen werden dürfen. Natürlich kann man mit Typecasting und void* alles in alles wandeln, aber dann müssen Sie genau wissen, was Sie tun.
Damit der Zeiger tatsächlich auf die nächste Adresse zeigt, wurde ptr+n zwischen Klammern gestellt, weil Klammern eine höhere Bindungskraft haben und somit zuerst ausgewertet werden.
In der for-Schleife in den Zeilen (14) bis (17) finden Sie außerdem neben der Möglichkeit, die Adresse eines Zeigers um einem ganzzahligen Wert zu erhöhen, auch die Möglichkeit, die Adressen des Array-Zeigers mit dem Inkrementoperator (iptr2++) zu erhöhen und so durch das Array zu iterieren.
Reduzieren von Zeigern
Neben einer Addition von Zeigern mit ptr+1 können Sie diese ebenfalls mit ptr-1 subtrahieren. Gleiches gilt auch für den Dekrementoperator ptr--.
Um also auf ein Element eines Arrays über Zeiger zuzugreifen, haben Sie folgende Möglichkeiten:
01 int iarr[MAX] = { 11, 22, 33, 44, 55 };
02 int *iptr = iarr;
03 // Zugriff auf eine Element
04 printf("%d : %d\n", iarr[2], *(iptr+2));
05 // Zugriff auf Adresse
06 printf("%p : %p\n", &iarr[3], iptr+3);
07 // Array-Name als Zeiger verwenden
08 printf("%d : %d\n", iarr[1], *(iarr+1));
09 // Zeiger indizieren
10 printf("%d : %d\n", iarr[4], iptr[4]);
Beide in Zeile (04) demonstrierten Möglichkeiten, um auf ein Element in einem Array zuzugreifen, sind gleichwertig. In Zeile (06) sehen Sie hingegen zwei gleichwertige Methoden, auf die Adressen (!) eines Arrays zuzugreifen. In Zeile (08) können Sie sehr schön sehen, dass ein Array-Name auch als Zeiger aufgefasst werden kann, und Zeile (10) zeigt den umgekehrten Fall, in dem ein Zeiger mit der Zeiger-Index-Schreibweise indiziert wird.
Anhand dieser Beispiele können Sie erkennen, dass Arrays und Zeiger recht eng miteinander zusammenhängen. Ein Array-Name ist im Grunde ein konstanter Zeiger. Zeiger hingegen können verwendet werden, um auf die einzelnen Array-Elemente zuzugreifen.