Menu

Zeigerarithmetik in C: addieren, subtrahieren und durch den Speicher laufen

1 zu einem Zeiger zu addieren addiert kein Byte - es rückt zum nächsten Objekt dieses Typs. Diese Seite behandelt ptr+1, Inkrement und Dekrement, Zeigerdifferenzen, Vergleiche, das Durchlaufen eines Arrays mit einem Zeiger und die Eins-hinter-dem-Ende-Regel.

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

Zeiger halten Adressen, und Adressen sind Zahlen - es ist also naheliegend zu erwarten, dass Arithmetik auf ihnen funktioniert. Sie tut es, aber nicht ganz so wie schlichte Ganzzahlarithmetik. C skaliert jede Zeigeroperation mit der Größe dessen, worauf gezeigt wird, was aus „addiere eins" das weit nützlichere „rücke zum nächsten Element" macht.

Diese eine Entwurfsentscheidung ist der Grund, warum das Durchlaufen eines Arrays mit einem Zeiger in C genauso natürlich ist wie das Durchlaufen mit einem Index.

1 zu addieren rückt um ein Objekt vor, nicht um ein Byte

Nimm einen Zeiger, addiere 1 und gib beide Adressen aus:

Das int * springt 4 Bytes, das double * springt 8, das char * springt 1 - jedes landet genau dort, wo das nächste Objekt seines Typs beginnen würde. Die Regel ist einfach:

ptr + n  ==  (Adresse in ptr) + n * sizeof(*ptr)

Genau deshalb hat ein Zeiger überhaupt einen Typ. Eine nackte Adresse ohne angehängten Typ hätte keine Ahnung, wie weit „das Nächste" entfernt ist.

Ein Array per Zeiger durchlaufen

Nun der Gewinn. Die Elemente eines Arrays liegen in aufeinanderfolgendem Speicher, einen Zeiger weiterzurücken besucht sie also der Reihe nach:

Beide Schleifen geben dieselben fünf Zahlen aus. In der zweiten zerfällt scores zu einem Zeiger auf sein erstes Element, p++ rückt um ein int vor, und *p liest das dort liegende Element. Im Speicher ausgelegt:

 scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
|   88    |   92    |   75    |   60    |   100   |
+---------+---------+---------+---------+---------+
 ^         ^                                       ^
 p         p+1                                     scores+5
 (Anfang)  (4 Bytes später)                        (eins hinter dem Ende)

Die Beziehung zwischen beiden Formen ist exakt und merkenswert: scores[i] ist definiert als *(scores + i). Mehr dazu unter Zeiger und Arrays.

Inkrement, Dekrement und die *p++-Falle

++ und -- funktionieren auf Zeigern mit derselben Skalierung.

Der Ausdruck, der alle verwirrt, ist *p++. Das Postfix-++ bindet stärker als das *, es bedeutet also „nimm das aktuelle p, rücke p vor, dereferenziere dann den alten Wert":

Drei Ausdrücke, drei verschiedene Bedeutungen:

AusdruckZeiger danachErzeugter Wert
*p++vorgerücktdas Element vor der Bewegung
*++pvorgerücktdas Element nach der Bewegung
(*p)++unverändertder alte Wert, und das Element wird erhöht

*p++ ist idiomatisches C - so werden Zeichenketten- und Pufferkopien geschrieben -, aber setze die Klammern, wenn du etwas anderes meinst.

Zwei Zeiger subtrahieren

Einen Zeiger von einem anderen abzuziehen liefert die Anzahl der Elemente dazwischen, nicht die Anzahl der Bytes:

Der Ergebnistyp ist ptrdiff_t, ein vorzeichenbehafteter Ganzzahltyp aus <stddef.h>, ausgegeben mit %td. Zeiger in zwei verschiedene Arrays zu subtrahieren erzeugt ein undefiniertes Ergebnis - der Standard definiert die Operation nur innerhalb eines Objekts.

Du kannst von einem Zeiger auch eine Ganzzahl abziehen (p - 3 geht drei Elemente zurück), aber du kannst zwei Zeiger nicht addieren. „Die Summe zweier Adressen" benennt nichts.

Zeiger vergleichen

Vergleichsoperatoren funktionieren auf Zeigern in dasselbe Array, und sie bedeuten genau das, was das Speicherlayout nahelegt: p < q ist wahr, wenn p ein früheres Element adressiert.

front < back ist die ganze Abbruchgeschichte der Schleife, und sie ist korrekt, weil beide Zeiger in einem Array bleiben. == und != sind ebenfalls die natürliche Art, eine Schleife gegen ihre Endmarke zu prüfen, was uns zu der Regel bringt, die all das erlaubt macht.

Die Eins-hinter-dem-Ende-Regel

C erlaubt ausdrücklich, einen Zeiger auf die Position eins hinter dem letzten Element eines Arrays zu bilden. Du darfst ihn berechnen, speichern und damit vergleichen. Du darfst ihn nicht dereferenzieren.

int arr[4];

 arr[0]   arr[1]   arr[2]   arr[3]   (kein Element)
+------+ +------+ +------+ +------+  +- - - -+
|      | |      | |      | |      |  |       |
+------+ +------+ +------+ +------+  +- - - -+
 ^                                    ^
 arr                                  arr + 4
 nutzbar                              darf GEBILDET und VERGLICHEN werden
                                      nie dereferenzierbar

Diese Regel macht die Standardform der Schleife gültig:

for (int *p = arr; p != arr + n; p++) {
    /* ... */
}

In der letzten Iteration wird p zu arr + n, der Vergleich schlägt fehl und die Schleife endet - ohne diese Position je zu lesen.

Zwei Dinge bleiben dennoch undefiniert. Einen Zeiger zwei hinter das Ende oder eins vor den Anfang zu bilden ist undefiniertes Verhalten, auch wenn du ihn nie dereferenzierst:

int *bad1 = arr + n + 1;   // undefiniert - zu weit dahinter
int *bad2 = arr - 1;       // undefiniert - vor dem Anfang

Das Zweite zählt in der Praxis: Eine rückwärts laufende Schleife, geschrieben als for (int *p = arr + n - 1; p >= arr; p--), berechnet bei ihrem letzten Dekrement arr - 1. Sie funktioniert auf jedem gängigen Compiler und ist trotzdem formal undefiniert. Die saubere Rückwärtsschleife vermeidet es:

void-Zeiger und die Arithmetik, die du nicht machen kannst

Ein void * hält eine Adresse ohne angehängten Typ, und so bleiben malloc und qsort allgemein. Weil es keine Elementgröße gibt, ist Zeigerarithmetik auf einem void * vom Standard nicht erlaubt - caste zuerst in einen konkreten Typ (oder in char * für Arbeit auf Byteebene).

void *v = buffer;
// v + 1;              // kein Standard-C - keine Größe zum Skalieren
char *b = v;
b + 1;                 // in Ordnung - ein Byte weiter

GCC und Clang akzeptieren void *-Arithmetik als Erweiterung, die sie wie char * behandelt, Code, der sich darauf verlässt, kompiliert also bis zu dem Tag, an dem er irgendwo strenger gebaut wird. Kompiliere mit -std=c17 -pedantic, wenn du es gesagt bekommen willst.

Was dir das bringt

Zeigerarithmetik ist kein Optimierungstrick, zu dem man gelegentlich greift - sie ist der Mechanismus unter der Array-Indizierung, der Zeichenkettenverarbeitung und jedem Pufferdurchlauf in der Standardbibliothek. strlen ist ein Zeiger, der bis zum abschließenden '\0' vorgerückt und vom Anfang abgezogen wird. memcpy sind zwei Zeiger, die im Gleichschritt gehen.

Als Nächstes sieh dir an, wie die Array-Zeiger-Gleichwertigkeit tatsächlich funktioniert, einschließlich der sizeof-Falle, die beim ersten Übergeben eines Arrays an eine Funktion jeden erwischt: Zeiger und Arrays.

Häufig gestellte Fragen

Was macht ptr + 1 in C?

Es rückt den Zeiger um ein Objekt vor, nicht um ein Byte. Für ein int * sind das auf einer typischen Maschine 4 Bytes; für ein double * 8. Der Compiler multipliziert für dich mit sizeof(*ptr), weshalb der Typ des Zeigers zählt.

Kann man in C zwei Zeiger subtrahieren?

Ja, wenn beide in dasselbe Array zeigen. end - start liefert die Anzahl der Elemente dazwischen, mit dem Typ ptrdiff_t (auszugeben mit %td). Zeiger in unverwandte Objekte zu subtrahieren ist undefiniertes Verhalten.

Ist es erlaubt, eins hinter das Ende eines Arrays zu zeigen?

Ja - einen Zeiger auf die Position ein Element hinter dem letzten zu bilden ist ausdrücklich erlaubt, damit Schleifen wie for (int *p = arr; p != arr + n; p++) funktionieren. Du darfst diesen Zeiger berechnen und vergleichen, aber ihn zu dereferenzieren ist undefiniertes Verhalten.

Warum ist *p++ nicht dasselbe wie (*p)++?

*p++ erhöht den Zeiger und dereferenziert den alten Wert - es liest das aktuelle Element und rückt dann vor. (*p)++ lässt den Zeiger in Ruhe und erhöht den Wert, auf den er zeigt. Das Postfix-++ bindet stärker als *, die Klammern sind also das, was die Bedeutung ändert.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S