Warum überspringt std :: getline () die Eingabe nach einer formatierten Extraktion?

105

Ich habe den folgenden Code, der den Benutzer zur Eingabe seines Namens und Status auffordert:

#include <iostream>
#include <string>

int main()
{
    std::string name;
    std::string state;

    if (std::cin >> name && std::getline(std::cin, state))
    {
        std::cout << "Your name is " << name << " and you live in " << state;
    }
}

Was ich finde ist, dass der Name erfolgreich extrahiert wurde, aber nicht der Status. Hier ist die Eingabe und die resultierende Ausgabe:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

Warum wurde der Name des Staates in der Ausgabe weggelassen? Ich habe die richtige Eingabe gegeben, aber der Code ignoriert sie irgendwie. Warum passiert das?

0x499602D2
quelle
Ich glaube std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)sollte auch wie erwartet funktionieren. (Zusätzlich zu den Antworten unten).
JWW

Antworten:

122

Warum passiert das?

Dies hat wenig mit der Eingabe zu tun, die Sie selbst bereitgestellt haben, sondern mit dem Standardverhalten std::getline(). Bei der Eingabe für den Namen ( std::cin >> name) haben Sie nicht nur die folgenden Zeichen übermittelt, sondern auch eine implizite neue Zeile an den Stream angehängt:

"John\n"

Eine neue Zeile wird immer an Ihre Eingabe angehängt, wenn Sie auswählen Enteroder Returnvon einem Terminal aus senden . Es wird auch in Dateien verwendet, um zur nächsten Zeile zu gelangen. Die neue Zeile bleibt nach dem Extrahieren im Puffer namebis zur nächsten E / A-Operation, wo sie entweder verworfen oder verbraucht wird. Wenn der Kontrollfluss erreicht ist std::getline(), wird die neue Zeile verworfen, aber die Eingabe wird sofort beendet. Der Grund dafür ist, dass die Standardfunktionalität dieser Funktion dies vorschreibt (sie versucht, eine Zeile zu lesen und stoppt, wenn sie eine neue Zeile findet).

Da dieser führende Zeilenumbruch die erwartete Funktionalität Ihres Programms beeinträchtigt, muss er übersprungen und irgendwie ignoriert werden. Eine Möglichkeit besteht darin, std::cin.ignore()nach der ersten Extraktion aufzurufen . Das nächste verfügbare Zeichen wird verworfen, sodass die neue Zeile nicht mehr im Weg ist.

std::getline(std::cin.ignore(), state)

Ausführliche Erklärung:

Dies ist die Überlastung dessen std::getline(), was Sie angerufen haben:

template<class charT>
std::basic_istream<charT>& getline( std::basic_istream<charT>& input,
                                    std::basic_string<charT>& str )

Eine weitere Überladung dieser Funktion erfordert ein Trennzeichen vom Typ charT. Ein Trennzeichen ist ein Zeichen, das die Grenze zwischen Eingabesequenzen darstellt. Diese bestimmte Überladung setzt das Trennzeichen input.widen('\n')standardmäßig auf das Zeilenumbruchzeichen, da keines angegeben wurde.

Dies sind einige der Bedingungen, unter denen std::getline()die Eingabe beendet wird:

  • Wenn der Stream die maximale Anzahl von Zeichen extrahiert hat, die a enthalten std::basic_string<charT>kann
  • Wenn das Zeichen für das Dateiende (EOF) gefunden wurde
  • Wenn das Trennzeichen gefunden wurde

Die dritte Bedingung ist die, mit der wir es zu tun haben. Ihre Eingabe in statewird folgendermaßen dargestellt:

"John\nNew Hampshire"
     ^
     |
 next_pointer

Wo next_pointerist das nächste zu analysierende Zeichen? Da das an der nächsten Position in der Eingabesequenz gespeicherte Zeichen das Trennzeichen ist, std::getline()wird dieses Zeichen stillschweigend verworfen, next_pointerzum nächsten verfügbaren Zeichen erhöht und die Eingabe gestoppt. Dies bedeutet, dass die restlichen Zeichen, die Sie angegeben haben, für die nächste E / A-Operation noch im Puffer verbleiben. Sie werden feststellen, dass stateIhre Extraktion beim letzten Aufrufen std::getline()des Trennzeichens das richtige Ergebnis liefert , wenn Sie einen weiteren Lesevorgang von der Zeile in ausführen .


Möglicherweise haben Sie bemerkt, dass dieses Problem beim Extrahieren mit dem formatierten Eingabeoperator ( operator>>()) normalerweise nicht auftritt . Dies liegt daran, dass Eingabestreams Leerzeichen als Trennzeichen für die Eingabe verwenden und standardmäßig der Manipulator std::skipws1 aktiviert ist. Streams verwerfen das führende Leerzeichen aus dem Stream, wenn formatierte Eingaben ausgeführt werden. 2

Im Gegensatz zu den formatierten Eingabeoperatoren std::getline()handelt es sich um eine unformatierte Eingabefunktion. Und alle unformatierten Eingabefunktionen haben den folgenden Code etwas gemeinsam:

typename std::basic_istream<charT>::sentry ok(istream_object, true);

Das Obige ist ein Sentry-Objekt, das in allen formatierten / unformatierten E / A-Funktionen in einer Standard-C ++ - Implementierung instanziiert wird. Sentry-Objekte werden verwendet, um den Stream für E / A vorzubereiten und festzustellen, ob er sich in einem Fehlerzustand befindet oder nicht. Sie werden nur feststellen, dass in den unformatierten Eingabefunktionen das zweite Argument für den Sentry-Konstruktor lautet true. Dieses Argument bedeutet, dass führende Leerzeichen nicht vom Beginn der Eingabesequenz an verworfen werden. Hier ist das relevante Zitat aus dem Standard [§27.7.2.1.3 / 2]:

 explicit sentry(basic_istream<charT, traits>& is, bool noskipws = false);

[...] Wenn noskipwsNull ist und is.flags() & ios_base::skipwsungleich Null ist, extrahiert und verwirft die Funktion jedes Zeichen, solange das nächste verfügbare Eingabezeichen cein Leerzeichen ist. [...]

Da die obige Bedingung falsch ist, verwirft das Wachobjekt das Leerzeichen nicht. Der Grund noskipws, auf trueden diese Funktion eingestellt ist, std::getline()liegt darin, dass rohe, unformatierte Zeichen in ein std::basic_string<charT>Objekt eingelesen werden sollen .


Die Lösung:

Es gibt keine Möglichkeit, dieses Verhalten von zu stoppen std::getline(). Was Sie tun müssen, ist, die neue Zeile vor dem Ausführen selbst zu verwerfen std::getline()(aber nach der formatierten Extraktion). Dies kann erreicht werden, indem ignore()der Rest der Eingabe verworfen wird, bis eine neue Zeile erreicht ist:

if (std::cin >> name &&
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n') &&
    std::getline(std::cin, state))
{ ... }

Sie müssen einschließen <limits>, um zu verwenden std::numeric_limits. std::basic_istream<...>::ignore()ist eine Funktion, die eine bestimmte Anzahl von Zeichen verwirft, bis sie entweder ein Trennzeichen findet oder das Ende des Streams erreicht ( ignore()verwirft auch das Trennzeichen, wenn es es findet). Die max()Funktion gibt die größte Anzahl von Zeichen zurück, die ein Stream akzeptieren kann.

Eine andere Möglichkeit, das Leerzeichen zu verwerfen, besteht darin, die std::wsFunktion zu verwenden, die ein Manipulator ist, mit dem führende Leerzeichen vom Anfang eines Eingabestreams extrahiert und verworfen werden können:

if (std::cin >> name && std::getline(std::cin >> std::ws, state))
{ ... }

Was ist der Unterschied?

Der Unterschied besteht darin, dass ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 Zeichen wahllos verwirft, bis sie entweder countZeichen verwirft , das Trennzeichen (angegeben durch das zweite Argument delim) findet oder das Ende des Streams erreicht. std::wswird nur zum Verwerfen von Leerzeichen vom Anfang des Streams verwendet.

Wenn Sie formatierte Eingaben mit unformatierten Eingaben mischen und verbleibende Leerzeichen verwerfen müssen, verwenden Sie std::ws. Andernfalls verwenden Sie, wenn Sie ungültige Eingaben löschen müssen, unabhängig davon, um was es sich handelt ignore(). In unserem Beispiel müssen wir nur Leerzeichen löschen, da der Stream Ihre Eingabe "John"für die nameVariable verbraucht hat . Alles, was übrig blieb, war das Zeilenumbruchzeichen.


