CodeGym /Kurse /C++ SELF /Zuverlässiges Einlesen von Zeilen

Zuverlässiges Einlesen von Zeilen

C++ SELF
Level 9, Lektion 4
Verfügbar

1. Der unerwartet schwierige Teil des Programms

Wenn man gerade mit dem Programmieren anfängt, erscheint es logisch: „Der Benutzer hat eine Zeile eingegeben — also lese ich sie ein“. Und dann passiert plötzlich, dass getline eine leere Zeile zurückgibt, der Befehl " add milk 2 " nicht erkannt wird, oder die Eingabe nach einer Zahl aus irgendeinem Grund übersprungen wird. Das ist keine Magie und kein Fluch des Compilers — das sind einfach Besonderheiten davon, wie der Eingabestrom funktioniert.

Stellen Sie sich vor, die Eingabe ist ein Förderband (Buffer). Der Benutzer „tippt“ Zeichen, und std::cin nimmt sie weg. Der Operator operator>> nimmt ein Token (ein Stück bis zum Leerzeichen), während std::getline die gesamte Zeile bis zum '\n' nimmt. Und diese beiden Ansätze existieren nebeneinander, essen aber Zeichen unterschiedlich.

Lasst uns eine Mini-Tabelle machen, um den Unterschied zu fixieren:

Was wir lesen Wie wir lesen Wo wir stoppen Beispiel, was wir aus Hello world bekommen
Token
std::cin >> s
am Leerzeichen/Zeilenumbruch
Hello
Zeile
std::getline(std::cin, s)
bei
'\n'
Hello world

Die wichtigste Schlussfolgerung für heute: Wenn Sie eine Konsolenanwendung mit Befehlen bauen, ist es fast immer vorteilhaft, die ganze Zeile einzulesen und sie anschließend zu parsen. Das ist wie einen Brief komplett zu nehmen und dann zu entscheiden, wo Adresse, Unterschrift und „ich habe vergessen, die Datei anzuhängen“ sind (klassisch in der Firmenwelt).

2. std::getline: Ganze Zeile lesen und sich nicht vor leeren Zeilen fürchten

std::getline ist Ihr Hauptwerkzeug für ehrliches Einlesen von Benutzereingaben, bei denen Leerzeichen Teil des Texts sein können. Aber er hat Eigenheiten: Er gibt eine leere Zeichenkette zurück, wenn der Benutzer einfach Enter gedrückt hat. Und das ist normal. Leere Eingabe ist auch Eingabe — nur eben ohne Text. Wie eine Nachricht „ok“ ohne „ok“.

Minimalbeispiel zum Einlesen einer Zeile:

#include <iostream>
#include <string>

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

    std::cout << "line=[" << line << "]\n"; // zum Beispiel: line=[hello world]
}

Jetzt fügen wir eine richtige Reaktion auf eine leere Zeile hinzu. In echten Programmen bedeutet eine leere Zeile häufig „nichts tun, nochmal fragen“:

#include <iostream>
#include <string>

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

    if (line.empty()) {
        std::cout << "empty input\n"; // empty input
    } else {
        std::cout << "len=" << line.size() << "\n"; // len=11 (zum Beispiel)
    }
}

Wichtige Gewohnheit: Bevor Sie auf line[0] zugreifen, prüfen Sie immer line.empty(). Der Index 0 in einer leeren Zeile ist wie der Versuch, eine Tür zu öffnen, die es nicht gibt: Die Geste mag richtig erscheinen, aber das Ergebnis ist unangenehm.

3. Die Falle >> + getline: Warum ich eine leere Zeile bekam

Jetzt kommt der klassische Trick, der Generationen von Studierenden (und ein paar Dozenten ebenfalls) den Kopf verdreht hat. Wenn Sie eine Zahl mit std::cin >> x einlesen, stoppt die Eingabe vor dem Leerzeichen oder Zeilenumbruch, aber das '\n' selbst bleibt oft im Buffer. Das folgende getline liest dann sofort den „Rest der Zeile“ — also die Leere bis zum '\n'.

