CodeGym /Kurse /C++ SELF /Sichere Schleifen: range-for, Iteratoren, std::ssize

Sichere Schleifen: range-for, Iteratoren, std::ssize

C++ SELF
Level 8, Lektion 5
Verfügbar

1. Eine Schleife über einen Container ist nicht so trivial

Wenn ein Anfänger die Zeichenkette "Hello" sieht, entsteht der natürliche Wunsch: „Das sind doch fünf Zeichen, worüber reden?“. Und in der Tat: wenn alle Zeichenketten der Welt Länge 5 hätten, würden Programmierer in Harmonie leben und der Debugger ginge in Rente. In Wirklichkeit kann eine Zeichenkette leer sein, Länge 1 haben oder eine Million Zeichen — und gerade an den Grenzen (0, size()-1, size()) entstehen die meisten Fehler.

Das Problem ist, dass eine Schleife nicht nur „N-mal wiederholen“ bedeutet, sondern auch ein Abkommen über Grenzen. An diesem Abkommen sind Typen beteiligt (int, std::size_t), Ausdrücke (i < size()) und manchmal auch Arithmetik (size()-1). Wenn im Abkommen der Punkt „was, wenn die Zeichenkette leer ist?“ fehlt und Sie ihn nicht gelesen haben, erwartet Sie eine Überraschung.

Lassen Sie uns die Werkzeuge zum Durchlaufen eines Containers nach dem Grad der „Nervensicherheit“ aufschlüsseln:

Ansatz Wie es aussieht Wann geeignet Haupt‑Risiko
range-for
for (char c : s)
wenn kein Index gebraucht wird Index ist nicht direkt verfügbar
Indizes (std::size_t)
for (size_t i=0; i<s.size(); ++i)
wenn die Positionsnummer gebraucht wird Rückwärtsschleife und size()-1 bei Leerstring
Iteratoren
for (auto it=s.begin(); it!=s.end(); ++it)
wenn Kontrolle oder ein einheitlicher Stil gewünscht ist der „Stern“ *it erschreckt Anfänger

2. Grundlegende Wege, eine Zeichenkette zu durchlaufen

Range-for: der entspannteste Weg, durch eine Zeichenkette zu gehen

range-for (auch „Bereichsschleife“) ist, wenn Sie dem Compiler sagen: „Lieber Compiler, durchlaufe alle Elemente dieses Dings, während ich etwas Nützliches tue — zum Beispiel die Zeichen ausgeben“. Das ist vergleichbar damit, dass Sie nicht jede Buchreihe nach Index durchgehen, sondern einfach den Regalgang entlanglaufen und Bücher eins nach dem anderen nehmen: geringere Chance, das letzte zu übersehen.

Die Syntax sieht so aus:

for (typ_element variable : container) {
    // Rumpf
}
Allgemeine Form des range-for

Für eine Zeichenkette ist der Container std::string und der Elementtyp char.

Beispiel: Zeichen der Zeichenkette nacheinander ausgeben

#include <iostream>
#include <string>

int main() {
    std::string s = "Hello";

    for (char ch : s) {
        std::cout << ch << '\n';
    }
}

Wenn ausgeführt, sehen Sie:

// H
// e
// l
// l
// o

Beachten Sie: hier gibt es überhaupt keine Indizes, kein size(), keine gefährlichen Vergleiche zwischen int und std::size_t. Ist die Zeichenkette leer, wird die Schleife einfach nullmal ausgeführt — genau das, was wir wollen.

Beispiel: Ziffern in einer Zeichenkette zählen

#include <iostream>
#include <string>

int main() {
    std::string s = "a1b22c";
    int digits = 0;

    for (char ch : s) {
        if (ch >= '0' && ch <= '9') {
            digits += 1;
        }
    }

    std::cout << digits << '\n'; // 3
}

Wichtiges Detail, das wir hier noch nicht vertiefen: range-for kann auch „per Referenz“ arbeiten (char&), um Elemente vor Ort zu verändern. Referenzen sind ein eigenes großes Thema später. Im Moment betrachten wir range-for als Werkzeug, um Elemente sicher zu lesen.

Indizierte Schleife und std::size_t: wenn die Positionsnummer gebraucht wird

