🏠 Startseite

Zeiger (Pointers)

Indirekter Zugriff mit Zeigern

Mächtige Werkzeuge für direkten Speicherzugriff

Fortgeschrittene Algorithmen und Programmierung

Themenübersicht

Teil 1: Grundlagen der Zeiger

  • Was sind Zeiger?
  • Zeiger-Deklaration
  • Adressoperator &
  • Dereferenzierungsoperator *
  • Null-Zeiger

Teil 2: Zeiger und Funktionen

  • Call-by-Value vs. Call-by-Reference
  • Parameter per Zeiger übergeben
  • Mehrere Rückgabewerte

Teil 3: Zeiger und Arrays

  • Arrays als Zeiger
  • Zeigerarithmetik
  • Arrays an Funktionen übergeben

Was sind Zeiger?

Definition:

Ein Zeiger (Pointer) ist eine Variable, die die Speicheradresse einer anderen Variable speichert.

Warum Zeiger?

  • Direkter Zugriff auf Speicherbereiche
  • Effiziente Parameterübergabe bei großen Datenstrukturen
  • Veränderung von Variablen in Funktionen
  • Mehrere Rückgabewerte aus Funktionen
  • Dynamische Speicherverwaltung
  • Arbeit mit Arrays und Strings

Konzept:

Statt direkt mit dem Wert zu arbeiten, arbeiten wir mit der Adresse,
an der der Wert im Speicher liegt.

Speicheradressen verstehen

Jede Variable hat eine Adresse:

Wenn Sie eine Variable deklarieren, reserviert der Computer Speicherplatz.
Dieser Speicherplatz hat eine eindeutige Adresse.

int zahl = 42;

Speicher-Visualisierung:

Variable Speicheradresse Wert
zahl 0x7fff5fbff8ac 42

Die Variable zahl hat den Wert 42
und liegt an der Adresse 0x7fff5fbff8ac im Speicher.

Zeiger-Deklaration

Syntax: datentyp *zeigerName;

int *zeiger;        // Zeiger auf einen int-Wert
double *preisZeiger; // Zeiger auf einen double-Wert
char *buchstabe;     // Zeiger auf einen char-Wert

Wichtige Punkte:

  • Das * (Stern) kennzeichnet eine Zeiger-Variable
  • Der Datentyp gibt an, auf welchen Typ der Zeiger zeigt
  • int *zeiger bedeutet: "zeiger ist ein Zeiger auf int"
  • Der Zeiger selbst speichert nur eine Adresse!

Achtung: Ein nicht initialisierter Zeiger zeigt auf eine zufällige Adresse!
Verwenden Sie immer initialisierte Zeiger.

Adressoperator &

Der & Operator:

Der & Operator gibt die Adresse einer Variable zurück.

int zahl = 42;
int *zeiger;

zeiger = &zahl;  // zeiger erhält die Adresse von zahl

printf("Wert von zahl: %d\n", zahl);         // 42
printf("Adresse von zahl: %p\n", &zahl);     // z.B. 0x7fff5fbff8ac
printf("Wert von zeiger: %p\n", zeiger);     // z.B. 0x7fff5fbff8ac

Visualisierung:

zahl
Adresse: 0x...8ac
Wert: 42
zeiger
Adresse: 0x...8b0
Wert: 0x...8ac

Dereferenzierungsoperator *

Der * Operator (Dereferenzierung):

Der * Operator greift auf den Wert zu, auf den ein Zeiger zeigt.

int zahl = 42;
int *zeiger = &zahl;  // zeiger zeigt auf zahl

printf("Wert von zahl: %d\n", zahl);      // 42
printf("Wert über Zeiger: %d\n", *zeiger); // 42

*zeiger = 100;  // Ändert den Wert von zahl auf 100!

printf("Neuer Wert von zahl: %d\n", zahl);  // 100

Zwei Bedeutungen von *:

  • Bei Deklaration: int *zeiger; → Zeiger-Variable
  • Bei Verwendung: *zeiger = 100; → Dereferenzierung

Wichtig: *zeiger greift auf den Wert an der Adresse zu,
die in zeiger gespeichert ist!

Vollständiges Beispiel

#include <stdio.h>

