Perché std :: getline () salta l'input dopo un'estrazione formattata?


105

Ho la seguente parte di codice che richiede all'utente il nome e lo stato:

#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;
    }
}

Quello che ho scoperto è che il nome è stato estratto con successo, ma non lo stato. Ecco l'input e l'output risultante:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

Perché il nome dello stato è stato omesso dall'output? Ho fornito l'input corretto, ma il codice in qualche modo lo ignora. Perché succede questo?


Credo che std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)dovrebbe funzionare anche come previsto. (Oltre alle risposte di seguito).
jww

Risposte:


122

Perché succede questo?

Questo ha poco a che fare con l'input che hai fornito tu stesso, ma piuttosto con il comportamento predefinito std::getline()esibito. Quando hai fornito il tuo input per il nome ( std::cin >> name), non solo hai inviato i seguenti caratteri, ma anche una nuova riga implicita è stata aggiunta allo stream:

"John\n"

Una nuova riga viene sempre aggiunta al tuo input quando selezioni Entero Returnquando invii da un terminale. Viene anche utilizzato nei file per spostarsi verso la riga successiva. La nuova riga viene lasciata nel buffer dopo l'estrazione namefino alla successiva operazione di I / O in cui viene eliminata o consumata. Quando il flusso di controllo raggiunge std::getline(), la nuova riga verrà scartata, ma l'input cesserà immediatamente. Il motivo per cui ciò accade è perché la funzionalità predefinita di questa funzione impone che dovrebbe (tenta di leggere una riga e si ferma quando trova una nuova riga).

Poiché questa nuova riga iniziale inibisce la funzionalità prevista del programma, ne consegue che deve essere saltata o ignorata in qualche modo. Un'opzione è chiamare std::cin.ignore()dopo la prima estrazione. Scarterà il prossimo personaggio disponibile in modo che la nuova riga non sia più d'intralcio.

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

Spiegazione approfondita:

Questo è il sovraccarico di std::getline()quello che hai chiamato:

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

Un altro sovraccarico di questa funzione accetta un delimitatore di tipo charT. Un carattere delimitatore è un carattere che rappresenta il confine tra le sequenze di input. Questo particolare overload imposta il delimitatore sul carattere di nuova riga input.widen('\n')per impostazione predefinita poiché uno non è stato fornito.

Ora, queste sono alcune delle condizioni in base alle quali std::getline()termina l'input:

  • Se il flusso ha estratto il numero massimo di caratteri che un std::basic_string<charT>può contenere
  • Se è stato trovato il carattere di fine file (EOF)
  • Se è stato trovato il delimitatore

La terza condizione è quella con cui abbiamo a che fare. Il tuo input stateè rappresentato così:

"John\nNew Hampshire"
     ^
     |
 next_pointer

dov'è next_pointeril prossimo carattere da analizzare. Poiché il carattere memorizzato nella posizione successiva nella sequenza di input è il delimitatore, std::getline()scarterà silenziosamente quel carattere, passerà next_pointeral successivo carattere disponibile e interromperà l'immissione. Ciò significa che il resto dei caratteri forniti rimangono ancora nel buffer per la successiva operazione di I / O. Noterai che se esegui un'altra lettura dalla riga in state, la tua estrazione produrrà il risultato corretto come l'ultima chiamata a std::getline()scartare il delimitatore.


Potresti aver notato che in genere non incontri questo problema durante l'estrazione con l'operatore di input formattato ( operator>>()). Questo perché i flussi di input usano spazi bianchi come delimitatori per l'input e hanno il manipolatore std::skipws1 impostato per impostazione predefinita. Streams eliminerà lo spazio bianco iniziale dal flusso quando inizierà a eseguire l'input formattato. 2

A differenza degli operatori di input formattati, std::getline()è una funzione di input non formattata . E tutte le funzioni di input non formattate hanno il seguente codice in qualche modo in comune:

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

Quanto sopra è un oggetto sentinella che viene istanziato in tutte le funzioni di I / O formattate / non formattate in un'implementazione C ++ standard. Gli oggetti Sentry vengono utilizzati per preparare il flusso per l'I / O e per determinare se si trova o meno in uno stato di errore. Troverai solo che nelle funzioni di input non formattate , il secondo argomento del costruttore di sentry è true. Questo argomento significa che gli spazi bianchi iniziali non verranno eliminati dall'inizio della sequenza di input. Ecco la citazione pertinente dallo Standard [§27.7.2.1.3 / 2]:

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

[...] Se noskipwsè zero ed è diverso da is.flags() & ios_base::skipwszero, la funzione estrae e scarta ogni carattere fintanto che il successivo carattere di input disponibile cè uno spazio bianco. [...]

Poiché la condizione precedente è falsa, l'oggetto sentinella non eliminerà lo spazio vuoto. Il motivo noskipwsè impostato trueda questa funzione è perché il punto std::getline()è quello di leggere caratteri grezzi e non formattati in un std::basic_string<charT>oggetto.


La soluzione:

Non c'è modo di fermare questo comportamento di std::getline(). Quello che dovrai fare è scartare tu stesso la nuova riga prima std::getline()dell'esecuzione (ma fallo dopo l'estrazione formattata). Questo può essere fatto usando ignore()per scartare il resto dell'input fino a raggiungere una nuova riga fresca:

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

Dovrai includere <limits>per utilizzare std::numeric_limits. std::basic_istream<...>::ignore()è una funzione che scarta una quantità specificata di caratteri finché non trova un delimitatore o raggiunge la fine del flusso ( ignore()scarta anche il delimitatore se lo trova). La max()funzione restituisce il maggior numero di caratteri che un flusso può accettare.

Un altro modo per eliminare gli spazi bianchi è usare la std::wsfunzione che è un manipolatore progettato per estrarre e scartare gli spazi bianchi iniziali dall'inizio di un flusso di input:

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

Qual è la differenza?

La differenza è che ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 scarta indiscriminatamente i caratteri finché non scarta i countcaratteri, trova il delimitatore (specificato dal secondo argomento delim) o raggiunge la fine del flusso. std::wsviene utilizzato solo per eliminare i caratteri di spazio vuoto dall'inizio del flusso.

Se stai mescolando un input formattato con un input non formattato e devi eliminare gli spazi vuoti residui, usa std::ws. Altrimenti, se è necessario cancellare l'input non valido indipendentemente da cosa sia, utilizzare ignore(). Nel nostro esempio, abbiamo solo bisogno di cancellare gli spazi vuoti poiché il flusso ha consumato il tuo input "John"per la namevariabile. Tutto ciò che era rimasto era il carattere di nuova riga.


1: std::skipwsè un manipolatore che indica al flusso di input di eliminare gli spazi bianchi iniziali durante l'esecuzione di input formattato. Questo può essere disattivato con il std::noskipwsmanipolatore.

2: I flussi di input considerano alcuni caratteri come spazi bianchi per impostazione predefinita, come il carattere spazio, il carattere di nuova riga, l'avanzamento modulo, il ritorno a capo, ecc.

3: questa è la firma di std::basic_istream<...>::ignore(). Puoi chiamarlo con zero argomenti per scartare un singolo carattere dal flusso, un argomento per scartare una certa quantità di caratteri o due argomenti per scartare countcaratteri o finché non raggiunge delim, a seconda di quale si verifica per primo. Normalmente usi std::numeric_limits<std::streamsize>::max()come valore di countse non sai quanti caratteri ci sono prima del delimitatore, ma vuoi scartarli comunque.


1
Perché non semplicemente if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson

@FredLarson Buon punto. Anche se non funzionerebbe se la prima estrazione fosse di un numero intero o di qualsiasi cosa che non sia una stringa.
0x499602D2

Ovviamente non è così qui e non ha senso fare la stessa cosa in due modi diversi. Per un numero intero potresti inserire la riga in una stringa e poi usarla std::stoi(), ma poi non è così chiaro che ci sia un vantaggio. Ma tendo a preferire l'uso solo std::getline()per l'input orientato alla riga e quindi mi occupo dell'analisi della linea in qualsiasi modo abbia senso. Penso che sia meno soggetto a errori.
Fred Larson

@FredLarson d'accordo. Forse lo aggiungerò se ho tempo.
0x499602D2

1
@Albin Il motivo che potresti voler usare std::getline()è se vuoi catturare tutti i caratteri fino a un dato delimitatore e inserirli in una stringa, per impostazione predefinita questa è la nuova riga. Se quel Xnumero di stringhe sono solo singole parole / gettoni, questo lavoro può essere facilmente eseguito >>. Altrimenti inseriresti il ​​primo numero in un numero intero con >>, chiamerai cin.ignore()sulla riga successiva e quindi eseguirai un ciclo in cui usi getline().
0x499602D2

11

Tutto andrà bene se modifichi il codice iniziale nel modo seguente:

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

3
Grazie. Funzionerà anche perché get()consuma il personaggio successivo. C'è anche quello (std::cin >> name).ignore()che ho suggerito prima nella mia risposta.
0x499602D2

"..lavora perché get () ..." Sì, esattamente. Ci scusiamo per aver dato la risposta senza dettagli.
Boris

4
Perché non semplicemente if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson

0

Ciò accade perché un avanzamento riga implicito noto anche come carattere di nuova riga \nviene aggiunto a tutti gli input dell'utente da un terminale mentre indica al flusso di iniziare una nuova riga. È possibile tenerne conto in modo sicuro utilizzando std::getlinequando si verificano più righe di input dell'utente. Il comportamento predefinito di std::getlineleggerà tutto fino al carattere di nuova riga incluso \ndall'oggetto flusso di input che è std::cinin questo caso.

#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"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.