Schauen wir uns das Problem an:

#include <iostream>
#include <string>

int main() {
    int n = 0;
    std::cin >> n;

    std::string line;
    std::getline(std::cin, line);

    std::cout << "n=" << n << "\n";
    std::cout << "line=[" << line << "]\n"; // oft: line=[]
}

Lösung: std::ws — Überspringe Leerzeichen vor getline

Der kompakteste und beliebteste Trick ist, so zu lesen:

std::getline(std::cin >> std::ws, line);

Der Manipulator std::ws sagt: „überspringe zuerst führende Whitespace-Zeichen (inklusive '\n'), dann lese die Zeile“.

Beispiel:

#include <iostream>
#include <string>

int main() {
    int age = 0;
    std::cin >> age;

    std::string comment;
    std::getline(std::cin >> std::ws, comment);

    std::cout << "age=" << age << ", comment=[" << comment << "]\n";
    // age=20, comment=[hello]
}

Hier ist die Logik „stabil“: Nach der Zahl lesen wir tatsächlich die nächste sinnvolle Zeile.

Lösung: ignore(...) — Wirf alles bis zum Zeilenende

Manchmal benötigt man ein expliziteres Verhalten: Nachdem man ein Token/eine Zahl gelesen hat, möchte man den gesamten Rest der aktuellen Zeile verwerfen. Dafür gibt es ignore.

Das gebräuchlichste Muster sieht so aus (ja, es ist etwas länger, aber dafür sehr ehrlich):

#include <iostream>
#include <string>
#include <limits>

int main() {
    int n = 0;
    std::cin >> n;

    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    std::string line;
    std::getline(std::cin, line);

    std::cout << "line=[" << line << "]\n";
}

Der Sinn: „ignorier Zeichen, bis du '\n' triffst (oder das Limit erreicht ist)“.

Kleiner Fun-Fact: Selbst in der Welt der C++-Standardisierung wurde diskutiert, wie man std::istream::ignore weniger „überraschend“ machen kann. Sie sind also nicht allein, wenn er Ihnen etwas seltsam vorkommt.

Wie wählen: ws oder ignore

Wenn Sie einfach nur „nach >> die nächste Zeile normal lesen“ wollen — ist std::ws in der Regel einfacher und kürzer.

Wenn Sie strikt „das aktuelle Zeilenende aufräumen und mit sauberem Blatt beginnen“ wollen (zum Beispiel: Ihre Eingabe hat das Format Zahl, dann in der nächsten Zeile Text) — wirkt ignore wie ein klarer Vertrag.

Wichtiger ist: Wählen Sie einen Stil und bleiben Sie dabei, damit Sie nicht in einem Projekt fünf verschiedene „magische Rituale“ für dasselbe Problem haben.

4. Normalisierung der Eingabe: leere Zeilen und Leerzeichen

Wenn Sie eine Anwendung schreiben, die auf Befehle wartet, werden Sie schnell merken: Der Benutzer tippt nicht nur „perfekte“ Zeilen. Er kann eine leere Zeile eingeben, zehn Leerzeichen eingeben oder aus Versehen ein Leerzeichen am Ende hinzufügen. Und all das sollte ruhig behandelt werden — ohne Panik, ohne Absturz, ohne „Befehl nicht gefunden“ bei einem offensichtlichen Befehl.

Hier ist es hilfreich, drei ähnliche, aber unterschiedliche Fälle zu unterscheiden:

  • leere Zeile: ""
  • Zeile aus Leerzeichen: " "
  • Zeile mit Text und zusätzlichen Leerzeichen: " add milk 2 "

Für ein Programm mit Befehlen bedeuten die ersten beiden Fälle meistens „nichts tun“. Der dritte Fall heißt „auf Normalform bringen und dann parsen“.

Damit das funktioniert, brauchen wir eine Normalisierung: Wir nehmen die Eingabezeile und bringen sie in eine Form, mit der wir bequem arbeiten können. In dieser Vorlesung machen wir zwei einfache Operationen: Wir entfernen Leerzeichen an den Rändern (trim) und wir reduzieren Folgen von Leerzeichen innerhalb der Zeile auf ein einzelnes Leerzeichen (collapse). Wir betrachten bewusst nur das Leerzeichen-Zeichen ' ', ohne Tabulatoren und exotische Fälle.

Trim: Entfernen der Leerzeichen links und rechts

Erstellen wir die Funktion trim_spaces, die eine neue Zeichenkette zurückgibt (das macht es einfacher zu erklären; Optimierung ist jetzt nicht wichtig):

#include <string>

std::string trim_spaces(const std::string& s) {
    std::size_t begin = 0;
    while (begin < s.size() && s[begin] == ' ') ++begin;

    std::size_t end = s.size();
    while (end > begin && s[end - 1] == ' ') --end;

    return s.substr(begin, end - begin);
}

Achten Sie auf die Sorgfalt bei den Indizes: Rechts prüfen wir s[end - 1], weil end die Position nach dem letzten Zeichen im Bereich ist. Das ist ein typisches Modell in C++: [begin, end).

Mini-Test in main:

#include <iostream>
#include <string>

int main() {
    std::string s = "   hello   ";
    std::cout << "[" << trim_spaces(s) << "]\n"; // [hello]
}

Collapse von Leerzeichen: "a b" → "a b"

Jetzt gehen wir die Zeile durch und fügen ein Leerzeichen nur dann hinzu, wenn das vorherige Zeichen kein Leerzeichen war:

#include <string>

std::string collapse_spaces(const std::string& s) {
    std::string out;
    bool prev_space = false;

    for (std::size_t i = 0; i < s.size(); ++i) {
        char c = s[i];
        if (c == ' ') {
            if (!prev_space) out += ' ';
            prev_space = true;
        } else {
            out += c;
            prev_space = false;
        }
    }
    return out;
}

Und eine kurze Demonstration:

#include <iostream>
#include <string>

int main() {
    std::string s = "a   b    c";
    std::cout << collapse_spaces(s) << "\n"; // a b c
}

Normalisierung in einer Funktion zusammenfassen

Jetzt kombinieren wir: Zuerst trim (Entfernen der Ränder), dann collapse. Die Reihenfolge ist wichtig: Wenn Sie kein trim ausführen, kann am Anfang/Ende ein Leerzeichen bleiben (und das stört manchmal die Erkennung von Befehlen).

#include <string>

std::string normalize_spaces(const std::string& s) {
    std::string trimmed = trim_spaces(s);
    return collapse_spaces(trimmed);
}

5. Wir bauen ein robustes Einlesen von Befehlen für eine Mini-Anwendung

Jetzt bauen wir alles zu einem kleinen, aber vollständigen Baustein zusammen: die Funktion read_command_line(), die eine Zeile liest, Leerzeichen normalisiert und das Ergebnis zurückgibt. Das ist eine Vorlage, die Sie später in fast jedem Konsolenprojekt mit Befehlen verwenden werden: Taschenrechner, Notizen, Minispiel, „intelligente Einkaufsliste“ — was auch immer.

Wir machen eine einfache Version: Wir lesen die Zeile mit getline, und wenn das Lesen fehlschlägt, geben wir eine leere Zeichenkette zurück. Wir gehen hier nicht auf die Zustände von cin ein (das kommt später), aber eine grundlegende Prüfung schadet nicht.

#include <iostream>
#include <string>

std::string read_command_line() {
    std::string line;
    if (!std::getline(std::cin, line)) return ""; // EOF oder Fehler

    return normalize_spaces(line);
}

Jetzt die minimale Schleife im „Command Mode“:

#include <iostream>
#include <string>

