Menu

Zeiger und Arrays in C: Zerfall, arr[i] = *(arr+i) und die sizeof-Falle

In C wird ein Arrayname fast überall, wo du ihn benutzt, zu einem Zeiger auf sein erstes Element. Diese Seite erklärt diesen Zerfall, warum arr[i] buchstäblich *(arr+i) ist, warum die Arraygröße getrennt in Funktionen reisen muss und wie sich ein Zeiger auf ein Array von einem Array von Zeigern unterscheidet.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Arrays und Zeiger sind in C verschiedene Dinge, die sich die meiste Zeit gleich verhalten. Dieses „die meiste Zeit" macht das Thema glitschig: Code, der ein Array wie einen Zeiger behandelt, funktioniert meist - bis zu dem Tag, an dem er an einer der beiden Stellen landet, an denen der Unterschied zählt, und die Antwort still falsch ist.

Diese Seite zieht die Linie genau.

Array-Zerfall: die eine Regel hinter allem

In fast jedem Ausdruck wird ein Arrayname automatisch in einen Zeiger auf sein erstes Element umgewandelt. Das heißt Zerfall. Aus arr wird &arr[0], mit dem Typ int *.

Der Arrayname und die Adresse seines ersten Elements werden identisch ausgegeben. Das ist der Zerfall in Aktion, und deshalb kannst du ein Array ohne & einem Zeiger zuweisen.

Es gibt genau drei Stellen, an denen kein Zerfall passiert: als Operand von sizeof, als Operand von & und beim Initialisieren eines Zeichenarrays aus einem Zeichenkettenliteral. Überall sonst gehe von Zerfall aus.

arr[i] ist buchstäblich *(arr + i)

Der C-Standard definiert den Indexoperator über Zeigerarithmetik: a[b] bedeutet *(a + b). Indizieren ist kein eigenes Merkmal - es ist eine Kurzschreibweise.

Zwei Konsequenzen fallen heraus. Erstens lässt sich ein Zeiger indizieren: p[2] ist *(p + 2), weshalb Funktionen, die einen Zeiger erhalten, weiterhin die vertraute arr[i]-Syntax benutzen können. Zweitens kompiliert 2[arr] - *(2 + arr) ist dieselbe Adresse wie *(arr + 2). Niemand schreibt das außerhalb eines Quiz, aber es klärt die Frage, ob Indizieren „wirklich" Zeigerarithmetik ist.

Ein Array an eine Funktion übergeben

Wegen des Zerfalls erhält eine Funktion nie ein Array. Sie erhält einen Zeiger.

Drei Dinge lohnt es sich aus diesem Beispiel herauszuziehen.

Die Größe in int arr[100] ist Dokumentation, sonst nichts. Der Compiler schreibt den Parameter zu int *arr um und prüft nie, ob du 100 von irgendetwas übergeben hast.

Die Länge muss getrennt mitreisen. Die Funktion hat eine Adresse und keine Ahnung, wie weit die Daten reichen. Deshalb nimmt praktisch jede array-verarbeitende C-Funktion eine Anzahl entgegen: memcpy(dst, src, n), fread(ptr, size, count, f), qsort(base, nmemb, size, cmp). Zeichenketten sind nur deshalb die Ausnahme, weil sie ihre eigene Endmarke tragen, das '\0'.

Einen Zeiger zu übergeben ist billig, und die Daten sind geteilt. Es wird keine Kopie des Arrays gemacht, die Funktion kann also die Elemente des Aufrufers ändern - und oft ist genau das gewollt:

Markiere nur lesende Parameter als const int *, wie print_all es tut. Es dokumentiert die Absicht und lässt den Compiler einen versehentlichen Schreibzugriff abfangen.

Die sizeof-Falle

Hier ist der Bug, der jeden C-Programmierer einmal erwischt. sizeof ist eine der beiden Stellen, an denen kein Zerfall passiert - es liefert also die wahre Arraygröße dort, wo das Array deklariert ist, und die Größe eines Zeigers überall dort, wo das Array zerfallen ist.

In main ist sizeof data gleich 40 Bytes und die Länge berechnet sich zu 10. Innerhalb von inspect ist sizeof arr die Größe eines Zeigers - 8 auf einer 64-Bit-Maschine -, und die „Länge" kommt als 2 heraus. Der Code sieht identisch aus und ist still falsch.

