Jede Variable in deinem Programm liegt irgendwo im Speicher, und jedes „irgendwo" hat eine Nummer - seine Adresse. Ein Zeiger ist einfach eine Variable, die eine dieser Nummern speichert. Das ist die ganze Idee. Alles andere auf dieser Seite ist Syntax und Konsequenz.
Zeiger sind in C deshalb so wichtig, weil C jeder Funktion eine Kopie ihrer Argumente reicht. Eine Funktion, der die Zahl 7 übergeben wird, darf ihre Kopie beliebig ändern; die Variable des Aufrufers rührt sich nie. Gib der Funktion stattdessen eine Adresse, und sie kann zurückgreifen und das Original ändern. Arrays, Zeichenketten, dynamischer Speicher und jede Datenstruktur, die du je in C bauen wirst, ruhen auf dieser einen Fähigkeit.
Speicher, Adressen und warum überhaupt etwas eine hat
Wenn du int score = 42; schreibst, reserviert der Compiler ein paar Bytes Speicher und gibt ihnen einen Namen, den du benutzen kannst. Diese Bytes haben auch eine numerische Adresse. Stell dir Speicher als eine sehr lange Straße mit nummerierten Häusern vor: score ist die Familie, die in Hausnummer 0x7ffd4c2a wohnt - der Name ist für dich, die Nummer für die Maschine.
Der &-Operator fragt nach dieser Nummer:
Führe es aus. Die drei Adressen unterscheiden sich, weil die drei Variablen verschiedene Bytes belegen, und sie ändern sich von Lauf zu Lauf - das ist normal, und deshalb schreibt man eine Adresse nie fest in den Code. Beachte den Cast (void *): %p ist so festgelegt, dass es ein void * entgegennimmt, und jeden anderen Zeigertyp zu übergeben ist streng genommen undefiniertes Verhalten. Mach den Cast zur Gewohnheit.
Dass die Adressen unvorhersehbar sind, ist kein Problem, denn es interessiert dich fast nie, was eine Adresse ist. Dich interessiert, dass du sie hast.
Einen Zeiger deklarieren
Eine Zeigerdeklaration benennt den Typ, auf den sie zeigt:
int *p; // p kann die Adresse eines int halten
double *q; // q kann die Adresse eines double halten
char *name; // name kann die Adresse eines char halten
Lies int *p; als „*p ist ein int" - p zu dereferenzieren gibt dir ein int. Diese Lesart übersteht jede komplizierte Deklaration, die C dir später vorwerfen kann.
Wohin du das * setzt, ist eine Stilfrage, die der Compiler ignoriert: int* p, int *p und int * p deklarieren alle dasselbe. Der meiste C-Code schreibt int *p, und dafür gibt es einen praktischen Grund:
int* a, b; // a ist ein Zeiger auf int; b ist ein SCHLICHTES int, kein Zeiger
int *a, *b; // beide sind Zeiger - das * bindet an den Namen, nicht an den Typ
Das * hängt am Deklarator, nicht am Typ. Es neben den Namen zu schreiben hält das sichtbar.
& und *: die beiden Operatoren
Zwei Operatoren erledigen die ganze Arbeit, und sie sind exakte Gegensätze.
&x- Adresse von. Liefert die Adresse, an derxliegt.*p- dereferenzieren. Geht zu der Adresse inpund gibt dir das dort liegende Objekt.
Die letzten beiden Zeilen sind der ganze Sinn von Zeigern. age wurde nichts direkt zugewiesen, und doch hat sich age geändert - weil *p = 31 bedeutet „speichere 31 an der Adresse, die p hält", und diese Adresse ist die Adresse von age.
Hier das Bild im Speicher:
p age
+------------+ +----------+
| 0x7ffd1c40 | --------> | 31 |
+------------+ +----------+
bei 0x7ffd1c38 bei 0x7ffd1c40
p -> die Adresse 0x7ffd1c40
*p -> das dort gespeicherte int, 31
&age -> 0x7ffd1c40, dieselbe Adresse, die p hält
&p -> 0x7ffd1c38, wo p selbst liegt
Beachte die letzte Zeile: Der Zeiger ist selbst eine Variable irgendwo im Speicher, hat also auch seine eigene Adresse. Das ist keine Fangfrage, es ist nur Konsequenz - und so funktionieren Zeiger auf Zeiger (int **).
Weil & und * einander aufheben, ist *&age schlicht age und &*p schlicht p.
Warum Zeigertypen zählen
Ein Zeiger speichert eine Adresse, und eine Adresse ist nur eine Zahl - warum besteht der Compiler also darauf zu wissen, ob sie auf ein int oder ein double zeigt? Zwei Gründe:
- Wie viele Bytes zu lesen sind. Ein
int *zu dereferenzieren liest (typischerweise) 4 Bytes; eindouble *zu dereferenzieren liest 8. Ohne den Typ wäre*pbedeutungslos. - Wie diese Bytes zu deuten sind. Dasselbe Bitmuster ist als
inteine Zahl und alsfloateine völlig andere.
Die Größe des Zeigers selbst hängt dagegen nicht davon ab, worauf er zeigt - eine Adresse ist eine Adresse:
Auf einer 64-Bit-Maschine sind alle drei Zeiger 8 Bytes groß, während die Dinge, auf die sie zeigen, 4, 8 und 1 Byte belegen. Der Typ des Ziels ist es, der *p und die Zeigerarithmetik überhaupt bedeutsam macht.
Über Zeigertypen hinweg zuzuweisen ist ein Fehler, den der Compiler beanstandet, und das zu Recht - pi = &d; würde dich dazu bringen, 4 Bytes eines double zu lesen und das Ergebnis eine Ganzzahl zu nennen.
Die kanonische Begründung: swap()
Hier ist die Funktion, zu der jeder C-Kurs greift, weil sie genau so scheitert, dass sie Zeiger erklärt.
Die Funktion meldet, dass sie getauscht hat - und main meldet, dass nichts geschehen ist. broken_swap erhielt Kopien von x und y. Sie hat ihre Kopien perfekt getauscht, und dann hörten diese Kopien beim Zurückkehren auf zu existieren.
Reiche ihr stattdessen die Adressen:
Jetzt sind a und b weiterhin Kopien - Kopien zweier Adressen -, aber eine Kopie einer Adresse zeigt auf genau dieselbe Variable wie das Original. *a ist x, von wo auch immer die Funktion aufgerufen wird.
Das ist das Muster hinter jeder C-Funktion, die die Daten ihres Aufrufers ändert, und hinter Funktionsparametern überhaupt: C hat keine Referenzübergabe, also übergibst du einen Zeiger und dereferenzierst ihn. Deshalb braucht auch scanf ein &:
int n;
scanf("%d", &n); // scanf muss in n schreiben können, braucht also n's Adresse
Mehr als einen Wert zurückgeben
Eine C-Funktion liefert einen Wert. Zeiger sind der Weg drumherum: Übergib Adressen für die zusätzlichen Ergebnisse.
Der Rückgabewert trägt den Status und die Zeiger tragen die Ergebnisse. Diese Aufteilung findet sich überall in Cs eigener Standardbibliothek und in Betriebssystem-APIs.
Zeiger auf Zeiger
Da ein Zeiger eine Variable ist, kannst du seine Adresse nehmen, und der Typ dieser Adresse ist „Zeiger auf Zeiger auf int":
pp p value
+------+ +------+ +------+
| &p | ----> | &val | -----> | 8 |
+------+ +------+ +------+
** begegnet dir in der Praxis, wenn eine Funktion einen Zeiger ändern muss, den der Aufrufer hält - zum Beispiel ein Allokator, der den Zeiger des Aufrufers auf frischen Speicher setzt - und im char *argv[] der Kommandozeilenargumente.
Vier Fehler, die man kennen sollte, bevor man sie macht
Einen Zeiger benutzen, der nie irgendwohin gezeigt hat. Ein uninitialisierter Zeiger hält die Bytes, die schon dort lagen. Ihn zu dereferenzieren liest oder schreibt an einer zufälligen Adresse.
int *p; // p hält Müll
*p = 10; // undefiniertes Verhalten - wahrscheinlich ein Absturz
Initialisiere jeden Zeiger, mit einer echten Adresse oder mit NULL. Siehe Nullzeiger für die Disziplin drumherum.
*p mit p verwechseln. p = 5; setzt den Zeiger auf Adresse 5 (Unsinn); *p = 5; speichert 5 dort, wohin er zeigt. Der Compiler warnt beim ersten, und Warnungen sind lesenswert.
Das & in scanf vergessen. scanf("%d", n) übergibt den Wert von n als Adresse. Es kompiliert mit einer Warnung und schreibt dann in den Speicher, den diese Zahl benennt.
Die Adresse einer lokalen Variablen zurückgeben. Die lokale Variable ist in dem Moment fort, in dem die Funktion zurückkehrt; die Adresse ist eine Quittung für abgerissenes Eigentum.
int *bad(void) {
int local = 42;
return &local; // der Aufrufer bekommt einen baumelnden Zeiger
}
Wenn du Speicher brauchst, der den Aufruf überlebt, belege ihn - dafür ist dynamischer Speicher da.
Wohin als Nächstes
Zeiger hören nicht bei „halte eine Adresse" auf. Zu einem Zeiger zu addieren bewegt ihn in Einheiten seines Typs durch den Speicher, und das ist das Thema von Zeigerarithmetik; und ein Arrayname zerfällt in C zu einem Zeiger auf sein erstes Element, weshalb Zeiger und Arrays faktisch dasselbe Thema sind, sobald du beides kennst. Lies diese beiden als Nächstes, und der Rest von C hört auf, wie Magie auszusehen.
Häufig gestellte Fragen
Was ist ein Zeiger in C?
Ein Zeiger ist eine Variable, deren Wert eine Speicheradresse ist - der Ort eines anderen Objekts im Speicher. int *p = &age; lässt p die Adresse von age halten, und *p liest oder schreibt dann über diese Adresse die Variable age.
Was ist der Unterschied zwischen * und & in C?
&x ist der Adressoperator: Er liefert die Adresse, an der x liegt. *p ist der Dereferenzierungsoperator: Er geht zu der in p gespeicherten Adresse und gibt dir das dortige Objekt. Sie heben einander auf, *&x ist also einfach x.
Warum taucht das * zweimal auf - in der Deklaration und beim Benutzen des Zeigers?
Es sind zwei verschiedene Verwendungen desselben Zeichens. In int *p; gehört das * zum Typ und sagt „p ist ein Zeiger auf int". In *p = 5; ist das * der Dereferenzierungsoperator und sagt „speichere 5 an der Adresse, die p hält". Die Deklaration als „*p ist ein int" zu lesen lässt beides Sinn ergeben.
Warum brauche ich Zeiger in C?
C übergibt jedes Argument als Wert, eine Funktion kann die Variable eines Aufrufers also nie ändern, sofern ihr nicht die Adresse gereicht wird. Zeiger lassen dich außerdem Arrays effizient durchlaufen, mehr als ein Ergebnis zurückgeben, verkettete Strukturen bauen und zur Laufzeit mit malloc belegten Speicher benutzen.