int main()
{
    double preis = 19.99;
    double *preisZeiger;

    preisZeiger = &preis;  // Adresse von preis speichern

    printf("Preis: %.2f\n", preis);              // 19.99
    printf("Adresse: %p\n", &preis);            // z.B. 0x7fff...
    printf("Zeiger Wert: %p\n", preisZeiger);   // gleiche Adresse
    printf("Wert über Zeiger: %.2f\n", *preisZeiger);  // 19.99

    *preisZeiger = 24.99;  // preis ändern über Zeiger

    printf("Neuer Preis: %.2f\n", preis);       // 24.99

    return 0;
}
Preis: 19.99 Adresse: 0x7fff5fbff8b0 Zeiger Wert: 0x7fff5fbff8b0 Wert über Zeiger: 19.99 Neuer Preis: 24.99

Zeiger-Visualisierung

double preis = 19.99;
double *preisZeiger = &preis;

Speicher-Darstellung:

preis
Adresse:
0x7fff5fbff8b0
Wert:
19.99
preisZeiger
Adresse:
0x7fff5fbff8b8
Wert (= Adresse):
0x7fff5fbff8b0
Der Zeiger preisZeiger zeigt auf die Variable preis

Häufige Fehler mit Zeigern

1. Nicht initialisierte Zeiger:

int *zeiger;      // Zeigt auf zufällige Adresse!
*zeiger = 42;      // ❌ GEFÄHRLICH! Kann abstürzen!

2. Verwechslung von & und *:

int zahl = 10;
int *zeiger;

zeiger = zahl;   // ❌ FALSCH! zahl ist kein Zeiger
zeiger = &zahl;  // ✅ RICHTIG!

3. Zeiger ohne Dereferenzierung verwenden:

int zahl = 10;
int *zeiger = &zahl;

printf("%d", zeiger);   // ❌ Gibt Adresse aus, nicht Wert
printf("%d", *zeiger);  // ✅ Gibt Wert aus (10)

Null-Zeiger

Was ist ein Null-Zeiger?

Ein Null-Zeiger ist ein Zeiger, der auf nichts zeigt.
Er wird mit NULL initialisiert.

#include <stdio.h>
#include <stdlib.h>  // für NULL

int main()
{
    int *zeiger = NULL;  // Zeiger auf nichts

    if (zeiger == NULL) {
        printf("Zeiger ist NULL\n");
    }

    // Vor Verwendung prüfen!
    if (zeiger != NULL) {
        printf("%d\n", *zeiger);
    }

    return 0;
}

Best Practice:

  • Initialisieren Sie Zeiger immer mit NULL oder einer gültigen Adresse
  • Prüfen Sie Zeiger auf NULL vor Dereferenzierung
  • Vermeiden Sie "wilde Zeiger" - Zeiger, die auf ungültige Speicherbereiche zeigen

Zusammenfassung: Zeiger-Grundlagen

Wichtige Konzepte:

Operation Syntax Bedeutung
Deklaration int *zeiger; Zeiger auf int deklarieren
Adresse nehmen zeiger = &zahl; Adresse von zahl holen
Dereferenzieren *zeiger = 42; Wert an Adresse ändern
Null-Zeiger zeiger = NULL; Zeiger auf nichts setzen

Merksatz:

& = "Adresse von" (Address-of)
* = "Wert an Adresse" (Value-at)

Teil 2: Zeiger und Funktionen

Das Problem:

Wenn wir eine Variable an eine Funktion übergeben,
wird nur eine Kopie übergeben (Call-by-Value).

Änderungen in der Funktion wirken sich nicht auf die Original-Variable aus!

Die Lösung: Zeiger!

Mit Zeigern können wir die Adresse übergeben.
So kann die Funktion die Original-Variable ändern (Call-by-Reference).

Vorteile:

  • Funktionen können Variablen ändern
  • Mehrere "Rückgabewerte" möglich
  • Effizient bei großen Datenstrukturen

Call-by-Value: Das Problem

#include <stdio.h>

void aendernUeberWert(double wert1, double wert2)
{
    wert1 = wert1 * 1.5;
    wert2 = wert2 * 2.0;
    printf("In Funktion: %.2f, %.2f\n", wert1, wert2);
}

int main()
{
    double preis1 = 10.0;
    double preis2 = 20.0;

    aendernUeberWert(preis1, preis2);

    printf("In main: %.2f, %.2f\n", preis1, preis2);
    return 0;
}
In Funktion: 15.00, 40.00 In main: 10.00, 20.00

Problem: Die Änderungen in der Funktion bleiben in der Funktion!
preis1 und preis2 in main() bleiben unverändert.

Call-by-Reference: Die Lösung

#include <stdio.h>

void aendernUeberZeiger(double *zeiger1, double *zeiger2)
{
    *zeiger1 = *zeiger1 * 1.5;
    *zeiger2 = *zeiger2 * 2.0;
    printf("In Funktion: %.2f, %.2f\n", *zeiger1, *zeiger2);
}

