Menu

C++-Iteratoren erklärt: begin, end und Iteratortypen

Wie C++-Iteratoren als verallgemeinerte Zeiger in Container funktionieren - begin() und end(), Dereferenzieren, Vorrücken, die const-/reverse-Varianten sowie die Stolperfallen der Invalidierung und des Dereferenzierens von end(), die zu undefiniertem Verhalten führen.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Was ein Iterator wirklich ist

Jeder Standardcontainer - vector, string, map, set, list - speichert seine Elemente intern unterschiedlich. Ein vector ist ein zusammenhängender Block, eine map ein balancierter Baum, eine list sind verkettete Knoten. Dennoch kannst du sie alle auf dieselbe Weise durchlaufen. Möglich macht das der Iterator: ein kleines Objekt, das auf ein Element „zeigt" und weiß, wie es zum nächsten weiterschreitet.

Stell dir einen Iterator als verallgemeinerten Zeiger vor. Einen bekommst du mit begin(), das Element, auf das er zeigt, liest du mit *, und mit ++ rückst du ihn vorwärts. Die Teile fügen sich so zusammen:

v.begin() gibt einen Iterator auf das erste Element zurück; *it liefert dir dieses Element; ++it rückt zum nächsten weiter. Dieses Trio - dereferenzieren, vorrücken, vergleichen - ist das gesamte mentale Modell.

begin(), end() und der halboffene Bereich

Die andere Hälfte des Bildes ist end(). Entscheidend: end() zeigt nicht auf das letzte Element - es zeigt auf die Position eine hinter dem letzten Element. Das ist ein bewusst „halboffener" Bereich [begin, end): begin ist eingeschlossen, end ist das Stoppsignal.

Dieser Entwurf macht die Standardschleife sauber - du läufst, bis der Iterator gleich end() ist:

Beachte it != v.end(), nicht it < v.end(). Die meisten Container-Iteratoren (wie map oder list) unterstützen kein <, nur == und !=, daher ist != die portable Wahl. Und auto erspart dir, vector<int>::iterator von Hand zu schreiben - das Schlüsselwort lässt den Compiler den Typ ableiten.

Der Fall des leeren Containers ergibt sich von selbst: Ist ein Container leer, gilt begin() == end(), sodass der Schleifenrumpf nie ausgeführt wird. Keine Sonderbehandlung nötig.

Niemals end() dereferenzieren

Der häufigste Iterator-Bug ist das Dereferenzieren von end(). Da es eine Position hinter das letzte Element zeigt, liest *v.end() Speicher, der dir nicht gehört - undefiniertes Verhalten, das heißt ein Absturz oder stiller Datenmüll, kein freundlicher Fehler:

vector<int> v = {1, 2, 3};
cout << *v.end();   // UNDEFINIERTES VERHALTEN - end() ist kein Element

Dieselbe Falle trifft Suchfunktionen. std::find gibt end() zurück, wenn der Wert nicht gefunden wird, daher musst du vor dem Dereferenzieren prüfen:

Vergleiche den zurückgegebenen Iterator immer mit end(), bevor du ihn dereferenzierst. Dieses if zu vergessen ist eine der häufigsten Absturzursachen in Anfänger-STL-Code.

const, cbegin und reverse-Iteratoren

Container geben je nach Bedarf unterschiedliche Iterator-Varianten heraus:

  • begin() / end() - normale Lese-/Schreib-Iteratoren (*it = ... funktioniert).
  • cbegin() / cend() - const_iterators; du kannst durch sie lesen, das Element aber nicht ändern.
  • rbegin() / rend() - reverse-Iteratoren, die von hinten nach vorn laufen; ++ bewegt sich tatsächlich rückwärts.

Reverse-Iteratoren sind die saubere Art, rückwärts zu durchlaufen, ohne fummelige Index-Rechnerei:

Bei reverse-Iteratoren schreibst du weiterhin ++it, um voranzukommen - der Iterator behandelt die „rückwärts"-Richtung intern. Verwende cbegin()/cend() (oder eine const-Referenz auf den Container), wenn eine Schleife nur lesen soll, damit der Compiler dich daran hindert, versehentlich zu schreiben.

map-Iteratoren liefern Pairs

Nicht jeder Iterator ist nur eine dünne Hülle um einen Zeiger. Ein std::map-Iterator läuft durch einen Baum, und ihn zu dereferenzieren liefert dir ein Paar aus Schlüssel und Wert, auf das du über ->first und ->second zugreifst (genau wie ein Zeiger unterstützt ein Iterator ->):

Die bereichsbasierte for-Schleife baut direkt auf begin()/end() auf, daher greifst du für schlichtes Vorwärtsiterieren meist zu ihr. Explizite Iteratoren lohnen sich, wenn du einen Rückwärtsdurchlauf, die Position eines Elements oder das Übergeben eines Bereichs an einen Algorithmus brauchst.

Der große Stolperstein: Iterator-Invalidierung

Das ist die Falle, in die früher oder später jeder tappt. Wenn du die Struktur eines Containers änderst, können bestehende Iteratoren invalidiert werden - sie zeigen auf Speicher, der freigegeben oder verschoben wurde. Einen davon zu verwenden ist undefiniertes Verhalten.

Bei einem vector kann push_back zum Wachsen den gesamten Puffer neu allokieren und damit jeden offenen Iterator invalidieren. Löschen während des Durchlaufens ist noch berüchtigter - das ist ein klassischer Absturz:

vector<int> v = {1, 2, 3, 4};
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0)
        v.erase(it);   // BUG - erase invalidiert it, dann ist ++it UB
}

Die Lösung: erase gibt einen gültigen Iterator auf das Element nach dem entfernten zurück. Rücke nur weiter, wenn du nicht gelöscht hast:

Beachte, dass der for-Kopf kein ++it enthält - der Rumpf entscheidet, ob weitergerückt wird. (In echtem Code erledigt das das erase-remove-Idiom oder std::erase_if aus C++20 in einer Zeile.) Die Merkregel: Jede Operation, die Elemente hinzufügt oder entfernt, kann Iteratoren invalidieren, halte also keinen alten Iterator über eine solche Änderung hinweg fest.

Weiter: Algorithmen

Da du nun einen Bereich als begin/end-Paar beschreiben kannst, hast du die gesamte Bibliothek der STL-Algorithmen freigeschaltet. Funktionen wie sort, find, count und accumulate ist es egal, welchen Container du hast - sie arbeiten auf Iteratorbereichen, sodass derselbe Aufruf bei einem vector, einem Array oder einem Ausschnitt davon funktioniert. Als Nächstes setzen wir diese Iteratoren ein und lassen die Standardbibliothek das Schleifen für dich übernehmen.

Häufig gestellte Fragen

Was ist ein Iterator in C++?

Ein Iterator ist ein Objekt, das auf ein Element innerhalb eines Containers zeigt und weiß, wie es zum nächsten weiterrückt. Den ersten erhältst du mit container.begin() und eine Markierung für die Position hinter dem letzten Element mit container.end(). Dereferenziere ihn mit *it, um das Element zu lesen oder zu schreiben, und rücke ihn mit ++it weiter. Iteratoren sind die gemeinsame Schnittstelle, dank der STL-Algorithmen mit jedem Container funktionieren.

Was ist der Unterschied zwischen einem Iterator und einem Zeiger in C++?

Bei einem vector oder Array verhält sich ein Iterator fast genau wie ein Zeiger - du dereferenzierst mit *, rückst mit ++ weiter und vergleichst mit ==/!=. Aber ein Iterator ist ein Konzept, nicht zwingend ein roher Zeiger: Ein Iterator von map oder list läuft durch einen Baum oder verkettete Knoten, ist also ein Klassentyp, der * und ++ überlädt. Zeiger sind eine Art von Iterator; Iteratoren verallgemeinern die Idee auf jeden Container.

Was verursacht die Iterator-Invalidierung in C++?

Das Ändern der Struktur eines Containers kann dazu führen, dass bestehende Iteratoren auf freigegebenen oder verschobenen Speicher zeigen. Bei einem vector kann push_back neu allokieren und alle Iteratoren invalidieren; erase invalidiert die Iteratoren am entfernten Element und dahinter. Einen invalidierten Iterator zu verwenden ist undefiniertes Verhalten. Um sicher zu bleiben, verwende den Iterator, den erase zurückgibt, oder reserviere die Kapazität im Voraus.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S