CodeGym /Kurse /C++ SELF /Zeiger T*: speichert Adresse und kann nullptr sein

Zeiger T*: speichert Adresse und kann nullptr sein

C++ SELF
Level 39, Lektion 0
Verfügbar

1. Eintauchen in T*

Wenn Sie zum ersten Mal &x sehen, scheint die Sache erledigt: „Adresse bekommen, fertig“. In echten Programmen wird die Adresse jedoch selten „nur für eine Sekunde“ gebraucht. Häufig will man die Adresse speichern, weitergeben, vergleichen, in einer Variable ablegen, sagen „Objekt ist vorhanden/nicht vorhanden“ oder eine „Referenz auf das gewählte Objekt“ als Zustand halten. Dafür brauchen wir einen eigenen Datentyp: den Zeiger.

Man kann an einen Zeiger wie an einen Zettel mit Koordinaten denken: das Objekt ist die „Wohnung“, der Zeiger ist die „Adresse auf dem Zettel“. Der Zettel kann in Ihrer Tasche liegen, Sie können ihn einem Freund geben (an eine Funktion übergeben), Sie können die Adresse wegradieren und eine neue schreiben. Aber der Zettel ist nicht die Wohnung — das ist der zentrale Punkt dieser Vorlesung.

Was T* im Kern ist: „Adresswert“

Ein Zeiger ist eine Variable, die eine Adresse speichert. Nicht der Wert des Objekts, keine Kopie des Objekts, kein „kleines Objekt innen drin“, sondern genau die Adresse, unter der das Objekt liegt.

  • int x = 10; — die Variable x speichert die Zahl 10
  • int* p = &x; — die Variable p speichert die Adresse von x

Es ist sehr wichtig, das beim Lesen von Code auszusprechen. Nicht „p ist gleich x“, sondern „p zeigt auf x“ oder „in p liegt die Adresse von x“.

Kleine schematische Darstellung (grob, aber ehrlich):

Stack (konzeptionell)
+-------------------+
| x: 10             |   <-- Objekt (Wert)
+-------------------+
| p: 0x7ffd...      |   <-- Zeiger (Adresse von x)
+-------------------+

Adressen wie 0x7ffd... sind nur ein Ausgabefomat (häufig hexadezimal). Und ja: Adressen können bei verschiedenen Programmläufen unterschiedlich sein, das ist normal. Eine Adresse ist keine „Benutzer-ID in Ihrer Datenbank“, sondern eine technische Koordinate während der Lebenszeit des Objekts.

2. Syntax und Initialisierungsdisziplin

Wie man einen Zeiger T* deklariert

Die Syntax sieht so aus:

int* p = nullptr;

Das wird gelesen als: „p — Zeiger auf int“.

Und hier stellen Anfänger üblicherweise zwei gute Fragen:

Erstens: warum der Stern *? Historisch hängt das mit der Dereferenzierungsoperation zusammen (die behandeln wir ausführlich in der nächsten Vorlesung). Vorerst nehmen Sie das * in der Deklaration als „Zeigerzeichen“ wahr.

Zweitens: zu was gehört das * — zum Typ oder zum Namen? In C++ gehört es zur Deklaration, und die Schreibweisen int* p; und int *p; sind äquivalent. Persönlich empfehle ich den Stil, der leichter zu lesen ist:

  • entweder int* p (Stern neben dem Typ: „Zeiger-auf-int“),
  • oder int *p (Stern neben dem Namen: „p — Zeiger“).

Wichtig ist: vermischt die Stile nicht innerhalb eines Projekts, weil das Gehirn sonst Energie für Unwichtiges statt für Logik verschwendet.

Wichtigste Disziplinregel: der Zeiger muss immer initialisiert sein

Die gefährlichste Zeile auf dem Weg mit Zeigern ist diese:

int* p; // schlecht: p ist nicht initialisiert

Ein nicht initialisierter Zeiger enthält eine Mülladresse. Sie kann wie eine „normale Zahl“ aussehen und manchmal „zufällig“ auf gültigen Speicher zeigen, aber das ist schon UB-Bereich und ein Glücksspiel „Glück gehabt/nicht Glück gehabt“.