int main()
{
    double preis1 = 10.0;
    double preis2 = 20.0;

    aendernUeberZeiger(&preis1, &preis2);  // Adressen übergeben!

    printf("In main: %.2f, %.2f\n", preis1, preis2);
    return 0;
}
In Funktion: 15.00, 40.00 In main: 15.00, 40.00

Erfolg! Die Änderungen wirken sich auf die Original-Variablen aus!

Call-by-Value vs Call-by-Reference

Call-by-Value

void funktion(int wert)
{
    wert = 42;  // ändert nur Kopie
}

int main()
{
    int x = 10;
    funktion(x);
    // x ist noch 10
}
  • Kopie wird übergeben
  • Original bleibt unverändert
  • Sicher, aber begrenzt

Call-by-Reference

void funktion(int *zeiger)
{
    *zeiger = 42;  // ändert Original
}

int main()
{
    int x = 10;
    funktion(&x);
    // x ist jetzt 42
}
  • Adresse wird übergeben
  • Original kann geändert werden
  • Mächtig und flexibel

Wann sollte man Zeiger verwenden?

Verwenden Sie Zeiger wenn:

  • Die Funktion eine Variable ändern soll
  • Sie mehrere Werte zurückgeben möchten
  • Sie mit großen Datenstrukturen arbeiten (Arrays, Structs)
  • Sie Arrays an Funktionen übergeben
  • Sie Speichereffizienz wichtig ist (keine Kopien erstellen)

Verwenden Sie Call-by-Value wenn:

  • Die Funktion die Variable nicht ändern soll
  • Sie mit kleinen Werten arbeiten (int, char, etc.)
  • Sie die Original-Daten schützen möchten

Mehrere Rückgabewerte mit Zeigern

Eine Funktion kann normalerweise nur einen Wert zurückgeben.
Mit Zeigern können wir mehrere Werte "zurückgeben":

#include <stdio.h>

void berechneKreis(double radius, double *umfang, double *flaeche)
{
    *umfang = 2 * 3.14159 * radius;
    *flaeche = 3.14159 * radius * radius;
}

int main()
{
    double u, f;

    berechneKreis(5.0, &u, &f);

    printf("Umfang: %.2f\n", u);   // 31.42
    printf("Fläche: %.2f\n", f);   // 78.54

    return 0;
}
Umfang: 31.42 Fläche: 78.54

Beispiel: Swap-Funktion

Aufgabe: Zwei Variablen vertauschen

#include <stdio.h>

void tausche(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main()
{
    int x = 5, y = 10;

    printf("Vorher: x=%d, y=%d\n", x, y);

    tausche(&x, &y);  // Adressen übergeben

    printf("Nachher: x=%d, y=%d\n", x, y);

    return 0;
}
Vorher: x=5, y=10 Nachher: x=10, y=5

Best Practices: Zeiger in Funktionen

1. Klare Benennung:

// Gut: Zeiger-Parameter erkennbar
void aendereWert(int *wertZeiger);

// Besser: Dokumentation
void aendereWert(int *wertZeiger);  // ändert *wertZeiger

2. NULL-Prüfung:

void sichereAenderung(int *zeiger)
{
    if (zeiger == NULL) {
        return;  // Schutz vor NULL-Zeigern
    }
    *zeiger = 42;
}

3. Const für Nur-Lesen:

// Zeiger darf Wert nicht ändern
void leseWert(const int *zeiger)
{
    printf("%d", *zeiger);  // OK
    // *zeiger = 42;  // Fehler!
}

Teil 3: Zeiger und Arrays

Wichtige Erkenntnis:

Der Name eines Arrays ist ein Zeiger auf das erste Element!

int zahlen[5] = {10, 20, 30, 40, 50};

// Diese sind identisch:
printf("%p\n", zahlen);      // Adresse des ersten Elements
printf("%p\n", &zahlen[0]); // Adresse des ersten Elements

// Wert des ersten Elements:
printf("%d\n", zahlen[0]);  // 10
printf("%d\n", *zahlen);     // 10 (gleichwertig!)

Merke:

  • zahlen = Adresse des ersten Elements
  • &zahlen[0] = Adresse des ersten Elements
  • *zahlen = Wert des ersten Elements
  • zahlen[0] = Wert des ersten Elements

Zeigerarithmetik

Was ist Zeigerarithmetik?

Man kann mit Zeigern rechnen, um durch Arrays zu navigieren.

int zahlen[5] = {10, 20, 30, 40, 50};
int *zeiger = zahlen;  // zeiger zeigt auf zahlen[0]

printf("%d\n", *zeiger);       // 10 (erstes Element)
printf("%d\n", *(zeiger + 1)); // 20 (zweites Element)
printf("%d\n", *(zeiger + 2)); // 30 (drittes Element)

// Äquivalent zu:
printf("%d\n", zahlen[0]);  // 10
printf("%d\n", zahlen[1]);  // 20
printf("%d\n", zahlen[2]);  // 30

Wichtig:

zeiger + 1 bedeutet: "Gehe zum nächsten Element"
(nicht zur nächsten Speicheradresse!)

*(zeiger + i) ist dasselbe wie zeiger[i]

Array-Index vs Zeiger-Notation

Array-Notation Zeiger-Notation Bedeutung
zahlen[0] *zahlen Erstes Element
zahlen[1] *(zahlen + 1) Zweites Element
zahlen[2] *(zahlen + 2) Drittes Element
zahlen[i] *(zahlen + i) Element an Position i
&zahlen[i] zahlen + i Adresse von Element i

Merksatz:

zahlen[i] und *(zahlen + i) sind identisch!

Arrays an Funktionen übergeben

Wenn wir ein Array an eine Funktion übergeben, wird automatisch ein Zeiger übergeben:

#include <stdio.h>

// Beide Schreibweisen sind äquivalent:
void ausgabeFeld1(double feld[], int groesse);
void ausgabeFeld2(double *feld, int groesse);

void ausgabeFeld1(double feld[], int groesse)
{
    for (int i = 0; i < groesse; i++) {
        printf("%.2f ", feld[i]);
    }
    printf("\n");
}

int main()
{
    double preise[3] = {1.45, 0.85, 0.75};
    ausgabeFeld1(preise, 3);
    return 0;
}

Wichtig: Die Größe des Arrays geht verloren!
Deshalb müssen wir die Größe als separaten Parameter übergeben.

Vollständiges Array-Beispiel

#include <stdio.h>

void ausgabeFeld(double *dFeld, int groesse)
{
    printf("Array-Elemente:\n");
    for (int i = 0; i < groesse; i++) {
        // Zeiger-Notation verwenden:
        printf("Element %d: %.2f\n", i, *(dFeld + i));
    }
}

int main()
{
    double preise[4] = {1.45, 0.85, 0.75, 2.30};

    ausgabeFeld(preise, 4);

    return 0;
}
Array-Elemente: Element 0: 1.45 Element 1: 0.85 Element 2: 0.75 Element 3: 2.30

Array-Elemente über Zeiger ändern

#include <stdio.h>

void verdoppleFeld(int *feld, int groesse)
{
    for (int i = 0; i < groesse; i++) {
        *(feld + i) = *(feld + i) * 2;  // oder: feld[i] *= 2;
    }
}

int main()
{
    int zahlen[5] = {1, 2, 3, 4, 5};

    printf("Vorher: ");
    for (int i = 0; i < 5; i++) printf("%d ", zahlen[i]);

    verdoppleFeld(zahlen, 5);

    printf("\nNachher: ");
    for (int i = 0; i < 5; i++) printf("%d ", zahlen[i]);

    return 0;
}
Vorher: 1 2 3 4 5 Nachher: 2 4 6 8 10

Warum Array-Größe übergeben?

Problem: Arrays kennen ihre Größe nicht!

Wenn ein Array als Zeiger übergeben wird, geht die Information
über die Größe verloren.

void funktion(int *feld)
{
    // sizeof(feld) gibt NICHT die Array-Größe zurück!
    // Es gibt nur die Größe des Zeigers zurück (z.B. 8 Bytes)

    int groesse = sizeof(feld);  // ❌ FALSCH! (gibt 8 zurück)
}

int main()
{
    int zahlen[100];

    int groesse = sizeof(zahlen) / sizeof(zahlen[0]);  // ✅ OK (100)

    funktion(zahlen);  // Größe geht verloren!
}

Lösung:

Übergeben Sie die Größe immer als separaten Parameter!

Zusammenfassung: Zeiger und Arrays

Wichtige Punkte:

  • Array-Name = Zeiger auf erstes Element
  • zahlen ist gleich &zahlen[0]
  • zahlen[i] ist gleich *(zahlen + i)
  • Arrays werden als Zeiger an Funktionen übergeben
  • Array-Größe muss separat übergeben werden

Vorteile:

  • Effizient: Kein Kopieren großer Arrays
  • Flexibel: Funktionen können Arrays ändern
  • Universell: Funktionen arbeiten mit Arrays beliebiger Größe

Notation-Empfehlung:

Verwenden Sie zahlen[i] für bessere Lesbarkeit,
aber verstehen Sie, dass *(zahlen + i) identisch ist.

Übungsaufgabe: u_zeiger.c

Aufgabenstellung:

Erstellen Sie ein Programm, das:

  • 3 Double-Werte vom Benutzer einliest
  • Diese Werte in Variablen speichert
  • Eine Funktion verwendet, die über Zeiger:
    • Alle 3 Werte ausgibt
    • Den Durchschnitt berechnet und zurückgibt
  • Den Durchschnitt in main() ausgibt

Anforderungen:

  • Verwenden Sie Zeiger für die Parameterübergabe
  • Die Funktion soll die Werte nicht kopieren, sondern über Zeiger zugreifen
  • Berechnen Sie den Durchschnitt und geben Sie ihn über einen Zeiger zurück

Lösung: u_zeiger.c

#include <stdio.h>

void verarbeiteWerte(double *w1, double *w2, double *w3, double *durchschnitt)
{
    printf("Wert 1: %.2f\n", *w1);
    printf("Wert 2: %.2f\n", *w2);
    printf("Wert 3: %.2f\n", *w3);

    *durchschnitt = (*w1 + *w2 + *w3) / 3.0;
}

int main()
{
    double wert1, wert2, wert3, durchschnitt;

    printf("Geben Sie 3 Werte ein:\n");
    scanf("%lf %lf %lf", &wert1, &wert2, &wert3);

    verarbeiteWerte(&wert1, &wert2, &wert3, &durchschnitt);

    printf("\nDurchschnitt: %.2f\n", durchschnitt);

    return 0;
}

Häufige Zeiger-Fehler vermeiden

1. Wild Pointers (Wilde Zeiger):

int *zeiger;  // Nicht initialisiert - zeigt irgendwohin!
*zeiger = 42;  // ❌ GEFAHR! Kann System zum Absturz bringen

// Besser:
int *zeiger = NULL;  // oder: int zahl; int *zeiger = &zahl;

2. Dangling Pointers:

int *gefaehrlich()
{
    int temp = 42;
    return &temp;  // ❌ temp existiert nach Funktionsende nicht mehr!
}

3. Puffer-Überlauf:

int zahlen[5];
int *zeiger = zahlen;
*(zeiger + 10) = 42;  // ❌ Außerhalb des Arrays!

Zeiger-Sicherheit: Best Practices

1. Immer initialisieren:

int *zeiger = NULL;  // ✅ Gut!

2. Vor Verwendung prüfen:

if (zeiger != NULL) {
    *zeiger = 42;  // ✅ Sicher!
}

3. Array-Grenzen einhalten:

for (int i = 0; i < groesse; i++) {  // ✅ Bleibt innerhalb der Grenzen
    *(feld + i) = 0;
}

4. Dokumentieren:

// Ändert *wert auf das Doppelte
void verdopple(int *wert);  // ✅ Klar dokumentiert

Wann sollte man Zeiger verwenden?

Verwenden Sie Zeiger für:

  • Änderung von Funktionsparametern
  • Mehrere Rückgabewerte
  • Übergabe von Arrays
  • Große Datenstrukturen (Structs)
  • Dynamische Speicherverwaltung
  • Verkettete Listen, Bäume
  • Strings (char-Arrays)

Vermeiden Sie Zeiger wenn:

  • Einfache Werte ausreichen
  • Keine Änderung nötig ist
  • Code übersichtlicher ohne Zeiger
  • Sicherheit wichtiger als Performance

Regel: Verwenden Sie Zeiger nur wenn nötig,
aber scheuen Sie sich nicht, wenn sie die richtige Lösung sind!

Zusammenfassung

Teil 1: Grundlagen

  • Zeiger speichern Speicheradressen
  • & holt die Adresse einer Variable
  • * greift auf den Wert an einer Adresse zu
  • Immer mit NULL oder gültiger Adresse initialisieren

Teil 2: Funktionen

  • Call-by-Reference ermöglicht Änderung von Variablen
  • Zeiger erlauben mehrere Rückgabewerte
  • Adressen mit & übergeben

Teil 3: Arrays

  • Array-Name = Zeiger auf erstes Element
  • zahlen[i] = *(zahlen + i)
  • Array-Größe separat übergeben