Zum Inhalt springen

Zeiger: Adressen, Dereferenzierung & Arithmetik

Verstehe Zeiger als Speicheradressen, lerne den Adress- und den Dereferenzierungsoperator kennen, arbeite mit Zeigerarithmetik in Arrays, achte auf const-Korrektheit bei Zeigern und erfahre, wann rohe Zeiger im modernen C++ noch angebracht sind.

Was ist ein Zeiger?

Ein Zeiger ist eine Variable, die eine Speicheradresse speichert. Jedes Objekt in C++ liegt an irgendeiner Adresse im Speicher, und ein Zeiger erlaubt dir, dieses Objekt indirekt anzusprechen. Zeiger sind fundamental für C++ — sie ermöglichen dynamische Datenstrukturen, Polymorphie und effiziente Parameterübergabe.

Ein Zeigertyp wird mit * deklariert: int* p; deklariert einen Zeiger auf ein int. Der Zeiger selbst belegt Speicher (typischerweise 8 Bytes auf 64-Bit-Systemen), und sein Wert ist eine Adresse, nicht die Daten, auf die er zeigt.

Adressoperator & Dereferenzierung

Die beiden grundlegenden Zeigeroperatoren sind & (Adresse von) und * (Dereferenzierung). Der Operator & liefert die Adresse einer Variablen. Der Operator * folgt der Adresse, um auf das Objekt zuzugreifen, auf das gezeigt wird.

address_deref.cpp
#include <iostream>

int main() {
    int value = 42;
    int* ptr = &value;  // ptr holds the address of value

    std::cout << "value:  " << value << '\n';        // 42
    std::cout << "&value: " << &value << '\n';       // e.g. 0x7ffd5a3c
    std::cout << "ptr:    " << ptr << '\n';           // same address
    std::cout << "*ptr:   " << *ptr << '\n';          // 42 (dereference)

    *ptr = 100;  // modify value through the pointer
    std::cout << "value after *ptr = 100: " << value << '\n'; // 100

    int* null_ptr = nullptr;  // points to nothing — safe sentinel
    // *null_ptr = 5;  // UNDEFINED BEHAVIOR — crash on most systems
}

Zeigerarithmetik & Array-Decay

Zeigerarithmetik ist nur innerhalb eines Arrays gültig (oder ein Element hinter dem Ende). Der Compiler skaliert die Arithmetik mit sizeof(T). Ein Arrayname zerfällt in den meisten Kontexten zu einem Zeiger auf sein erstes Element — deshalb verlieren C-Arrays ihre Größeninformation, wenn sie an Funktionen übergeben werden.

pointer_arithmetic.cpp
#include <iostream>
#include <cstddef>  // std::ptrdiff_t

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int* p = arr;  // array decays to pointer to first element

    std::cout << *p << '\n';        // 10
    std::cout << *(p + 2) << '\n';  // 30  (moves 2*sizeof(int) bytes)
    std::cout << p[3] << '\n';      // 40  (p[i] is *(p + i))

    // Iterating via pointer
    for (int* it = arr; it != arr + 5; ++it) {
        std::cout << *it << ' ';
    }
    std::cout << '\n';  // 10 20 30 40 50

    // Pointer difference
    std::ptrdiff_t diff = (arr + 4) - arr;  // 4 (elements, not bytes)
    std::cout << "diff: " << diff << '\n';

    // WARNING: arithmetic outside the array is UNDEFINED BEHAVIOR
    // int* bad = arr + 10;  // UB — don't do this
}

const-Zeiger vs. Zeiger auf const

Die Platzierung von const relativ zum * entscheidet, was unveränderlich ist:

- const int* p (oder int const* p) — Zeiger auf const: Du kannst den Wert, auf den gezeigt wird, nicht über p ändern, aber du kannst p auf etwas anderes zeigen lassen.
- int* const p — const-Zeiger: Der Zeiger selbst ist unveränderlich (kann nicht umgehängt werden), aber du kannst den Wert ändern, auf den er zeigt.
- const int* const p — sowohl der Zeiger als auch der Wert, auf den er zeigt, sind unveränderlich.

Lies Deklarationen von rechts nach links: int* const p liest sich als "p ist ein const-Zeiger auf int."

void-Zeiger (void*) können jede Adresse aufnehmen, lassen sich aber ohne Cast nicht dereferenzieren. Sie werden vor allem in C-APIs und Allokator-Implementierungen verwendet.

Best Practice

Im modernen C++ sollten rohe Zeiger ausschließlich als nicht besitzende, beobachtende Zeiger eingesetzt werden. Sie sagen: "Ich kann dieses Objekt sehen, aber ich bin nicht für seine Lebensdauer verantwortlich." Gängige legitime Einsätze:

- Implementierung von Datenstrukturen (Baumknoten, die auf den Elternknoten zeigen)
- Funktionsparameter, die beobachten, aber nicht besitzen (auch wenn Referenzen oft besser sind)
- Anbindung an C-APIs
- Iteratoren in eigenen Containern

Wenn ein Zeiger eine Ressource besitzt, verpacke ihn in std::unique_ptr oder std::shared_ptr. Wenn du dabei bist, delete zu schreiben, machst du im modernen C++ mit ziemlicher Sicherheit etwas falsch.

Stolperfalle

Dangling-Zeiger sind der Zeigerfehler Nummer 1. Ein Zeiger wird zum Dangling-Zeiger, wenn das Objekt, auf das er zeigt, zerstört wird:

- Einen Zeiger auf eine lokale Variable zurückgeben
- Ein Objekt löschen, während ein anderer Zeiger es noch referenziert
- Ein Zeiger in einen std::vector, der nach einem push_back reallokiert wird

Nicht initialisierte Zeiger enthalten Müll-Adressen. Initialisiere sie immer mit nullptr, wenn du noch kein Ziel hast. Der Compiler warnt NICHT bei jeder Verwendung eines nicht initialisierten Zeigers.

Zeiger vs. Referenz: Wann was?

Referenzen und Zeiger bieten beide Indirektion, unterscheiden sich aber in wichtigen Punkten. Referenzen können nicht null sein, können nicht umgehängt werden und erfordern keine explizite Dereferenzierung. Bevorzuge Referenzen für Funktionsparameter; verwende Zeiger, wenn du Null-Werte oder das Umhängen brauchst.

ptr_vs_ref.cpp
#include <iostream>

void increment_ref(int& val) { ++val; }         // cannot be null
void increment_ptr(int* val) { if (val) ++(*val); } // must null-check

int main() {
    int x = 10;
    increment_ref(x);        // clean call syntax
    increment_ptr(&x);       // caller must take address
    std::cout << x << '\n';  // 12

    int* p = &x;
    p = nullptr;  // OK — pointers can be re-seated and nulled

    int& r = x;
    // r = ???;    // references cannot be re-seated
    // int& bad;   // ERROR — references must be initialized
}
Wichtigste Erkenntnisse
  • & liefert eine Adresse, * folgt ihr — das sind inverse Operationen
  • Zeigerarithmetik ist nur innerhalb von Arrays gültig und skaliert mit sizeof(T)
  • Arraynamen zerfallen zu Zeigern und verlieren dabei die Größeninformation
  • Lies die const-Platzierung von rechts nach links: const int* vs. int* const
  • Im modernen C++ sollten rohe Zeiger nur nicht besitzende Beobachter sein
  • Bevorzuge Referenzen gegenüber Zeigern, außer du brauchst Null-Werte oder das Umhängen

Quiz — Teste dein Wissen

(15 XP)

1. Was bedeutet `const int* p`?

2. Was passiert, wenn ein Array an eine Funktion übergeben wird, die einen Zeigerparameter erwartet?

3. Wann ist Zeigerarithmetik in C++ gültig?