Gute Gewohnheit am Anfang: der Zeiger ist immer in einem von zwei Zuständen:

1) entweder ist er gleich nullptr (das heißt „zeigt auf nichts“),
2) oder er enthält die Adresse eines realen Objekts, das gerade existiert.

Deshalb ist die typische Startinitialisierung so:

int* p = nullptr;

Das ist wie eine Variable in den Zustand „noch niemand ausgewählt“ zu versetzen. Und das ist völlig normal.

nullptr: der korrekte „leere Zustand“ eines Zeigers

Das Wort nullptr kann man mit „Nullzeiger“ übersetzen. Auf der Bedeutungsebene ist es hilfreicher, so zu denken: nullptr bedeutet „der Zeiger zeigt auf nichts“.

Das ist kein Fehler. Es ist eine normale Situation, wie ein leerer String oder ein leerer vector: der Zustand „noch kein Wert“.

Mini-Beispiel:

#include <iostream>

int main() {
    int* p = nullptr;

    std::cout << std::boolalpha;
    std::cout << (p == nullptr) << '\n'; // true
}

Der Vergleich mit nullptr ist die grundlegende Überprüfung „Objekt vorhanden oder nicht“.

Und noch etwas Wichtiges: in modernem C++ sollten wir genau nullptr verwenden, nicht 0 und nicht NULL. Denn nullptr ist ein spezieller Zeigerwert, mit dem der Compiler klarer arbeitet.

3. Zuweisung, Kopieren und Vergleich von Zeigern

Adresszuweisung: p = &x

Fassen wir das zusammen: Es gibt ein Objekt und einen Zeiger, der dessen Adresse speichert.

#include <iostream>

int main() {
    int x = 10;
    int* p = nullptr;

    p = &x; // jetzt speichert p die Adresse von x

    std::cout << "x  = " << x << '\n';   // x  = 10
    std::cout << "&x = " << &x << '\n';  // &x = 0x...
    std::cout << "p  = " << p << '\n';   // p  = 0x... (gleiche Adresse)
}

In diesem Beispiel sieht man schön, dass p und &x gleich ausgegeben werden (als Adresse), weil in p genau die Adresse &x liegt.

Beachten Sie: wir greifen heute nicht an „was an der Adresse liegt“. Wir trainieren heute die Idee „Zeiger speichert Adresse“ und „kann leer sein“.

Ein Zeiger kopieren heißt: die Adresse kopieren

Einer der häufigsten Denkfehler: ein Student sieht p2 = p1 und denkt unbewusst „das Objekt wurde kopiert“. Nein. Wir haben den Zettel mit der Adresse kopiert. Nun zeigen zwei Zettel auf dieselbe Wohnung.

#include <iostream>

int main() {
    int x = 42;

    int* p1 = &x;
    int* p2 = p1; // Kopie der Adresse

    std::cout << std::boolalpha;
    std::cout << (p1 == p2) << '\n'; // true
}

Die Bedeutung dieser Gleichheit: „sie zeigen auf dasselbe Objekt“.

Diese Eigenschaft ist zugleich sehr mächtig und sehr gefährlich. Mächtig, weil man Zugriff auf ein Objekt teilen kann. Gefährlich, weil man leicht mehrere „Einflussstellen“ auf ein Objekt haben und das dann vergessen kann, sodass man lange rätselt, warum sich etwas „von selbst“ ändert. Das behandeln wir in der nächsten Vorlesung, wenn wir lernen, am Ort zu lesen/schreiben.

Zeiger kann man vergleichen: mit nullptr und untereinander

Vergleiche sind eine weitere Grundoperation für Zeiger. Für diese Vorlesung brauchen wir zwei Vergleichsarten.

Die erste ist der Vergleich mit nullptr. Das prüft „ist ein Objekt vorhanden oder nicht“.

#include <iostream>

int main() {
    int* p = nullptr;

    if (p == nullptr) {
        std::cout << "p is empty\n"; // p is empty
    }
}

Die zweite ist der Vergleich von Zeigern untereinander. Das prüft „dasselbe Objekt oder verschieden“.

#include <iostream>

