1. erase() und Invalidierung von Bindungen
Wenn man zum ersten Mal hört „ein Element aus einem Array entfernen“, stellt das Gehirn sich folgendes Bild vor: man streicht ein Kästchen durch, der Rest bleibt unverändert. Bei std::vector geht das aus praktischen Gründen nicht: er speichert die Elemente contigu in einem Puffer, ohne Lücken. Daher ist Entfernen fast immer ein Verschieben der verbleibenden Elemente nach links. Das bedeutet, Positionen ändern sich und einige „gespeicherte Verweise“ auf Elemente sind nicht mehr korrekt.
Stellen wir uns eine To‑Do‑Liste vor: ["Milch kaufen", "Katze waschen", "Labor abgeben"]. Wenn das mittlere Element entfernt wird, muss der Vector dafür sorgen, dass die Elemente wieder kontigu liegen: ["Milch kaufen", "Labor abgeben"]. Für Sie erscheint das logisch, aber für einen Iterator, der auf das Element „Labor abgeben“ zeigte, ist das eine völlig andere Situation: er kann ungültig werden oder auf das „Falsche“ zeigen.
Modell ohne Lücken: was physisch bei erase passiert
Bevor man Code schreibt, ist es nützlich, für ein paar Minuten den „Röntgenmodus“ einzuschalten und sich vorzustellen, wie ein vector innen aussieht. Er hat einen Speicherpuffer, der Platz für capacity() Elemente bietet, und die tatsächliche Anzahl an Elementen size() (das, was real existiert). Ein Element in der Mitte zu entfernen ist nicht nur „size minus eins“, sondern bedeutet auch Verschieben von Elementen, um die Lücke zu schließen.
Schematisch kann man sich das so vorstellen:
Vorher:
Indizes: 0 1 2 3
Daten: [10] [20] [30] [40]
Wir entfernen das Element bei Index 1 (20)
Nachher:
Indizes: 0 1 2
Daten: [10] [30] [40]
Das Element [30] ist auf die Position von [20] gerutscht, [40] ist auf die Position von [30] gerutscht. Wichtig: an der Stelle des entfernten Elements steht nun ein anderes Element. Deshalb werden jegliche Bindungen zu den Elementen rechts davon (Iteratoren, Referenzen) gefährlich: sie zeigen entweder ins Leere oder auf etwas anderes.
erase(pos): ein Element per Iterator entfernen
std::vector entfernt Elemente über die Methode erase, und das Entscheidende ist, dass sie einen Iterator und keinen Index entgegennimmt. Das ist logisch: ein Iterator ist eine „Position“ im Container, und erase kann anhand dieser Position löschen.
„Nach Index löschen“ ist ebenfalls möglich, aber dann wandeln Sie zuerst den Index in einen Iterator um (oder laufen mit ++ bis zur gewünschten Position) und rufen danach erase auf.
Mini‑Beispiel: wir entfernen das zweite Element (20), ohne Iterator‑Arithmetik zu verwenden, nur mit ++.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30, 40};
auto it = v.begin();
++it; // jetzt zeigt it auf 20
v.erase(it); // entfernen 20
for (std::size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << ' '; // 10 30 40
}
std::cout << '\n';
}
erase(it) setzt den Platz nicht auf null, sondern rekonstruiert die Sequenz. Nach dem Entfernen nimmt das nächste Element (30) die Position des entfernten ein.
erase(first, last): Bereich [first, last) entfernen
Manchmal möchte man nicht ein einzelnes Element entfernen, sondern ein ganzes Stück, z. B. „Elemente von 2 bis 4“. In einem vector macht man das mit erase(first, last), und hier gibt es eine sehr wichtige Regel, die Ihnen in C++ oft begegnen wird: Bereiche sind als [first, last) definiert, also links inklusiv und rechts exklusiv.
last ist die Position nach dem letzten zu löschenden Element.
Entfernen wir aus [1, 2, 3, 4, 5] die Zahlen 2, 3, 4, und sammeln die Iteratoren wieder über ++:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3, 4, 5};
auto first = v.begin();
++first; // auf 2
auto last = first;
++last; // auf 3
++last; // auf 4
++last; // auf 5 (also "nach 4")
v.erase(first, last); // entfernen 2,3,4
for (std::size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << ' '; // 1 5
}
std::cout << '\n';
}
Nützliche Formulierung zur Kontrolle: last muss auf das Element nach dem letzten zu löschenden zeigen. Im Beispiel ist das letzte zu löschende Element 4, also muss last auf 5 zeigen.
Was erase zurückgibt und warum das Schleifen rettet
Wenn erase einfach nur gelöscht und nichts zurückgegeben hätte, würde Löschen während des Durchlaufens zu einem Ratespiel werden, wohin der Iterator jetzt zeigt. Deshalb gibt erase einen Iterator zurück: die Position des Elements, das auf die Stelle des entfernten Elements gerückt ist (oder end(), falls das letzte Element gelöscht wurde).
Mini‑Beispiel: wir löschen 6 und arbeiten weiter mit dem von erase zurückgegebenen Iterator.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{5, 6, 7, 8};
auto it = v.begin();
++it; // it auf 6
it = v.erase(it); // 6 gelöscht, it zeigt jetzt auf 7
if (it != v.end()) {
std::cout << *it << '\n'; // 7
}
}
Wichtige Gewohnheit: vertraue nach erase nicht dem alten Iterator, sondern benutze den von der Funktion zurückgegebenen Iterator.
Entfernen in einer Schleife: schlechter und guter Ansatz
Wenn man zum ersten Mal schreibt „alle Elemente löschen, die gleich X sind“, tendiert man dazu zu: „ich laufe den Vector ab, wenn ich X finde — erase“. Und das ist genau der Fall, in dem der Code zwar manchmal funktioniert, aber besonders gefährlich ist.
Der schlechte Fall sieht meist so aus: der Iterator wird im Schleifenkopf erhöht, und im Inneren ruft man zusätzlich erase auf, wodurch die aktuelle Position zerstört wird.
#include <vector>
int main() {
std::vector<int> v{1, 2, 2, 3};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 2) {
v.erase(it); // danach ist it nicht mehr sicher
}
}
}
Die richtige Idee ist folgende: wenn wir das aktuelle Element gelöscht haben, liefert erase direkt die „nächste Position“. Wenn wir nichts löschen, dann erhöhen wir den Iterator selbst mit ++it.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{1, 2, 2, 3};
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 2) {
it = v.erase(it); // nehmen die zurückgegebene Position
} else {
++it; // bewegen uns selbst
}
}
for (std::size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << ' '; // 1 3
}
std::cout << '\n';
}
Beachten Sie: im Schleifenkopf gibt es kein ++it. Das ist kein „Trick“, sondern Disziplin: nur ein Codeabschnitt ist verantwortlich für das Weiterbewegen des Iterators, so vermeidet man „Doppelschritte“ oder Schritte auf bereits gelöschte Positionen.
Invalidierung: was genau wird nach erase ungültig
Unter Invalidierung verstehen wir die Situation, in der ein Iterator/Reference/Pointer nicht mehr garantiert korrekt auf das Element zugreifen kann, auf das er „gedeutet“ hat. So etwas zu benutzen ist wie die Adresse auf einem Briefumschlag zu lesen, nachdem das Haus bereits umgezogen ist.
Für std::vector hilft diese Merkhilfe:
| Operation am vector | Was kaputtgehen kann | Intuition |
|---|---|---|
| erase(pos) / erase(first,last) | Iteratoren/Referenzen/Pointer auf das gelöschte Element und auf Elemente rechts davon | Elemente werden nach links verschoben |
| push_back(...) (wenn kein Platz und ein „Umzug“ nötig ist) | Alle Iteratoren/Referenzen/Pointer auf Elemente | Der Puffer ändert sich, Elemente haben andere Adressen |
| reserve(...) (wenn capacity vergrößert wurde und ein „Umzug“ stattfand) | Alle Iteratoren/Referenzen/Pointer | derselbe „Umzug“ |
Über den „Umzug“ des Vectors haben wir bereits gesprochen, als es um capacity() und Reallokation ging. Hier die Konsequenz: wenn der Vector seinen internen Puffer geändert hat, wird jede Bindung an Elemente (Iterator, Referenz) gefährlich.
Eine kleine Randbemerkung aus der C++‑Praxis: die Formulierungen der Standardregeln zu erase und Invalidierung wurden historisch oft diskutiert und präzisiert. Wenn Ihnen erase heute „glitschig“ vorkommt, ist das normal — das Thema ist wirklich subtil.
Referenz auf ein Element: warum T& gefährlich bei Änderungen am vector
Wenn Sie schreiben int& ref = v[1];, erstellen Sie eine Referenz — einen zweiten Namen für ein konkretes Element des Vectors. Optisch ähnelt das einer normalen Variable, aber ein wichtiger Unterschied ist, dass sie nicht unabhängig lebt, sondern auf Speicher zeigt, der dem Vector gehört. Wenn der Vector restrukturiert wird (Entfernen/„Umzug“), kann die Referenz ungültig werden.
Beispiel „sieht harmlos aus, ist aber gefährlich“:
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
int& ref = v[1]; // ref referenziert "20"
v.erase(v.begin()); // 10 gelöscht, Elemente verschoben
// ref kann jetzt "falsch" referenzieren.
}
Hier ist es wichtig, nicht zu versuchen zu erraten „was genau passiert“: im Lehrmodell reicht es zu verstehen, dass nach Änderung der Struktur des Vectors alte Referenzen/Iteratoren nicht mehr verwendet werden dürfen.
Ein noch realistischeres Szenario: Sie nehmen eine Referenz, danach rufen Sie push_back auf und der vector entscheidet sich für einen Umzug in einen neuen Puffer (weil capacity() erschöpft ist). Dann ist die Referenz praktisch sicher kaputt.
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
int& ref = v[1]; // "20"
v.push_back(40); // Reallokation kann passieren
// ref kann ungültig werden
}
Ja, das ist unangenehm. Aber jetzt ist klar, warum in realen Projekten oft geraten wird, Referenzen auf Vector‑Elemente nicht länger aufzubewahren als nötig.
Mini‑Task‑Liste: eine Aufgabe nach Index entfernen
Wir haben noch keine Klassen oder komplexe Architektur, daher bauen wir eine Lern‑„Mini‑Task‑Liste“: std::vector<std::string> tasks, wobei jede Zeichenkette eine Aufgabe ist. Wir fügen das Entfernen nach Nummer (Index) hinzu, aber vorsichtig: zuerst Grenzen prüfen, dann den Iterator finden und erase aufrufen.
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> tasks{"Buy milk", "Wash cat", "Submit lab"};
std::size_t idx = 0;
std::cin >> idx;
if (idx < tasks.size()) {
auto it = tasks.begin();
for (std::size_t k = 0; k < idx; ++k) ++it;
tasks.erase(it);
}
for (std::size_t i = 0; i < tasks.size(); ++i) {
std::cout << i << ": " << tasks[i] << '\n';
}
}
Darin stecken zwei wichtige Gewohnheiten. Erstens prüfen wir, ob idx < tasks.size(), weil der Index von außen (Eingabe) kommt und deshalb kein Vertrauen verdient. Zweitens versuchen wir nicht, direkt per Index zu löschen, sondern wandeln den Index ehrlich in einen Iterator um, weil erase mit Positionen arbeitet.
Wenn Sie jetzt gedacht haben „hach, ich möchte tasks.begin() + idx“, Glückwunsch: Ihr Gehirn hat bereits die Idee der Random‑Access‑Iteratoren gespürt. In diesem Kursabschnitt halten wir uns allerdings an das, was wir eingeführt haben: begin()/end() und ++.
2. Typische Fehler beim Entfernen aus std::vector
Fehler Nr. 1: den Iterator nach erase verwenden, als wäre nichts geschehen.
erase verändert die Struktur des Vectors und macht die aktuelle Position „rutschig“. Typische Symptome: das Programm überspringt manchmal Elemente, stürzt gelegentlich ab oder verhält sich seltsam. Abhilfe ist Disziplin: nach it = v.erase(it) verwenden Sie genau den zurückgegebenen Iterator und inkrementieren it nicht automatisch „aus Gewohnheit“.
Fehler Nr. 2: den Bereich [first, last) verwechseln und erwarten, dass last auch gelöscht wird.
Das ist Klassiker: Sie wollen „von 2 bis 4 inclusive“ löschen, setzen last auf das Element 4 und wundern sich, dass 4 bleibt. Das richtige Modell ist nicht „inklusive“, sondern „rechts exklusiv“: last muss auf die Position nach dem letzten zu löschenden Element zeigen.
Fehler Nr. 3: eine Referenz (T&) auf ein Element halten und dann den vector verändern.
Eine Referenz sieht wie eine bequeme Variable aus, ist aber an den internen Speicher des Containers gebunden. Nach erase, push_back (bei Reallokation) oder sogar reserve (falls ein „Umzug“ stattfand) kann die Referenz ungültig werden. Wenn Sie nur den Wert merken müssen, machen Sie eine Kopie (z. B. int x = v[i];). Wenn Sie eine Position merken wollen, bedenken Sie, dass diese Position bei Modifikationen brechen kann.
Fehler Nr. 4: versuchen, innerhalb einer for‑Schleife mit (...;++it) zu löschen und gleichzeitig erase(it) aufzurufen.
Dabei bricht die Schrittlogik: erase hat die Sequenz bereits neu organisiert, und Sie inkrementieren den Iterator nach altem Schema. Im besten Fall überspringen Sie die Prüfung des Elements, das in die gelöschte Position gerutscht ist; im schlimmsten Fall greifen Sie auf eine nicht valide Position zu. Ein zuverlässiges Muster ist eine Schleife ohne automatisches ++it, wobei der Schritt entweder durch ++it oder durch it = erase(it) erfolgt.
Fehler Nr. 5: denken, Invalidierung tritt nur nach erase auf.
Auch wenn wir uns hier auf Entfernen konzentrieren, ist die Verbindung zur capacity() wichtig: wenn der Vector wegen Wachstum des Puffers umzieht, brechen alle Iteratoren/Referenzen. Daher ist das Muster „Iterator speichern, viele push_back, dann den gespeicherten Iterator nutzen“ eine Einladung zu unerwarteten Problemen.
GO TO FULL VERSION