Почему std::getline() пропускает ввод после форматированного извлечения?
У меня есть следующий фрагмент кода, который запрашивает у пользователя его имя и состояние:
#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 "
Почему название штата было опущено в выводе? Я дал правильный ввод, но код почему-то игнорирует это. Почему это происходит?
5 ответов
Почему это происходит?
Это не имеет ничего общего с вводом, который вы предоставили самостоятельно, а скорее с поведением по умолчанию std::getline()
экспонаты. Когда вы предоставили свой ввод для имени (std::cin >> name
), вы не только отправили следующие символы, но и неявный символ новой строки был добавлен в поток:
"John\n"
Новая строка всегда добавляется к вашему вводу, когда вы выбираете Enter или Return при отправке с терминала. Он также используется в файлах для перехода к следующей строке. Новая строка остается в буфере после извлечения в name
до следующей операции ввода / вывода, когда она либо сбрасывается, либо потребляется. Когда поток управления достигает std::getline()
, новая строка будет отброшена, но ввод немедленно прекратится. Причина, по которой это происходит, заключается в том, что функциональность по умолчанию этой функции диктует, что она должна (она пытается прочитать строку и останавливается, когда находит новую строку).
Поскольку этот ведущий символ новой строки запрещает ожидаемую функциональность вашей программы, из этого следует, что ее нужно как-то пропустить. Один из вариантов - позвонить std::cin.ignore()
после первого извлечения. Он будет отбрасывать следующий доступный символ, чтобы новая строка больше не была навязчивой.
Углубленное объяснение:
Это перегрузка 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::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
Манипулятор.
2: Входные потоки по умолчанию считают определенные символы пробелами, такими как пробел, символ новой строки, перевод формы, возврат каретки и т. Д.
3: это подпись std::basic_istream<...>::ignore()
, Вы можете вызвать его с нулевыми аргументами, чтобы удалить один символ из потока, одним аргументом, чтобы отбросить определенное количество символов, или двумя аргументами, чтобы отбросить count
символы или пока не достигнет delim
в зависимости от того, кто приходит первым Вы обычно используете std::numeric_limits<std::streamsize>::max()
как значение count
если вы не знаете, сколько символов перед разделителем, но вы все равно хотите их удалить.
Все будет хорошо, если вы измените свой исходный код следующим образом:
if ((cin >> name).get() && std::getline(cin, state))
Это происходит потому, что неявный перевод строки также известен как символ перевода строки \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"
Поскольку все вышеперечисленные ответили на проблему для ввода, я хотел бы ответить по-другому. все вышеприведенное решение опубликовало код, если буфер похож на
10\nMr Whisker\n
. но что, если мы не знаем, как пользователь будет вести себя, вводя данные. пользователь может ввести
10\n\nMr. Whisker\n
или же
10 \n\n Mr. whisker\n
по ошибке. в этом случае приведенные выше коды могут не работать. Итак, я использую функцию ниже, чтобы ввести строковый ввод для решения проблемы.
string StringInput() //returns null-terminated string
{
string input;
getline(cin, input);
while(input.length()==0)//keep taking input until valid string is taken
{
getline(cin, input);
}
return input.c_str();
}
Итак, ответ будет:
#include <iostream>
#include <string>
int main()
{
int age;
std::string name;
std::cin >> age;
name = StringInput();
if (std::cin)
{
std::cout << "My cat is " << age << " years old and their name is " << name << std::endl;
}
}
Мне действительно интересно. C++ имеет специальную функцию для заполнения любых оставшихся пробелов. Он называется std::ws . И тогда вы можете просто использовать
std::getline(std::cin >> std::ws, name);
Это должно быть идолопоклонническим подходом. Для каждого перехода между форматированными и неформатированными входными данными следует использовать taht.