Manchmal reicht es nicht, das Zeichen zu sehen; Sie brauchen seine Position. Zum Beispiel möchten Sie „0: H“, „1: e“ ausgeben oder das Zeichen an Position i prüfen. Hier ist range-for weniger praktisch: es liefert Elemente, aber keinen Index.

Dann greifen wir zur indizierten Schleife, machen das aber sorgfältig: wenn die Grenze s.size() ist, ist es logisch, den Index in einem Typ zu halten, der zu dieser Größe passt. Normalerweise ist das std::size_t.

Beispiel: sichere direkte Ausgabe „Index: Zeichen“

#include <cstddef>
#include <iostream>
#include <string>

int main() {
    std::string s = "abcd";

    for (std::size_t i = 0; i < s.size(); ++i) {
        std::cout << i << ": " << s[i] << '\n';
    }
}

Ausgabe:

// 0: a
// 1: b
// 2: c
// 3: d

Hier ist entscheidend, dass i und s.size() im selben „Typen‑Universum“ leben. Keine zusätzlichen Warnungen, keine impliziten Konvertierungen von int → unsigned innerhalb des Vergleichs.

Iteratoren: universelle Methode zum Durchlaufen

Das Wort „Iterator“ klingt, als wäre es erfunden worden, um Menschen in Interviews zu erschrecken. In der Praxis ist ein Iterator einfach ein „Ding, das auf das aktuelle Element zeigt“ und sich zum nächsten verschieben kann. Es ist vergleichbar mit einem Lesezeichen im Buch: das Lesezeichen zeigt die aktuelle Seite, und Sie können weiterschlagen.

Warum braucht man überhaupt Iteratoren? Weil die Standardbibliothek in C++ historisch um die Idee aufgebaut ist: „ein Container stellt begin() und end() bereit, und dann kannst du die Elemente einheitlich durchlaufen“. Selbst wenn ein Container nicht so bequem indizierbar ist wie eine Zeichenkette.

Die minimale Syntax zum Durchlaufen mit Iteratoren sieht so aus:

for (auto it = s.begin(); it != s.end(); ++it) {
    char ch = *it;
}

Hier taucht ein neues Symbol auf: *it. Das ist keine Multiplikation. Das ist die Operation „nimm das Element, auf das der Iterator gerade zeigt“. Ja, dieselbe Sternchen‑Notation wie in der Mathematik — deshalb protestiert das Gehirn zuerst. Aber man gewöhnt sich: das Gehirn lernt schnell, vor allem wenn es regelmäßig mit Compiler‑Fehlern gefüttert wird.

Beispiel: Zeichen über Iteratoren ausgeben

#include <iostream>
#include <string>

int main() {
    std::string s = "Hi";

    for (auto it = s.begin(); it != s.end(); ++it) {
        std::cout << *it << '\n';
    }
}

Ausgabe:

// H
// i

Vorläufig wirkt das wie eine wortreichere Version des range-for. Das ist in Ordnung: in der Basisversion sind Iteratoren wirklich „wortreicher“. Ihre Stärke zeigt sich später, wenn Container vielfältiger werden und das Durchlaufen nicht nur „vorwärts bis Ende“ ist.

3. Rückwärtiges Durchlaufen: wo vorzeichenlose Typen oft versagen

Die Rückwärtsschleife ist der Champion, was Fehlerdichte pro Codefläche angeht. Der Grund ist einfach: wenn der Index vorzeichenlos ist (und std::size_t ist genau das), funktioniert die Idee „bis -1 runterzuzählen“ nicht mehr, weil „-1“ in einer vorzeichenlosen Welt kein Minus‑Eins ist, sondern eine sehr große Zahl.

Der häufigste Fehler sieht so aus (das sollte man nicht tun):

// SCHLECHT: kann zu einer Endlosschleife werden
for (std::size_t i = s.size() - 1; i >= 0; --i) {
    std::cout << s[i] << '\n';
}

Es gibt zwei Probleme. Erstens bricht s.size() - 1 bei einer leeren Zeichenkette. Zweitens ist die Bedingung i >= 0 für ein vorzeichenloses i immer wahr, weil „kleiner als null“ dort nicht existiert.