Daraus folgt die Regel: Berechne die Länge eines Arrays nur in dem Gültigkeitsbereich, in dem das Array deklariert wurde, und reiche sie von dort weiter.

Ein & auf ein Array ist nicht derselbe Typ

Die andere Stelle, an der kein Zerfall passiert, ist der &-Operator. &arr ist ein Zeiger auf das ganze Array, nicht auf sein erstes Element. Beide halten dieselbe Adresse; der Unterschied ist, was „addiere 1" bedeutet.

Dieselbe Startadresse, andere Schrittweite. arr hat den Typ int * und geht um ein Element weiter; &arr hat den Typ int (*)[5] und geht um das ganze Array weiter. Das ist der Mechanismus, der das Übergeben einer Zeile eines 2D-Arrays funktionieren lässt, behandelt unter mehrdimensionale Arrays.

Zeiger auf Array gegen Array von Zeigern

Zwei Deklarationen, die sich ähneln und Gegenteiliges bedeuten:

int *p[5];     // Array aus 5 Zeigern auf int
int (*q)[5];   // Zeiger auf ein Array aus 5 ints

[] hat höheren Vorrang als *, int *p[5] liest sich also als „p ist ein Array von Zeigern". Die Klammern in int (*q)[5] zwingen das *, zuerst zu binden: „q ist ein Zeiger auf ein Array".

int *p[5]                     int (*q)[5]

 p[0] -> [ int ]               q -> [ int | int | int | int | int ]
 p[1] -> [ int ]                     ein zusammenhängender Block von 5
 p[2] -> [ int ]
 p[3] -> [ int ]
 p[4] -> [ int ]
 fünf getrennte Adressen

Ein Array von Zeigern ist das Verbreitete, und du hast es bereits benutzt: char *argv[] ist ein Array von Zeigern auf Zeichenketten, eines pro Kommandozeilenargument. So hältst du auch eine Liste von Zeichenketten verschiedener Länge, ohne Platz zu verschwenden.

Beachte (*q)[0]: den Zeiger dereferenzieren, um das Array zu erhalten, und es dann indizieren. Die Klammern sind wieder Pflicht, aus demselben Vorranggrund.

Wo Arrays und Zeiger sich wirklich unterscheiden

Behalte diese kurze Tabelle im Kopf, und die Verwirrung verschwindet:

Array int arr[10]Zeiger int *p
Was es ist10 im Speicher liegende intseine Variable, die eine Adresse hält
sizeof40 - der ganze Block8 - nur der Zeiger
Zuweisbarnein: arr = x; ist ein Fehlerja: p = x; ist in Ordnung
& liefertint (*)[10]int **
Woher der Speicher kommtmit dem Array deklariertwohin auch immer du zeigst

Ein Arrayname ist keine Variable, die eine Adresse hält - er ist der Speicher, und die Adresse wird bei Bedarf berechnet. Deshalb kannst du ihm nichts zuweisen.

Häufig gestellte Fragen

Sind Arrays und Zeiger in C dasselbe?

Nein, aber sie lassen sich leicht verwechseln, weil ein Arrayname in fast jedem Ausdruck zu einem Zeiger auf sein erstes Element zerfällt. Das Array selbst ist ein Block von Elementen mit bekannter Größe; ein Zeiger ist eine einzelne Variable, die eine Adresse hält. sizeof und & sind die beiden Stellen, an denen sich der Unterschied zeigt.

Warum liefert sizeof(arr) innerhalb einer Funktion die falsche Antwort?

Weil der Parameter kein Array ist. void f(int arr[]) wird still zu void f(int *arr) umgeschrieben, sizeof(arr) misst also einen Zeiger (auf den meisten Maschinen 8 Bytes), nicht das ursprüngliche Array. Übergib die Länge als separaten Parameter.

Was ist der Unterschied zwischen int *p[5] und int (*p)[5]?

int *p[5] ist ein Array aus 5 Zeigern auf int. int (*p)[5] ist ein einzelner Zeiger auf ein Array aus 5 ints. Die Klammern binden das * zuerst an den Namen; ohne sie gewinnt [], weil es höheren Vorrang hat.

Ist arr[i] wirklich dasselbe wie *(arr + i)?

Ja - der Standard definiert a[b] als *(a + b). Deshalb kompiliert und funktioniert auch das seltsam aussehende i[arr]: Die Addition ist kommutativ, *(i + arr) ist also dasselbe Element. Schreib es nie in echtem Code, aber es beweist die Regel.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S