int main() {
    int a = 1;
    int b = 2;

    int* pa = &a;
    int* pb = &b;

    std::cout << std::boolalpha;
    std::cout << (pa == pb) << '\n'; // false
    std::cout << (pa == &a) << '\n'; // true
}

Wir behandeln hier nicht die Reihenfolge-Vergleiche (<, >). Solche Vergleiche existieren zwar für Zeiger, sind für Anfänger aber meistens unnötig verwirrend. Wichtig ist jetzt die klare Logik: „leer / nicht leer“ und „gleiches Objekt / anderes Objekt“.

4. Typisierung von Zeigern: warum int* und double* nicht gemischt werden

In C++ ist eine Adresse nicht „nur eine Adresse“. Sie trägt immer den Typ: die Adresse eines int ist int*, die Adresse eines double ist double*.

Warum ist das wichtig? Weil der Typ bestimmt, wie die Sprache später den Zugriff über die Adresse interpretiert (Dereferenzierung). Wenn der Typ falsch ist, fordern Sie vom Compiler eine potenziell sinnlose Operation. Deshalb erlaubt C++ nicht einfach so die Zuweisung von Zeigern unterschiedlichen Typs.

Beispiel (das sollte nicht kompilieren — und das ist gut):

int main() {
    int x = 1;
    int* pi = &x;

    // double* pd = pi; // Kompilierfehler: unterschiedliche Zeigertypen
}

Hier hat ein Anfänger vielleicht die „geniale“ Idee: „ich castiere einfach“. In den meisten Lehr- und Praxisfällen ist das eine schlechte Idee. Wenn Sie den Wunsch spüren, so etwas zu casten, ist das oft ein Signal: „Dein Datenmodell ist schlampig, stoppe und denke nach“. Pointer-Casts sind kein Thema dieser Vorlesung, wir vermeiden sie bewusst.

5. Lernscenario: „ausgewählter Benutzer“ als Zeiger

Damit Zeiger nicht wie Magie ohne Nutzen erscheinen, verbinden wir sie mit einer einfachen realen Aufgabe: den „aktuell ausgewählten Benutzer“ als Zustand zu speichern.

Stellen Sie sich vor, wir haben ein struct User (struc ts können wir seit früheren Lektionen), und im main wählen wir manchmal einen Benutzer aus und manchmal setzen wir die Auswahl zurück. Dazu ist eine Variable praktisch, die „entweder einen Benutzer oder keinen“ halten kann.

Genau hier wirkt nullptr sehr natürlich.

#include <iostream>
#include <string>

struct User {
    std::string name;
    int age{};
};

int main() {
    User alice{"Alice", 20};
    User bob{"Bob", 30};

    User* selected = nullptr;      // noch niemand ausgewählt

    selected = &alice;             // Alice ausgewählt
    std::cout << (selected == &alice) << '\n'; // 1 (true)

    selected = nullptr;            // Auswahl zurückgesetzt
    std::cout << (selected == nullptr) << '\n'; // 1 (true)

    selected = &bob;               // Bob ausgewählt
    std::cout << (selected == &bob) << '\n'; // 1 (true)
}

Beachten Sie: wir lesen hier noch nicht die Felder über den Zeiger. Wir verwenden den Zeiger nur als „Marker für die Auswahl“ und lernen, ihn in korrekten Zuständen zu halten. In der nächsten Vorlesung machen wir den nächsten Schritt: das Lesen/Ändern des Objekts über die Adresse.

6. Was man jetzt schon mit T* tun kann

Um die Aktionen zu strukturieren, ist es hilfreich, eine einfache Tabelle im Kopf zu behalten. Sie ersetzt kein Verständnis, hilft aber, nicht in Panik zu geraten, wenn man Zeiger im Code sieht.

Operation Beispiel Bedeutung
Zeiger deklarieren
int* p = nullptr;
„p speichert die Adresse eines int, aktuell leer“
Adresse nehmen
&x
Adresse des Objekts x erhalten
Adresse zuweisen
p = &x;
jetzt zeigt p auf x
Zeiger kopieren
p2 = p1;
Adresse kopieren, nicht das Objekt
Auf Leerheit prüfen
p == nullptr
ist ein Objekt vorhanden oder nicht
„Dasselbe Objekt?“ vergleichen
p1 == p2
zeigen sie auf dasselbe Objekt

Wenn Sie diese Dinge sicher beherrschen und den Sinn verstehen, sind Sie auf einem guten Weg. Weiter geht es mit „Zugriff über die Adresse“, dort wird es besonders interessant.

7. Vorbereitung auf die nächste Vorlesung: Dereferenzierung

Gewöhnlich fragt das Gehirn: „okay, Adresse, aber wie bekomme ich den Wert?“. Antwort: durch die Dereferenzierung mit *p. Dereferenzierung ist jedoch ein eigenes Thema (nächste Vorlesung), denn dort tritt die wichtigste Sicherheitsregel auf: nur einen gültigen Zeiger dereferenzieren.

Um kurz zu zeigen, wohin die Reise geht, hier ein Beispiel, in dem die Dereferenzierung absichtlich auskommentiert ist:

#include <iostream>

int main() {
    int x = 5;
    int* p = &x;

    std::cout << p << '\n';   // 0x... (Adresse)

    // std::cout << *p << '\n'; // 5  (Dereferenzierung — nächste Vorlesung)
}

Genug, um sich zu merken: p ist die Adresse, und *p ist „das, was an der Adresse liegt“. Zwischen beiden liegt eine große Verantwortungsspalte.

8. Typische Fehler im Umgang mit T* und nullptr

Fehler Nr. 1: nicht initialisierter Zeiger (int* p;).
Das ist eine der gefährlichsten Sachen für Anfänger, weil der Fehler nicht sofort sichtbar sein muss. Ein solcher p enthält eine „Mülladresse“, und jede spätere Nutzung kann zu UB führen. Gute Gewohnheit: sofort initialisieren: int* p = nullptr; oder int* p = &x;.

Fehler Nr. 2: Verwendung von 0 oder NULL statt nullptr.
Historisch schrieb man in C und altem C++ oft 0 oder NULL. In modernem C++ ist das schlechter lesbar und kann bei Überladungen zu Mehrdeutigkeiten führen. Wenn Sie „leeren Zeiger“ ausdrücken wollen, schreiben Sie nullptr und ersparen sich späteren Ärger.

Fehler Nr. 3: erwarten, dass p2 = p1 das Objekt kopiert.
Die Zuweisung von Zeigern kopiert nur die Adresse. Am Ende zeigen zwei Variablen auf dasselbe Objekt, und das kann zwei Programmteile unerwartet „verbinden“. Nützliche Gewohnheit: sich sagen: „ich kopiere die Koordinaten, nicht die Daten“.

Fehler Nr. 4: Verwechslung des Zeigertyps (int* vs. double*).
Adressen sind in C++ typisiert. Wenn Sie versuchen, einen int* einem double* zuzuweisen, beschwert sich der Compiler nicht ohne Grund: sonst würde man Speicher falsch interpretieren. Am Anfang ist es ratsam, Zeiger-Casts ganz zu vermeiden und Compilerfehler als Hinweis zu nehmen: „du versuchst verschiedene Entitäten zu vermischen“.

Fehler Nr. 5: „einmal auf nullptr geprüft und für immer beruhigt“.
Die Prüfung p != nullptr sagt nur „der Zeiger ist nicht leer“. Sie garantiert nicht, dass die Adresse jederzeit gültig bleibt. Die Themen „Gültigkeit einer Adresse über die Zeit“ (lifetime) und „hängende Zeiger“ sind eigene Gespräche und folgen später. Wichtig ist jetzt: nullptr betrifft Leere, die Korrektheit der Adresse betrifft die Lebensdauer des Objekts.

1
Aufgabe
C++ SELF, Level 39, Lektion 0
Gesperrt
Fernbedienung und Adresse
Fernbedienung und Adresse
1
Aufgabe
C++ SELF, Level 39, Lektion 0
Gesperrt
Zwei Badges
Zwei Badges
1
Aufgabe
C++ SELF, Level 39, Lektion 0
Gesperrt
Auswahl des führenden Spielers
Auswahl des führenden Spielers
1
Aufgabe
C++ SELF, Level 39, Lektion 0
Gesperrt
Aktive Stadt
Aktive Stadt
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION