Почему std::getline()пропускает ввод после форматированного экстрагирования

c++ input iostream istream c++-faq


У меня есть следующий кусок кода,который подсказывает пользователю его имя и состояние:

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

Что я нахожу,так это то,что название было успешно извлечено,но не государство.Вот входное и результирующее состояние:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

Почему название государства было опущено из вывода? Я дал правильный ввод,но код каким-то образом игнорирует его.Почему это происходит?




Answer 1 0x499602D2


Почему это происходит?

Это имеет мало общего с вводом, который вы предоставили самостоятельно, а скорее с поведением по умолчанию std::getline() . Когда вы указали свой ввод для имени ( std::cin >> name ), вы не только отправили следующие символы, но и неявный символ новой строки был добавлен к потоку:

"John\n"

Новая строка всегда добавляется к вашему входу,когда вы выбираетеEnterorReturnпри отправке из терминала. Он также используется в файлах для перехода к следующей строке. Новая строка остается в буфере после извлечения в name до следующей операции ввода-вывода, где она либо отбрасывается, либо используется. Когда поток управления достигает std::getline() , новая строка будет отброшена, но ввод немедленно прекратится. Причина, по которой это происходит, заключается в том, что функциональность по умолчанию этой функции диктует, что она должна (она пытается прочитать строку и останавливается, когда находит новую строку).

Поскольку этот ведущий символ новой строки запрещает ожидаемую функциональность вашей программы, из этого следует, что ее нужно как-то игнорировать. Одним из вариантов является вызов std::cin.ignore() после первого извлечения. Он будет отбрасывать следующий доступный символ, чтобы новая строка больше не мешала.

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

Глубокое объяснение:

Это перегрузка std::getline() которую вы вызвали:

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

Другая перегрузка этой функции принимает разделитель типа charT . Символ разделителя - это символ, представляющий границу между последовательностями ввода. Эта конкретная перегрузка по умолчанию устанавливает разделитель на символ новой строки input.widen('\n') , поскольку он не был указан.

Теперь вот несколько условий, при которых std::getline() завершает ввод:

  • Если поток извлек максимальное количество символов, std::basic_string<charT> может содержать std :: basic_string <charT>
  • Если был найден символ конца файла (EOF)
  • Если разделитель найден

Третье условие - это то, с которым мы имеем дело. Ваш вклад в state представляется так:

"John\nNew Hampshire"
     ^
     |
 next_pointer

где next_pointer - следующий символ, который будет проанализирован. Поскольку символ, хранящийся в следующей позиции во входной последовательности, является разделителем, std::getline() тихо отбросит этот символ, next_pointer значение next_pointer до следующего доступного символа и остановит ввод. Это означает, что остальные предоставленные вами символы все еще остаются в буфере для следующей операции ввода-вывода. Вы заметите, что если вы выполните еще одно чтение из строки в state , ваше извлечение даст правильный результат, так как последний вызов std::getline() отбрасывает разделитель.


Возможно, вы заметили, что обычно вы не сталкиваетесь с этой проблемой при извлечении с помощью оператора форматированного ввода ( operator>>() ). Это связано с тем, что входные потоки используют пробелы в качестве разделителей для ввода и по умолчанию установлен манипулятор std::skipws 1 . Потоки будут отбрасывать начальные пробелы из потока, когда начинают выполнять форматированный ввод. 2

В отличие от форматированных операторов ввода, std::getline() является неформатированной функцией ввода. И все неотформатированные функции ввода имеют следующий общий код:

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

Выше представлен часовой объект, который создается во всех отформатированных / неформатированных функциях ввода-вывода в стандартной реализации C ++. Объекты Sentry используются для подготовки потока к вводу-выводу и определения того, находится ли он в состоянии сбоя. Вы найдете только то, что в неотформатированных входных функциях второй аргумент конструктора sentry равен true . Этот аргумент означает, что начальные пробелы не будут отбрасываться с начала входной последовательности. Вот соответствующая цитата из Стандарта [§27.7.2.1.3 / 2]:

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

[...] Если noskipws равен нулю и is.flags() & ios_base::skipws равен нулю, функция извлекает и удаляет каждый символ, если следующий доступный входной символ c является символом пробела. [...]

Поскольку вышеприведенное условие ложно, сторожевой объект не будет отбрасывать пробел. Причина, по которой noskipws устанавливается в true с помощью этой функции, заключается в том, что целью std::getline() является чтение необработанных неформатированных символов в объект std::basic_string<charT> .


Решение:

Нет способа остановить это поведение std::getline() . То, что вам нужно сделать, это удалить новую строку самостоятельно перед std::getline() (но сделайте это после форматированного извлечения). Это можно сделать с помощью ignore() чтобы отменить оставшуюся часть ввода, пока мы не достигнем новой новой строки:

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

Вам нужно будет включить <limits> чтобы использовать std::numeric_limits . std::basic_istream<...>::ignore() - это функция, которая отбрасывает указанное количество символов, пока не найдет разделитель или не достигнет конца потока ( ignore() также отбрасывает разделитель, если найдет его) , Функция max() возвращает наибольшее количество символов, которое может принять поток.

Еще один способ отбросить пробел - использовать функцию std::ws которая является манипулятором, предназначенным для извлечения и отбрасывания начальных пробелов из начала входного потока:

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

Какая разница?

Разница в том, что ignore(std::streamsize count = 1, int_type delim = Traits::eof()) 3 без разбора отбрасывает символы до тех пор, пока он либо не отбросит count символов, не найдет разделитель (заданный вторым аргументом delim ) или не достигнет конца потока. std::ws используется только для удаления пробельных символов в начале потока.

Если вы смешиваете форматированный ввод с неформатированным вводом и вам нужно удалить остаточный пробел, используйте std::ws . В противном случае, если вам нужно очистить недопустимый ввод независимо от того, что это, используйте ignore() . В нашем примере нам нужно только очистить пробел, так как поток потреблял ваш ввод "John" для переменной name . Осталось только символ новой строки.


1: std::skipws - это манипулятор, который сообщает потоку ввода отбрасывать начальные пробелы при выполнении форматированного ввода. Это можно отключить с std::noskipws манипулятора std :: noskipws .

2: Входные потоки по умолчанию считают определенные символы пробелами, такими как пробел, символ новой строки, перевод формы, возврат каретки и т. Д.

3: это подпись std::basic_istream<...>::ignore() . Вы можете вызвать его с нулевыми аргументами, чтобы отбросить один символ из потока, одним аргументом, чтобы отбросить определенное количество символов, или двумя аргументами, чтобы отбросить count символов, или пока он не достигнет delim , в зависимости от того, какой из них окажется первым. Обычно вы используете std::numeric_limits<std::streamsize>::max() в качестве значения count , если вы не знаете, сколько символов перед разделителем, но вы все равно хотите их отбросить.




Answer 2 Boris


Все будет хорошо,если вы измените свой исходный код следующим образом:

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



Answer 3 Justin Randall


Это происходит потому, что неявный перевод строки, также известный как символ новой строки \n , добавляется ко всем пользовательским вводам с терминала, когда он сообщает потоку начать новую строку. Вы можете смело учитывать это, используя std::getline при проверке нескольких строк пользовательского ввода. Поведение по умолчанию std::getline будет читать все, включая символ новой строки \n из объекта входного потока, в данном случае это std::cin .

#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"