Sicheres Muster: von size() starten und i - 1 verwenden

#include <cstddef>
#include <iostream>
#include <string>

int main() {
    std::string s = "abcd";

    for (std::size_t i = s.size(); i > 0; --i) {
        std::size_t idx = i - 1;
        std::cout << s[idx] << '\n';
    }
}

Ausgabe:

// d
// c
// b
// a

Der Schlüssel ist die Bedingung i > 0: sie stoppt die Schleife wirklich. Wir vergleichen i nicht >= 0, sondern prüfen „solange wir nicht bei Null angekommen sind“.

std::ssize: wenn ein signed‑Größenwert gebraucht wird

Manchmal möchte man eine Rückwärtsschleife mit einem normalen signed‑Zähler schreiben, der bis einschließlich 0 geht. Aber size() liefert einen vorzeichenlosen Typ, und hier kommt der praktische Helfer std::ssize(...) ins Spiel. Er gibt die Größe als signed‑Zahl zurück (üblicherweise std::ptrdiff_t), sodass man Werte wie -1 als „noch ein Schritt weiter“ bekommen kann.

Wichtig: std::ssize ist kein „magischer Schutz gegen alle Fehler“. Es ist einfach eine bequeme Brücke zwischen „Größe“ und „Zähler, der negativ werden kann“.

Beispiel: Rückwärtsschleife mit std::ssize und Schutz vor leerer Zeichenkette

#include <cstddef>
#include <iostream>
#include <iterator> // std::ssize
#include <string>

int main() {
    std::string s = "abcd";

    if (!s.empty()) {
        for (auto i = std::ssize(s) - 1; i >= 0; --i) {
            std::cout << s[static_cast<std::size_t>(i)] << '\n';
        }
    }
}

Ausgabe:

// d
// c
// b
// a

Zuerst prüfen wir !s.empty(), weil der Ausdruck std::ssize(s) - 1 für eine leere Zeichenkette -1 ergibt, und es dann wichtig ist, nicht auf s[-1] zuzugreifen.

Das static_cast<std::size_t>(i) ist ein klares Bekenntnis: die Stringindizierung erwartet einen vorzeichenlosen Index, unser i ist signed. Wir sagen dem Compiler explizit: „Ich weiß, was ich tue“, und das ist sicher, weil i hier garantiert nicht negativ ist.

4. Mini‑Anwendung TextScope: drei Arten, eine Zeichenkette zu durchlaufen

Jetzt wollen wir etwas Praktisches bauen, damit diese Schleifen nicht wie eine Sammlung von Zaubersprüchen aussehen. Stellen wir uns eine kleine Konsolenanwendung „TextScope“ vor: sie liest eine Zeile ein und zeigt drei Dinge. Erstens: die Zeichen „wie sie sind“ (via range-for). Zweitens: „Index: Zeichen“ (via indizierte Schleife). Drittens: die Zeichenkette rückwärts auf sichere Weise.

Wir starten mit dem einfachen Einlesen einer Zeile. Wir wissen bereits, dass std::getline bequemer für Zeilen mit Leerzeichen ist.

Schritt 1: Zeile einlesen und Zeichen per range-for ausgeben

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    for (char ch : s) {
        std::cout << ch << ' ';
    }
    std::cout << '\n';
}

Wenn Sie "Hi!" eingeben, erhalten Sie:

// H i !

Schritt 2: Indizes mit std::size_t ausgeben

#include <cstddef>
#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    for (std::size_t i = 0; i < s.size(); ++i) {
        std::cout << i << ": " << s[i] << '\n';
    }
}

Wenn Sie "abc" eingeben, sehen Sie:

// 0: a
// 1: b
// 2: c

Schritt 3: Zeichenkette rückwärts ausgeben mit dem sicheren size_t‑Muster

#include <cstddef>
#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    for (std::size_t i = s.size(); i > 0; --i) {
        std::cout << s[i - 1];
    }
    std::cout << '\n';
}

Wenn Sie "abcd" eingeben, ist die Ausgabe:

// dcba

Beachten Sie, wie wir elegant das s.size() - 1 in der Zählervariable vermeiden. Wir starten bei s.size() und gehen dann rückwärts.

