Eine Variable, die eine Adresse enthält
Jede Variable lebt irgendwo im Speicher, an einer nummerierten Stelle, die man ihre Adresse nennt. Meistens ist es dir egal, wo – du verwendest einfach den Namen der Variablen. Ein Zeiger dreht das um: Er ist eine Variable, deren Wert eine Adresse ist. Statt 42 zu enthalten, enthält er „den Ort, an dem 42 gespeichert ist“.
Diese Indirektion macht Zeiger so mächtig. Funktionen können damit eine Variable des Aufrufers verändern, Datenstrukturen wie verkettete Listen verketten ihre Knoten damit, und (wie du unter dynamischer Speicher sehen wirst) so erreichst du Speicher, den du zur Laufzeit anforderst.
Das & in &score ist der Adressoperator – es liefert den Ort von score. Das * in *p ist der Dereferenzierungsoperator – es folgt der Adresse zurück zu dem Wert, der dort lebt.
Die beiden Operatoren: & und *
Das mit Abstand Verwirrendste für Einsteiger ist, dass * je nach Position zwei verschiedene Dinge bedeutet. Halte das auseinander:
int* p; // DEKLARATION: "p ist ein Zeiger auf int"
p = &x; // & = Adresse von: speichert die Adresse von x in p
int y = *p; // * = Dereferenzierung: liest den Wert, auf den p zeigt
*p = 99; // Dereferenzierung links: schreibt durch den Zeiger
In einer Deklaration ist * Teil des Typs. In einem Ausdruck verrichtet * Arbeit. Sobald ein Zeiger eingerichtet ist, gibt dir das Dereferenzieren vollen Lese-/Schreibzugriff auf die ursprüngliche Variable:
Beachte, dass du health nach Zeile 1 nie wieder über seinen Namen angefasst hast, und sein Wert sich trotzdem immer wieder geändert hat. Genau darum geht es: hp ist ein Alias für denselben Speicherplatz. Die Abstände (int* p, int *p, int*p) sind rein kosmetisch und für den Compiler identisch – dieser Leitfaden verwendet int* p.
nullptr: auf nichts zeigen
Ein Zeiger, der nirgendwohin zeigt, sollte auf nullptr (C++11) gesetzt werden. Das ist eine klare, typsichere Art zu sagen „noch kein Ziel“, und es gibt dir etwas, gegen das du prüfen kannst, bevor du dereferenzierst.
Bevorzuge nullptr gegenüber dem alten Makro NULL oder einer nackten 0. Da nullptr einen echten Zeigertyp hat, wird es bei der Überladungsauflösung nie fälschlich als die Ganzzahl 0 gelesen – ein subtiler Bug, den der alte Stil verursachen konnte.
Falle: die Null-Dereferenzierung. Lesen oder Schreiben durch einen Null-Zeiger (oder nicht initialisierten Zeiger) ist undefiniertes Verhalten, meist ein sofortiger Absturz:
int* p = nullptr;
cout << *p; // ABSTURZ - das Dereferenzieren von null ist undefiniertes Verhalten
Sichere dich immer mit if (p) (oder if (p != nullptr)) ab, bevor du etwas dereferenzierst, das null sein könnte.
Zeiger und Arrays
Der Name eines Arrays zerfällt (decay) zu einem Zeiger auf sein erstes Element, daher sind Zeiger und Arrays eng miteinander verwoben. 1 zu einem Zeiger zu addieren fügt nicht ein Byte hinzu – es rückt um ein Element vor, und genau das lässt Zeigerarithmetik funktionieren:
p[i] und *(p + i) sind buchstäblich derselbe Ausdruck – diese Gleichwertigkeit ist der Grund, warum Arrays bei null indiziert sind. Der klassische Bug hier ist, über das Ende hinaus zu laufen: nums + 4 ist eine gültige Eins-hinter-dem-Ende-Markierung zum Vergleichen, aber *(nums + 4) zu dereferenzieren liest außerhalb der Grenzen. Off-by-one-Fehler mit Zeigern sind eine Hauptursache für Abstürze und stille Datenkorruption, also gehe bei deiner Abbruchbedingung bewusst vor.
const und Zeiger
const kann sich auf das, worauf der Zeiger zeigt, auf den Zeiger selbst oder auf beides beziehen. Lies die Deklaration von rechts nach links, um sie zu entschlüsseln:
const int* p; // Zeiger auf const int - *p nicht änderbar, p umlenkbar
int* const p = &x; // const-Zeiger auf int - *p änderbar, p nicht umlenkbar
const int* const p = &x; // beides gesperrt
Das ist im echten Code ständig wichtig. Eine Funktion, die verspricht, deine Daten nicht zu verändern, nimmt einen Zeiger auf const:
Das Markieren des Ziels als const dokumentiert die Absicht und lässt den Compiler versehentliche Schreibzugriffe unterbinden – kostenlose Sicherheit ohne Laufzeitkosten.
Die große Falle: hängende Zeiger
Ein hängender Zeiger zeigt auf Speicher, der nicht mehr den Wert enthält, den du erwartest – die Variable hat ihren Gültigkeitsbereich verlassen, oder der Speicher wurde freigegeben. Ihn zu dereferenzieren ist undefiniertes Verhalten, und das Tückische ist, dass es oft zu funktionieren scheint, bis es das nicht mehr tut.
int* makeBad() {
int local = 5;
return &local; // BUG: local stirbt, wenn die Funktion zurückkehrt
} // der zurückgegebene Zeiger hängt jetzt
Die Adresse ist immer noch eine gültige Zahl, aber sie zeigt auf einen Stack-Slot, der zurückgewonnen wurde – ihn zu lesen liefert Müll oder stürzt ab. Dasselbe passiert, wenn du einen Zeiger auf ein deletetes Heap-Objekt oder auf ein Element eines vector behältst, der später neu allokiert.
Drei Regeln halten dich sicher:
- Gib niemals die Adresse einer lokalen Variablen zurück. Gib per Wert zurück, oder lass den Aufrufer den Speicher besitzen.
- Setze einen Zeiger auf
nullptr, nachdem das, worauf er zeigt, verschwunden ist, und prüfe vor der Verwendung. - Für Besitz und Lebensdauern greife zu Smart Pointern statt zu rohem
new/delete– sie geben Speicher automatisch frei und verkleinern diese gesamte Bug-Klasse.
Als Nächstes: Referenzen vs. Zeiger
Zeiger sind nicht die einzige Möglichkeit, indirekt auf eine andere Variable zu verweisen. C++ hat auch Referenzen, die sich ähnlich anfühlen, aber nicht null sein können, nicht neu zugewiesen werden können und eine sauberere Syntax verwenden. Als Nächstes stellen wir sie unter Referenzen vs. Zeiger nebeneinander, damit du genau weißt, zu welchem Werkzeug du greifen solltest – und warum modernes C++ meist Referenzen bevorzugt, wenn es sie verwenden kann.
Häufig gestellte Fragen
Was ist ein Zeiger in C++?
Ein Zeiger ist eine Variable, die die Speicheradresse eines anderen Werts speichert statt des Werts selbst. Du deklarierst ihn mit * (z. B. int* p), holst dir eine Adresse mit dem Operator & (p = &x) und liest oder schreibst den Wert, auf den er zeigt, durch Dereferenzieren mit *p.
Was ist der Unterschied zwischen & und * bei C++-Zeigern?
Im Zeigerkontext ist & der Adressoperator – &x liefert dir die Adresse von x. * erfüllt zwei Aufgaben: in einer Deklaration (int* p) kennzeichnet es die Variable als Zeiger, und in einem Ausdruck (*p) dereferenziert es den Zeiger, um an den unter dieser Adresse gespeicherten Wert zu gelangen.
Was ist nullptr in C++ und warum sollte man es statt NULL verwenden?
nullptr ist ein typsicheres Null-Zeiger-Literal, das in C++11 eingeführt wurde. Es bedeutet „zeigt auf nichts“. Bevorzuge es gegenüber dem alten NULL oder einer nackten 0, denn nullptr hat einen echten Zeigertyp und wird daher bei der Überladungsauflösung nie mit einer Ganzzahl verwechselt. Prüfe immer if (p), bevor du dereferenzierst – das Dereferenzieren eines Null-Zeigers ist undefiniertes Verhalten.