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

109

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

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

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

0x499602D2
источник
Я считаю, std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)что он также должен работать, как ожидалось. (В дополнение к ответам ниже).
jww

Ответы:

125

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

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

"John\n"

Новая строка всегда добавляется к вашему вводу, когда вы выбираете Enterили Returnотправляете с терминала. Он также используется в файлах для перехода к следующей строке. Новая строка остается в буфере после извлечения в 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>может содержать
  • Если был найден символ конца файла (EOF)
  • Если разделитель найден

Мы имеем дело с третьим условием. Ваш вклад в stateпредставлен следующим образом:

"John\nNew Hampshire"
     ^
     |
 next_pointer

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


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

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

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

Вышеупомянутый объект-часовой, который создается во всех форматированных / неформатированных функциях ввода-вывода в стандартной реализации C ++. Объекты 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манипулятора.

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

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

0x499602D2
источник
1
Почему не просто if (getline(std::cin, name) && getline(std::cin, state))?
Фред Ларсон,
@FredLarson Хорошее замечание. Хотя это не сработает, если первое извлечение будет целым числом или чем-либо, кроме строки.
0x499602D2,
Конечно, здесь дело обстоит не так, и нет смысла делать одно и то же двумя разными способами. Для целого числа вы можете превратить строку в строку, а затем использовать std::stoi(), но тогда не так ясно, есть ли преимущество. Но я предпочитаю просто использовать std::getline()строчно-ориентированный ввод, а затем разбирать строку любым разумным способом. Я думаю, что это менее подвержено ошибкам.
Фред Ларсон,
@FredLarson Согласен. Может быть, я добавлю это, если у меня будет время.
0x499602D2
1
@Albin Причина, по которой вы, возможно, захотите использовать, std::getline()- это если вы хотите захватить все символы до заданного разделителя и ввести их в строку, по умолчанию это новая строка. Если это Xколичество строк - это просто отдельные слова / токены, тогда эту работу можно легко выполнить >>. В противном случае вы должны ввести первое число в целое число с помощью >>, вызвать cin.ignore()следующую строку, а затем запустить цикл, в котором вы используете getline().
0x499602D2
11

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

if ((cin >> name).get() && std::getline(cin, state))
Борис
источник
3
Спасибо. Это также будет работать, потому что get()потребляет следующий символ. Также есть то, (std::cin >> name).ignore()что я предлагал ранее в своем ответе.
0x499602D2
"..работаем, потому что get () ..." Да, именно так. Извините за ответ без подробностей.
Борис
4
Почему не просто if (getline(std::cin, name) && getline(std::cin, state))?
Фред Ларсон,
0

Это происходит потому, что неявный перевод строки, также известный как символ новой строки, \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"
Джастин Рэндалл
источник