5. Wie wählt man den Durchlaufstil aus

Manchmal lautet die Frage: „Also, was soll ich immer verwenden?“. Die ehrliche Antwort: nichts immer. Wie in der Küche: ein Messer ist ein tolles Werkzeug, aber Suppe isst man nicht gut mit dem Messer (auch wenn es sehr selbstbewusst aussieht).

Wenn Sie einfach nur jedes Element lesen und dieselbe Aktion ausführen wollen, ist range-for fast immer am lesbarsten. Wenn Sie einen Index brauchen und der Container schnellen []‑Zugriff unterstützt (z. B. ein String), ist die indizierte Schleife mit std::size_t direkt und klar. Wenn Sie einen einheitlichen Stil wollen, der für verschiedene Container gleich funktioniert, oder wenn Sie das „standardmäßige“ C++‑Denken üben möchten, sind Iteratoren eine gute Investition.

Bei Rückwärtsdurchläufen ist das grundlegende sichere Muster mit std::size_t oft leichter zu lesen als komplizierte Konstrukte, aber std::ssize kann hilfreich sein, wenn es Ihnen psychologisch leichter fällt, mit einem signed‑Zähler zu denken.

6. Typische Fehler beim Durchlaufen von Containern

Fehler Nr. 1: eine Rückwärtsschleife mit std::size_t und der Bedingung i >= 0 zu schreiben.
Das wirkt logisch, wenn man an int denkt, aber für einen vorzeichenlosen Typ erfüllt diese Bedingung nicht die Stop‑Funktion: sie ist immer wahr. In der Folge wird die Schleife entweder endlos oder läuft in sehr große Werte wegen Wraparound.

Fehler Nr. 2: s.size()-1 ohne Prüfung auf empty().
Bei nicht‑leerer Zeichenkette ist das in Ordnung, bei leerer erhalten Sie eine große vorzeichenlose Zahl (weil 0 - 1 in unsigned‑Arithmetik überläuft). Selbst wenn das Programm nicht sofort abstürzt, wird die Logik fehlerhaft.

Fehler Nr. 3: signed‑Index direkt mit size() zu vergleichen.
Die Prüfung if (idx < s.size()) bei einem int idx bricht bei idx = -1: der Compiler konvertiert die Typen und negative Werte werden zu großen vorzeichenlosen Zahlen. Das richtige Muster ist zuerst idx >= 0 zu prüfen und dann den Vergleich unter Verwendung einer Konvertierung zu std::size_t vorzunehmen.

Fehler Nr. 4: std::ssize(s)-1 ohne Schutz vor leerer Zeichenkette.
std::ssize liefert eine signed‑Größe und ist praktisch, aber bei leerer Zeichenkette wird die Startposition -1. Das an sich ist kein Fehler — der Fehler entsteht, wenn man versucht, die Zeichenkette bei -1 zu indexieren. Also entweder if (!s.empty()) machen oder die Schleife so gestalten, dass sie bei i < 0 nicht indiziert.

Fehler Nr. 5: Iterator und Index verwechseln und direkt vergleichen.
Ein Iterator ist keine Zahl. Er ist eine eigene Entität, die mit end() verglichen wird, nicht mit 0 oder size(). Wenn Sie sich fragen „welchen Index hat der Iterator?“, ist es besser, auf eine indizierte Schleife zu wechseln oder einen separaten Zähler parallel zu führen (das ist aber schon eine etwas fortgeschrittenere Technik).

1
Aufgabe
C++ SELF, Level 8, Lektion 5
Gesperrt
Buchstabenweise Dekodierung
Buchstabenweise Dekodierung
1
Aufgabe
C++ SELF, Level 8, Lektion 5
Gesperrt
Protokoll mit Indizes
Protokoll mit Indizes
1
Aufgabe
C++ SELF, Level 8, Lektion 5
Gesperrt
Die erste Ziffer
Die erste Ziffer
1
Aufgabe
C++ SELF, Level 8, Lektion 5
Gesperrt
Rückwärtsausgabe
Rückwärtsausgabe
1
Umfrage/Quiz
signed/unsigned, Level 8, Lektion 5
Nicht verfügbar
signed/unsigned
signed/unsigned
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION