it-swarm.com.de

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

Ich habe den folgenden Code, der den Benutzer nach Namen und Status fragt:

#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 Zustand. 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 gemacht, aber der Code ignoriert sie irgendwie. Warum passiert das?

81
0x499602D2

Warum passiert das?

Dies hat wenig mit den Eingaben zu tun, die Sie selbst gemacht haben, sondern mit dem Standardverhalten, das std::getline() aufweist. Bei der Eingabe des Namens (std::cin >> name) haben Sie nicht nur die folgenden Zeichen übergeben, sondern auch eine implizite neue Zeile an den Stream angehängt:

"John\n"

Bei Auswahl von wird immer eine neue Zeile an Ihre Eingabe angehängt Enter oder Return beim Einreichen von einem Terminal. Es wird auch in Dateien verwendet, um zur nächsten Zeile zu gelangen. Die neue Zeile bleibt nach dem Extrahieren in name bis zur nächsten E/A-Operation im Puffer, wo sie entweder verworfen oder verbraucht wird. Wenn der Kontrollfluss std::getline() erreicht, 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 einschränkt, folgt daraus, dass es übersprungen werden muss, was wir irgendwie ignorieren. Eine Möglichkeit besteht darin, nach der ersten Extraktion std::cin.ignore() aufzurufen. Das nächste verfügbare Zeichen wird verworfen, so dass der Zeilenumbruch nicht mehr aufdringlich ist.


Ausführliche Erklärung:

Dies ist die Überladung von std::getline(), die Sie aufgerufen haben:

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

Eine weitere Überladung dieser Funktion nimmt einen Begrenzer vom Typ charT an. Ein Trennzeichen ist ein Zeichen, das die Grenze zwischen Eingabesequenzen darstellt. Diese besondere Überladung setzt den Begrenzer standardmäßig auf das Zeilenumbruchszeichen input.widen('\n'), da eines nicht angegeben wurde.

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

  • Wenn der Stream die maximale Anzahl von Zeichen extrahiert hat, kann ein std::basic_string<charT> enthalten
  • Wenn das EOF-Zeichen (End-of-File) gefunden wurde
  • Wenn das Trennzeichen gefunden wurde

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

"John\nNew Hampshire"
     ^
     |
 next_pointer

dabei ist next_pointer das nächste zu analysierende Zeichen. Da das an der nächsten Stelle in der Eingabesequenz gespeicherte Zeichen das Trennzeichen ist, verwirft std::getline() dieses Zeichen stillschweigend, erhöht next_pointer auf das nächste verfügbare Zeichen und stoppt die Eingabe. Dies bedeutet, dass der Rest der Zeichen, die Sie eingegeben haben, für die nächste E/A-Operation noch im Puffer verbleibt. Sie werden feststellen, dass Ihre Extraktion bei einem erneuten Lesevorgang von der Zeile in state das richtige Ergebnis liefert, da der letzte Aufruf von std::getline() das Trennzeichen verworfen hat.


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 Begrenzer für die Eingabe verwenden und den std::skipws haben.1 Standardmäßig ist der Manipulator aktiviert. Streams verwerfen das führende Leerzeichen aus dem Stream, wenn Sie mit der formatierten Eingabe beginnen.2

Im Gegensatz zu den formatierten Eingabeoperatoren ist std::getline() 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 Wachobjekt, 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 true ist. Dieses Argument bedeutet, dass führende Leerzeichen nicht am Anfang der Eingabesequenz 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 noskipws Null ist und is.flags() & ios_base::skipws ungleich Null ist, extrahiert und löscht die Funktion jedes Zeichen, solange das nächste verfügbare Eingabezeichen c ein Leerzeichen ist. [...]

Da die obige Bedingung falsch ist, verwirft das Wachobjekt das Leerzeichen nicht. Der Grund, warum noskipws von dieser Funktion auf true gesetzt wird, liegt darin, dass std::getline() unformatierte Zeichen in ein std::basic_string<charT>-Objekt einliest.


Die Lösung:

Es gibt keine Möglichkeit, dieses Verhalten von std::getline() zu stoppen. Was Sie tun müssen, ist, die neue Zeile selbst zu verwerfen, bevor std::getline() ausgeführt wird (tun Sie dies jedoch nach der formatierten Extraktion). Verwenden Sie dazu ignore(), um den Rest der Eingabe zu verwerfen, bis Sie eine neue Zeile erhalten:

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

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

Eine andere Möglichkeit, den Whitespace zu verwerfen, besteht darin, die Funktion std::ws zu verwenden, einen Manipulator, mit dem führende Whitespaces am Anfang eines Eingabestreams extrahiert und verworfen werden:

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

Was ist der Unterschied?

Der Unterschied ist, dass ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 Verwirft wahllos Zeichen, bis entweder count Zeichen verworfen, der Begrenzer (angegeben durch das zweite Argument delim) gefunden oder das Ende des Streams erreicht wird. std::ws wird nur zum Löschen von Leerzeichen am 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 ignore(), wenn Sie ungültige Eingaben löschen müssen, unabhängig davon, um welche es sich handelt. In unserem Beispiel müssen wir nur Leerzeichen löschen, da der Stream die Eingabe von "John" für die Variable name verbraucht hat. Alles was übrig blieb, war das Newline-Zeichen.


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

2: In Eingabestreams werden bestimmte Zeichen standardmäßig als Leerzeichen betrachtet, z. B. Leerzeichen, Zeilenvorschub, Zeilenvorschub, Wagenrücklauf usw.

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

103
0x499602D2

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

if ((cin >> name).get() && std::getline(cin, state))
10
Boris

Dies geschieht, weil ein impliziter Zeilenvorschub, der auch als Zeilenvorschubzeichen \n bezeichnet wird, an alle Benutzereingaben eines Terminals angehängt wird, während er dem Stream mitteilt, eine neue Zeile zu beginnen. Sie können dies sicher berücksichtigen, indem Sie std::getline verwenden, wenn Sie nach mehreren Zeilen mit Benutzereingaben suchen. Das Standardverhalten von std::getline liest alles bis einschließlich des Zeilenvorschubzeichens \n aus dem Eingabestromobjekt, das in diesem Fall std::cin 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"
0
Justin Randall