1. Einführung
Wenn Sie jemals versucht haben, einen String „manuell zu parsen“ (also Zeichenweise zu laufen, Leerzeichen zu finden, Indizes zu zählen und dabei nicht ins npos zu fallen), dann wissen Sie bereits: das ist gut zum Üben der Aufmerksamkeit … aber als Programmierer fühlen Sie sich wie ein Minensucher ohne Plan.
Im echten Leben kommt oft ein String in dieser Form:
- sum 10 20
- add milk 3
- move 2 5
Und Sie wollen das genauso lesen wie von der Konsole: ein Wort‑Kommando, dann ein Paar Zahlen, und fertig. Genau hier kommt die geniale Idee der Standardbibliothek ins Spiel: „Warum nicht einen Stream machen, der nicht von der Tastatur liest, sondern aus einem String“.
std::stringstream ist ein Stream über einem String. Er kann:
- mit dem Operator >> aus dem String lesen, genau wie std::cin;
- mit dem Operator << in den String schreiben, genau wie std::cout;
- einen internen Puffersring halten, den man mit .str() bekommen kann.
Im C++-Standard ist das ein eigenständiger großer Teil der Bibliothek (in Entwürfen des Standards finden Sie Erwähnungen des Abschnitts [stringstream]).
String als Konsole
Machen wir eine kleine Analogie. std::cin ist wie ein „Band mit Daten“, das vom Nutzer kommt. Wir lesen Tokens davon: Wörter, Zahlen usw. std::stringstream ist dasselbe Band, nur sind die Daten bereits in einem std::string geschrieben.
Das heißt: wir lesen zunächst die ganze Zeile (meist via std::getline) und starten dann eine „Mini‑cin“ über dieser Zeile, um Stücke herauszuziehen.
Die Kernregel des Stream-Lesens: der Operator >> teilt standardmäßig nach Whitespace (Leerzeichen, Tabulator, Zeilenumbruch). Deshalb wird das Format „Wort + Zahl + Zahl“ sehr natürlich eingelesen.
2. Kommando und Argumente lesen
Includes und erstes Beispiel
Wenn Sie stringstream sehen, brauchen Sie in der Regel:
#include <sstream>
#include <string>
#include <iostream>
Machen wir ein kleines Beispiel: im String steht das Kommando "sum 10 20".
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string line = "sum 10 20";
std::stringstream ss(line);
std::string cmd;
int a{}, b{};
ss >> cmd >> a >> b;
std::cout << cmd << ": " << (a + b) << '\n'; // sum: 30
}
Sieht fast aus wie cin, und genau dafür ist es gedacht.
Aber in diesem Code steckt ein klassisches Anfängerproblem: wir haben nicht geprüft, ob das Lesen überhaupt geklappt hat. Wenn die Zeile "sum ten 20" wäre, würde das Lesen von a fehlschlagen und das Programm würde mit „mythischen“ Daten weiterleben. Das korrigieren wir jetzt.
Prüfung des Leseerfolgs: if (ss >> ...)
Wenn der Stream einen Wert nicht in den erwarteten Typ konvertieren kann (z. B. erwartet int, trifft aber auf "ten"), geht er in einen Fehlerzustand. Das Tolle: das Ergebnis des Leseoperators kann als Bedingung verwendet werden.
Machen wir einen „Schutz gegen schlechtes Format“:
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string line = "sum ten 20";
std::stringstream ss(line);
std::string cmd;
int a{}, b{};
if (ss >> cmd >> a >> b) {
std::cout << cmd << ": " << (a + b) << '\n';
} else {
std::cout << "bad format\n"; // bad format
}
}
Wichtig ist hier: wir verwenden a und b nur, falls das Lesen erfolgreich war. Das ist keine Style‑Frage, sondern vermeidet, ein Haus auf unsicherem Grund zu bauen.
3. Tokens: Wörter, Zahlen und gemischte Typen
Der Stream kann hintereinander unterschiedliche Typen lesen, was sehr praktisch ist, wenn das Format fest ist. Aber man muss verstehen, dass der Stream nicht „rät“, was Sie gemeint haben: er versucht ehrlich, Text in den gewünschten Typ zu konvertieren.
Schauen wir typische Fälle an.
Wörter lesen
Stellen Sie sich das Kommando "hello Alice" vor.
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string line = "hello Alice";
std::stringstream ss(line);
std::string cmd, name;
if (ss >> cmd >> name) {
std::cout << "cmd=" << cmd << ", name=" << name << '\n'; // cmd=hello, name=Alice
}
}
Wenn der Name aus zwei Wörtern besteht ("Alice Cooper"), hilft der normale >> nicht — er trennt am Leerzeichen. Das ist in Ordnung: dann muss das Format Zitate oder eine andere Lösung vorsehen. Das besprechen wir in der nächsten Lektion (std::quoted). Heute leben wir ehrlich in einer Welt von whitespace‑getrennten Tokens.
Zahlen lesen
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string line = "mul 7 8";
std::stringstream ss(line);
std::string cmd;
int x{}, y{};
if (ss >> cmd >> x >> y) {
std::cout << cmd << ": " << (x * y) << '\n'; // mul: 56
}
}
Gemischte Typen: Wort + int + double
Solche Formate treten oft in „Mini‑Kommandos“ auf:
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string line = "buy 3 19.90";
std::stringstream ss(line);
std::string cmd;
int count{};
double price{};
if (ss >> cmd >> count >> price) {
std::cout << cmd << ": total=" << (count * price) << '\n'; // buy: total=59.7
}
}
Nochmals: wenn irgendwo in der Mitte das Format bricht, fängt das if das ab.
4. Formatkontrolle und Stream‑Zustand
Prüfen, dass nichts Überflüssiges vorhanden ist
Ein häufiges Problem: die Zeile passt scheinbar, aber der Nutzer hat ein zusätzliches Argument eingegeben und Sie haben es nicht bemerkt.
Beispielsweise ist das Kommandoformat "sum A B", aber der Nutzer tippt "sum 10 20 30". Wenn Sie nur die ersten drei Tokens lesen, funktioniert das Kommando „irgendwie“, aber es entspricht nicht dem gewünschten Vertrag.
Gängiger Trick: nach den erwarteten Tokens versuchen, noch etwas zu lesen.
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string line = "sum 10 20 30";
std::stringstream ss(line);
std::string cmd;
int a{}, b{};
if (!(ss >> cmd >> a >> b)) {
std::cout << "bad format\n";
return 0;
}
std::string extra;
if (ss >> extra) {
std::cout << "too many arguments: " << extra << '\n'; // too many arguments: 30
return 0;
}
std::cout << a + b << '\n';
}
Das macht Ihren Parser unerwartet „erwachsener“, weil er aufhört, Müll stillschweigend zu akzeptieren.
Warum nach einem Fehler „nichts mehr gelesen wird“
Wenn ein Leseversuch scheitert, geht der Stream normalerweise in den Zustand fail. Das heißt: „ich lese nicht weiter, bis man mich wiederherstellt“.
Sehen wir dieses Verhalten live:
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::stringstream ss("10 xx 30");
int a{}, b{}, c{};
ss >> a; // ok: a=10
ss >> b; // fail: "xx" is not an int
ss >> c; // no longer reads
std::cout << "a=" << a << '\n'; // a=10
std::cout << "b=" << b << '\n'; // b=0 (remains default)
std::cout << "c=" << c << '\n'; // c=0
}
Wichtige Schlussfolgerung: wenn Sie die Prüfung ignorieren, können Variablen alte oder Default‑Werte behalten, und Sie führen dann Rechnungen mit nicht eingelesenen Werten aus.
5. Wiederverwendung und Aufbau von Strings
Wiederverwendung von std::stringstream: str(...) und clear()
Manchmal möchte man einen einzelnen std::stringstream für verschiedene Strings verwenden. Das ist in Ordnung, aber es gibt eine Falle: wenn der Stream einmal in fail gegangen ist, bringt das Austauschen des Buffers ihn nicht automatisch zurück in den Normalzustand.
Man muss zwei Operationen durchführen:
- ss.str("neue zeile") — den internen Puffer ersetzen,
- ss.clear() — die Statusflags zurücksetzen (z. B. fail).
Schauen wir das richtige Muster an:
#include <iostream>
#include <ssstream>
int main() {
std::stringstream ss;
ss.str("10 20");
int a{}, b{};
ss >> a >> b;
std::cout << a << "+" << b << '\n'; // 10+20
ss.str("30 40");
ss.clear(); // wichtig!
ss >> a >> b;
std::cout << a << "+" << b << '\n'; // 30+40
}
Wenn man clear() weglässt, dann bekommt man in manchen Szenarien (vor allem nach einem fehlgeschlagenen Lesen) plötzlich einen „stummen“ Stream. Das ist einer jener Bugs, die mystisch erscheinen, bis man den Stream‑Zustand kennt.
Zur Veranschaulichung — ein kleines Lebensdiagramm des Streams:
flowchart TD
A[Stream erstellt] --> B[Versuch zu lesen via >>]
B -->|Erfolg| C[guter Zustand]
B -->|Formatfehler| D[fail Zustand]
D --> E[Weiteres Lesen funktioniert nicht]
E --> F["ss.clear()"]
F --> G[Weiteres Lesen möglich]
std::stringstream als String-Assembler: << und .str()
Bisher haben wir aus einem String gelesen. Aber stringstream kann auch einen String zusammenbauen (wie cout Ausgabe zusammenstellt). Das ist hilfreich, wenn ein String aus vielen Teilen besteht und man nicht manuell mit + kleben und auf Leerzeichen achten möchte.
Beispiel: eine Log‑Nachricht oder einfach ein schöner String bauen.
#include <iostream>
#include <sstream>
#include <string>
int main() {
int id = 7;
int qty = 3;
std::stringstream ss;
ss << "added item id=" << id << " qty=" << qty;
std::string msg = ss.str();
std::cout << msg << '\n'; // added item id=7 qty=3
}
Warum das manchmal bequemer ist als std::string mit vielen +? Weil << Zahlen „frisst“, ohne dass Sie manuell to_string aufrufen müssen, und das Ergebnis fast wie normale Ausgabe lesbar ist.
Merke: was wir wirklich verwenden
Damit die Vorlesung nicht in „merkt euch 200 Methoden“ ausartet, behalten wir nur, was in Aufgaben wirklich nützlich ist:
| Was man tun will | Was man schreibt | Bedeutung |
|---|---|---|
| Stream aus String erstellen | |
Wir lesen Tokens aus line |
| Token/Zahl lesen | |
Liess, überspringt Leerzeichen |
| Prüfen, dass es gelesen wurde | |
Schutz gegen schlechtes Format |
| Ermitteln, ob es „zu viel“ gibt | |
Format „zu lang“ |
| Zusammengebauten String erhalten | |
Gibt den Puffer als std::string zurück |
| Puffer ersetzen | |
Legt neuen String hinein |
| Status zurücksetzen | |
Nach fail kann wieder gelesen werden |
6. Mini‑CLI „Einkaufsliste“
Jetzt machen wir einen kleinen Praxis‑Schritt und beginnen, die „gemeinsame Kurs‑Anwendung“ zusammenzubauen. Nehmen wir eine Konsolenanwendung „Einkaufsliste“, die Kommandos als Zeilen entgegennimmt (via getline) und sie mit stringstream parst.
Wir verwenden vorerst kein struct (das kommt später), daher bauen wir ein einfaches Modell: zwei vectors gleicher Länge — names[i] und counts[i]. Ja, das sieht ein bisschen so aus, als trüge man zwei Tüten und tue so, als wären es eine. Für den aktuellen Stand ist das aber in Ordnung.
Gerüst: Zeilen lesen und mit "exit" beenden
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names;
std::vector<int> counts;
std::string line;
while (std::getline(std::cin, line)) {
if (line == "exit") break;
std::cout << "you typed: " << line << '\n'; // debug
}
}
Das ist vorerst ein Echo‑Programm. Jetzt fügen wir das Parsing des Kommandos hinzu.
Parsen von add <name> <count> mit stringstream
Hier werden wir erstmals spüren, dass Kommando‑Parsing beinahe mechanisch wird:
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names;
std::vector<int> counts;
std::string line;
while (std::getline(std::cin, line)) {
if (line == "exit") break;
std::stringstream ss(line);
std::string cmd;
ss >> cmd;
if (cmd == "add") {
std::string name;
int count{};
std::string extra;
if ((ss >> name >> count) && !(ss >> extra)) {
names.push_back(name);
counts.push_back(count);
std::cout << "added\n"; // added
} else {
std::cout << "format: add <name> <count>\n";
}
}
}
}
Beachten Sie die Prüf‑Logik: wir verlangen genau zwei Argumente und vergewissern uns separat, dass es kein drittes gibt.
Ja, das Kommando "add milk 3" funktioniert. Das Kommando "add milk" funktioniert nicht. Das Kommando "add milk 3 now" funktioniert auch nicht. Und das ist gut so.
Kommando list: aktuelle Einkäufe ausgeben
Fügen wir das angenehmste Kommando hinzu — zeigen, was wir bisher gesammelt haben.
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names;
std::vector<int> counts;
std::string line;
while (std::getline(std::cin, line)) {
if (line == "exit") break;
std::stringstream ss(line);
std::string cmd;
ss >> cmd;
if (cmd == "list") {
for (std::size_t i = 0; i < names.size(); ++i) {
std::cout << i << ": " << names[i] << " x" << counts[i] << '\n';
}
}
// add is here as well (omitted to keep example short)
}
}
Und hier sieht man den großen Vorteil des String‑Streams: wir trennen „ganze Zeile lesen“ von „Zeile in Tokens zerlegen“. So sind viele Konsolen‑Utilities aufgebaut (und ja, eines Tages schreiben Sie Ihre eigene Mini‑Version von git und werden stolz darauf sein).
Warum stringstream nicht immer passt
Wenn das Format whitespace‑basiert ist, gewinnt stringstream meist gegenüber manuellem find/substr, weil >> bereits überflüssige Leerzeichen überspringt. Also die Zeile "add milk 3" funktioniert ohne zusätzlichen Aufwand.
Wenn das Format jedoch Kommas verwendet ("a,b,c") oder Anführungszeichen enthält ("\"milk chocolate\""), reicht stringstream allein nicht aus — dort sind andere Tricks nötig. Aber das ist Lernlogik: zuerst whitespace‑Formate, dann explizite Trennzeichen via getline(..., delim), später Zitate via std::quoted.
7. Typische Fehler bei der Arbeit mit std::stringstream
Fehler Nr. 1: ohne Prüfung lesen und Variablen sofort verwenden.
Es ist verlockend, einfach ss >> cmd >> a >> b; zu schreiben und das Ergebnis zu verwenden, weil „der String ja normal ist“. Sobald das Format aber bricht, setzt der Stream fail, Lesen findet nicht statt und die Variablen behalten alte/Default‑Werte. Richtige Gewohnheit: entweder if (ss >> ...) verwenden oder frühzeitig aus der Kommando‑Verarbeitung aussteigen.
Fehler Nr. 2: „überzählige Argumente“ nicht abfangen.
Anfänger prüfen oft nur, dass die ersten benötigten Felder gelesen wurden, aber nicht, ob die Zeile danach endet. Infolgedessen funktionieren Kommandos mit Müll am Ende „zufällig“ weiter und Nutzer gewöhnen sich an seltsames Verhalten. Zuverlässiger Trick: nach dem Lesen der erwarteten Argumente versuchen, extra zu lesen und bei Erfolg das Format als falsch werten.
Fehler Nr. 3: denselben stringstream wiederverwenden und clear() vergessen.
Wenn der Stream einmal beim Lesen scheiterte (z. B. versucht wurde, ein int aus "abc" zu lesen), bleibt er im Fehlerzustand. Selbst wenn Sie den Buffer mit str("...") ersetzen, „erinnert“ sich der Stream an den fail. Daher gilt bei Wiederverwendung: zuerst ss.str(...), dann ss.clear(), und erst dann lesen.
Fehler Nr. 4: erwarten, dass >> eine ganze „Phrase“ liest.
>> liest ein Token bis zum nächsten Leerzeichen. Wenn Sie einen Parameter mit Leerzeichen wollen (z. B. Produktname "milk chocolate"), hilft stringstream ohne Zusatz nicht. Das ist kein Bug, sondern ein Vertrag: entweder das Format ändern (z. B. mit Anführungszeichen), den Rest der Zeile separat lesen oder std::quoted verwenden (das Thema der nächsten Lektion).
Fehler Nr. 5: „Leerzeichen in Daten“ und „Leerzeichen als Trenner“ verwechseln.
Beim Design des Kommandoformats sollte man vorher klar entscheiden: ist ein Leerzeichen Teil des Werts oder Trenner der Tokens? In whitespace‑Formaten ist das Leerzeichen meist Trenner — das muss akzeptiert werden. Wenn Werte Leerzeichen enthalten können, muss das Format das explizit kennzeichnen (z. B. durch Anführungszeichen oder andere Delimiter), sonst hat der Parser recht und der Nutzer ist unzufrieden.
GO TO FULL VERSION