Por que std :: getline () pula a entrada após uma extração formatada?


105

Eu tenho o seguinte trecho de código que solicita ao usuário seu nome e estado:

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

O que descobri é que o nome foi extraído com sucesso, mas não o estado. Aqui está a entrada e a saída resultante:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

Por que o nome do estado foi omitido da saída? Dei a entrada adequada, mas o código de alguma forma a ignora. Por que isso acontece?


Eu acredito std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)que também deve funcionar conforme o esperado. (Além das respostas abaixo).
jww

Respostas:


122

Por que isso acontece?

Isso tem pouco a ver com a entrada que você forneceu, mas com as std::getline()exibições de comportamento padrão . Quando você forneceu sua entrada para o nome ( std::cin >> name), não apenas enviou os seguintes caracteres, mas também uma nova linha implícita foi anexada ao fluxo:

"John\n"

Uma nova linha é sempre anexada à sua entrada quando você seleciona Enterou Returnao enviar de um terminal. Também é usado em arquivos para mover para a próxima linha. A nova linha é deixada no buffer após a extração nameaté a próxima operação de E / S, onde é descartada ou consumida. Quando o fluxo de controle chegar std::getline(), a nova linha será descartada, mas a entrada cessará imediatamente. A razão para isso acontecer é porque a funcionalidade padrão dessa função determina que ela deve (ela tenta ler uma linha e para quando encontra uma nova linha).

Como essa nova linha inibe a funcionalidade esperada de seu programa, ela deve ser ignorada ou ignorada de alguma forma. Uma opção é chamar std::cin.ignore()após a primeira extração. Ele descartará o próximo caractere disponível para que a nova linha não atrapalhe.

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

Explicação detalhada:

Essa é a sobrecarga do std::getline()que você chamou:

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

Outra sobrecarga dessa função leva um delimitador de tipo charT. Um caractere delimitador é um caractere que representa o limite entre as sequências de entrada. Essa sobrecarga específica define o delimitador para o caractere de nova linha input.widen('\n')por padrão, pois um não foi fornecido.

Agora, essas são algumas das condições pelas quais std::getline()termina a entrada:

  • Se o stream extraiu a quantidade máxima de caracteres que um std::basic_string<charT>pode conter
  • Se o caractere de fim de arquivo (EOF) foi encontrado
  • Se o delimitador foi encontrado

A terceira condição é aquela com a qual estamos lidando. Sua entrada em stateé representada da seguinte forma:

"John\nNew Hampshire"
     ^
     |
 next_pointer

onde next_pointeré o próximo caractere a ser analisado. Visto que o caractere armazenado na próxima posição na sequência de entrada é o delimitador, std::getline()descartará silenciosamente esse caractere, aumentará next_pointerpara o próximo caractere disponível e interromperá a entrada. Isso significa que o restante dos caracteres fornecidos ainda permanecem no buffer para a próxima operação de E / S. Você notará que se realizar outra leitura da linha para dentro state, sua extração produzirá o resultado correto como a última chamada parastd::getline() descartar o delimitador.


Você deve ter notado que normalmente não encontra esse problema ao extrair com o operador de entrada formatado ( operator>>()). Isso ocorre porque os fluxos de entrada usam espaços em branco como delimitadores de entrada e têm o std::skipws1 manipulador ativado por padrão. Streams irá descartar o espaço em branco inicial do stream ao começar a realizar a entrada formatada. 2

Ao contrário dos operadores de entrada formatados, std::getline()é uma função de entrada não formatada . E todas as funções de entrada não formatadas têm o seguinte código um tanto em comum:

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

O acima é um objeto sentinela que é instanciado em todas as funções de E / S formatadas / não formatadas em uma implementação C ++ padrão. Objetos de sentinela são usados ​​para preparar o fluxo para E / S e determinar se ele está ou não em um estado de falha. Você descobrirá apenas que nas funções de entrada não formatadas , o segundo argumento para o construtor de sentinela é true. Esse argumento significa que o espaço em branco inicial não será descartado do início da sequência de entrada. Aqui está a citação relevante do Padrão [§27.7.2.1.3 / 2]:

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

[...] Se noskipwsfor zero e is.flags() & ios_base::skipwsfor diferente de zero, a função extrai e descarta cada caractere, desde que o próximo caractere de entrada disponível cseja um caractere de espaço em branco. [...]

Visto que a condição acima é falsa, o objeto sentinela não descartará o espaço em branco. O motivo noskipwsdefinido truepor essa função é porque o objetivo de std::getline()é ler caracteres brutos e não formatados em um std::basic_string<charT>objeto.


A solução:

Não há como impedir esse comportamento de std::getline(). O que você terá que fazer é descartar a nova linha antes de std::getline()executá-la (mas faça isso após a extração formatada). Isso pode ser feito usando ignore()para descartar o resto da entrada até chegarmos a uma nova linha:

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

Você precisará incluir <limits>para usar std::numeric_limits. std::basic_istream<...>::ignore()é uma função que descarta uma quantidade especificada de caracteres até encontrar um delimitador ou chegar ao final do fluxo ( ignore()também descarta o delimitador se o encontrar). A max()função retorna a maior quantidade de caracteres que um fluxo pode aceitar.

Outra maneira de descartar o espaço em branco é usar a std::wsfunção, que é um manipulador projetado para extrair e descartar o espaço em branco inicial do início de um fluxo de entrada:

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

Qual é a diferença?

A diferença é que ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 descarta caracteres indiscriminadamente até que descarta os countcaracteres, encontre o delimitador (especificado pelo segundo argumento delim) ou atinja o final do fluxo.std::wsé usado apenas para descartar caracteres de espaço em branco do início do fluxo.

Se você estiver misturando entrada formatada com entrada não formatada e precisar descartar os espaços em branco residuais, use std::ws. Caso contrário, se você precisar limpar a entrada inválida independentemente do que seja, use ignore(). Em nosso exemplo, só precisamos limpar os espaços em branco, pois o fluxo consumiu sua entrada "John"para a namevariável. Tudo o que restou foi o caractere de nova linha.


1: std::skipwsé um manipulador que diz ao fluxo de entrada para descartar os espaços em branco iniciais ao realizar a entrada formatada. Isso pode ser desligado com o std::noskipwsmanipulador.

2: Os fluxos de entrada consideram certos caracteres como espaços em branco por padrão, como o caractere de espaço, caractere de nova linha, avanço de formulário, retorno de carro, etc.

3: Esta é a assinatura de std::basic_istream<...>::ignore(). Você pode chamá-lo com zero argumentos para descartar um único caractere do fluxo, um argumento para descartar uma certa quantidade de caracteres ou dois argumentos para descartar countcaracteres ou até atingir delim, o que vier primeiro. Você normalmente usa std::numeric_limits<std::streamsize>::max()como o valor de countse não souber quantos caracteres existem antes do delimitador, mas deseja descartá-los de qualquer maneira.


1
Por que não simplesmente if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson

@FredLarson Bom argumento. Embora não funcione se a primeira extração for um inteiro ou qualquer coisa que não seja uma string.
0x499602D2

Claro, esse não é o caso aqui e não há por que fazer a mesma coisa de duas maneiras diferentes. Para um inteiro, você pode colocar a linha em uma string e usar std::stoi(), mas não fica tão claro que há uma vantagem. Mas eu tendo a preferir usar apenas std::getline()para entrada orientada por linha e, em seguida, lidar com a análise da linha de qualquer maneira que faça sentido. Acho que é menos sujeito a erros.
Fred Larson

@FredLarson concordou. Talvez eu acrescente isso se tiver tempo.
0x499602D2

1
@Albin O motivo que você pode querer usar std::getline()é se você deseja capturar todos os caracteres até um determinado delimitador e inseri-lo em uma string, por padrão que é a nova linha. Se esse Xnúmero de strings forem apenas palavras / tokens individuais, esse trabalho pode ser facilmente realizado com >>. Caso contrário, você entraria com o primeiro número em um inteiro com >>, chamaria cin.ignore()na próxima linha e, em seguida, executaria um loop onde usar getline().
0x499602D2

11

Tudo ficará bem se você alterar seu código inicial da seguinte maneira:

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

3
Obrigado. Isso também funcionará porque get()consome o próximo caractere. Também há o (std::cin >> name).ignore()que sugeri anteriormente em minha resposta.
0x499602D2

"..trabalhar porque get () ..." Sim, exatamente. Desculpe por dar a resposta sem detalhes.
Boris

4
Por que não simplesmente if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson

0

Isso acontece porque uma alimentação de linha implícita, também conhecida como caractere de nova linha, \né anexada a todas as entradas do usuário de um terminal, pois está informando ao fluxo para iniciar uma nova linha. Você pode considerar isso com segurança usando std::getlineao verificar várias linhas de entrada do usuário. O comportamento padrão de std::getlinelerá tudo até e incluindo o caractere de nova linha \ndo objeto de fluxo de entrada que é std::cinneste 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"
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.