1: std::skipwsist ein Manipulator, der den Eingabestream anweist, führende Leerzeichen zu verwerfen, wenn formatierte Eingaben ausgeführt werden. Dies kann mit dem std::noskipwsManipulator ausgeschaltet werden.

2: Eingabestreams betrachten bestimmte Zeichen standardmäßig als Leerzeichen, z. B. Leerzeichen, Zeilenumbruchzeichen, Formularvorschub, Wagenrücklauf usw.

3: Dies ist die Unterschrift von std::basic_istream<...>::ignore(). Sie können es mit null Argumenten aufrufen, um ein einzelnes Zeichen aus dem Stream zu verwerfen, einem Argument, um eine bestimmte Anzahl von Zeichen zu verwerfen, oder zwei Argumenten, um countZeichen zu verwerfen , oder bis es erreicht ist delim, je nachdem, welches zuerst eintritt. Normalerweise verwenden Sie std::numeric_limits<std::streamsize>::max()als Wert, countwenn Sie nicht wissen, wie viele Zeichen sich vor dem Trennzeichen befinden, diese aber trotzdem verwerfen möchten.

0x499602D2
quelle
1
Warum nicht einfach if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
@FredLarson Guter Punkt. Obwohl es nicht funktionieren würde, wenn die erste Extraktion eine Ganzzahl oder etwas ist, das keine Zeichenfolge ist.
0x499602D2
Natürlich ist das hier nicht der Fall und es macht keinen Sinn, dasselbe auf zwei verschiedene Arten zu tun. Für eine Ganzzahl könnte man die Zeile in eine Zeichenfolge umwandeln und dann verwenden std::stoi(), aber dann ist es nicht so klar, dass es einen Vorteil gibt. Aber ich bevorzuge es, nur std::getline()für zeilenorientierte Eingaben zu verwenden und dann die Zeile auf eine sinnvolle Weise zu analysieren. Ich denke, es ist weniger fehleranfällig.
Fred Larson
@FredLarson Einverstanden. Vielleicht füge ich das hinzu, wenn ich Zeit habe.
0x499602D2
1
@Albin Der Grund, den Sie möglicherweise verwenden möchten, std::getline()ist, wenn Sie alle Zeichen bis zu einem bestimmten Trennzeichen erfassen und in eine Zeichenfolge eingeben möchten. Dies ist standardmäßig die neue Zeile. Wenn diese XAnzahl von Zeichenfolgen nur einzelne Wörter / Token sind, kann diese Aufgabe leicht ausgeführt werden >>. Andernfalls würden Sie die erste Nummer in eine Ganzzahl mit >>eingeben, cin.ignore()in der nächsten Zeile aufrufen und dann eine Schleife ausführen, die Sie verwenden getline().
0x499602D2
11

Alles ist in Ordnung, wenn Sie Ihren ursprünglichen Code folgendermaßen ändern:

if ((cin >> name).get() && std::getline(cin, state))
Boris
quelle
3
Danke dir. Dies funktioniert auch, weil get()das nächste Zeichen verbraucht wird. Es gibt auch das, (std::cin >> name).ignore()was ich früher in meiner Antwort vorgeschlagen habe.
0x499602D2
"..work weil get () ..." Ja genau. Entschuldigen Sie die Antwort ohne Details.
Boris
4
Warum nicht einfach if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
0

Dies liegt daran, dass ein impliziter Zeilenvorschub, der auch als Zeilenumbruchzeichen bezeichnet \nwird, an alle Benutzereingaben eines Terminals angehängt wird, da der Stream angewiesen wird, eine neue Zeile zu beginnen. Sie können dies sicher berücksichtigen, indem Sie std::getlinebei der Suche nach mehreren Zeilen von Benutzereingaben verwenden. Das Standardverhalten von std::getlineliest alles bis einschließlich des Zeilenumbruchzeichens \naus dem Eingabestreamobjekt, das std::cinin diesem Fall vorhanden ist.

#include <iostream>
#include <string>

int main()
{
    std::string name;
    std::string state;

    if (std::getline(std::cin, name) && std::getline(std::cin, state))
    {
        std::cout << "Your name is " << name << " and you live in " << state;
    }
    return 0;
}
Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in New Hampshire"
Justin Randall
quelle