int main() {
    while (true) {
        std::cout << "> ";
        std::string cmd = read_command_line();

        if (cmd.empty()) continue;
        if (cmd == "exit") break;

        std::cout << "cmd=[" << cmd << "]\n";
    }
}

Wenn der Benutzer " exit " eingibt, wird dies nach der Normalisierung zu "exit", und der Befehl wird ausgeführt. Das ist genau das, was wir „stabile Eingabe“ nennen: Das Programm ist nicht davon abhängig, wie genau der Benutzer das Leerzeichen getroffen hat.

Mini-Schema: Was mit der Eingabe passiert, bevor geparst wird

Es ist nützlich, die gesamte Pipeline einmal visuell zu sehen:

flowchart TD
    A["getline: wir lesen die Zeile unverändert"] --> B["normalize: trim + collapse"]
    B --> C{"leer?"}
    C -->|ja| D["nichts tun / auf die nächste Zeile warten"]
    C -->|nein| E["weiter: Befehlsparsing (find/substr oder Tokenisierung)"]

Kernaussage: Normalisierung ist Datenvorbereitung, fast wie das Gemüse vor dem Kochen waschen. Man kann auch ohne sie arbeiten, aber wundern Sie sich später nicht über den Sand zwischen den Zähnen.

6. Typische Fehler

Fehler Nr. 1: Man mischt >> und getline, erhält eine „leere Zeile“ und glaubt, getline sei kaputt.
Tatsächlich ist nicht getline kaputt, sondern die Erwartung: Nach >> bleibt oft ein '\n' im Buffer, und getline liest ehrlich bis zum Zeilenende — also eine leere Zeichenkette. Das lässt sich durch Disziplin beheben: Entweder lesen Sie immer Zeilen mit getline, oder Sie führen den Übergang mit std::ws oder mit ignore durch.

Fehler Nr. 2: Man unterscheidet nicht zwischen leerer Zeile und Zeile nur mit Leerzeichen.
getline kann " " zurückgeben — das ist nicht empty(), aber dort ist kein nützlicher Text. Wenn Sie ein Befehlsinterface bauen, sollte eine solche Zeile nach der Normalisierung zu leer werden. Deshalb ist die Prüfung empty() zuverlässig erst nach trim (oder kompletter normalize).

Fehler Nr. 3: Man schreibt line[0] um den ersten Buchstaben zu prüfen, ohne empty() zu prüfen.
Das ist ein klassischer Off-by-one-/Bounds-Fehler: In einer leeren Zeile existiert kein Zeichen mit Index 0. Zuerst empty(), dann Indizierung. Immer.

Fehler Nr. 4: Man macht substr(begin, end) statt substr(begin, end - begin).
Das ist eine der häufigsten Verwechslungen: Der zweite Parameter von substr ist nicht der „Endindex“, sondern die Länge. Wenn Sie dort end übergeben, erhalten Sie sehr wahrscheinlich nicht das Erwartete (und manchmal sogar einen Out-of-range, falls end größer ist als size()).

Fehler Nr. 5: Man reduziert die Leerzeichen, vergisst aber das trim.
Am Ende wird aus " add milk" vielleicht " add milk" — das ist zwar besser, aber das führende Leerzeichen bleibt, und der Vergleich cmd == "add" funktioniert nicht. Normalisierung sollte in Stufen erfolgen: zuerst Ränder, dann Innenraum.

1
Aufgabe
C++ SELF, Level 9, Lektion 4
Gesperrt
Leeres Feedback
Leeres Feedback
1
Aufgabe
C++ SELF, Level 9, Lektion 4
Gesperrt
Nummer und Kommentar
Nummer und Kommentar
1
Aufgabe
C++ SELF, Level 9, Lektion 4
Gesperrt
Leerzeichen an den Rändern entfernen
Leerzeichen an den Rändern entfernen
1
Aufgabe
C++ SELF, Level 9, Lektion 4
Gesperrt
Befehlsbereinigung
Befehlsbereinigung
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION