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

#c

#c #ввод #iostream #istream #c -faq

Вопрос:

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

 #include <iostream>
#include <string>

int main()
{
    int age;
    std::string name;

    std::cin >> age;
    std::getline(std::cin, name);
    
    if (std::cin)
    {
        std::cout << "My cat is " << age << " years old and their name is " << name << std::endl;
    }
}
 

Я обнаружил, что возраст был успешно прочитан, но не имя. Вот ввод и вывод:

 Input:

"10"
"Mr. Whiskers"

Output:

"My cat is 10 years old and their name is "
 

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

Комментарии:

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

Ответ №1:

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

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

 "10n"
 

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

Решение

cin.ignore()

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

 std::cin >> age;
std::cin.ignore();
std::getline(std::cin, name);

assert(std::cin); 
// Success!
 

std::ws

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

 std::cin >> age;
std::getline(std::cin >> std::ws, name);

assert(std::cin);
// Success!
 

std::cin >> std::ws Выражение выполняется перед std::getline() вызовом (и после std::cin >> age вызова), так что символ новой строки удаляется.

Разница в том, что ignore() отбрасывается только 1 символ (или N символов при задании параметра) и std::ws продолжает игнорировать пробелы, пока не найдет символ, не содержащий пробелов. Итак, если вы не знаете, сколько пробелов будет предшествовать следующему токену, вам следует рассмотреть возможность его использования.

Сопоставьте операции

Когда вы сталкиваетесь с подобной проблемой, обычно это связано с тем, что вы комбинируете форматированные операции ввода с неформатированными операциями ввода. Операция форматированного ввода — это когда вы принимаете ввод и форматируете его для определенного типа. Это то operator>>() , для чего нужно. Неформатированные операции ввода — это что-то другое, например std::getline() , std::cin.read() , std::cin.get() , и т.д. Эти функции не заботятся о формате ввода и обрабатывают только необработанный текст.

Если вы придерживаетесь использования одного типа форматирования, вы можете избежать этой досадной проблемы:

 // Unformatted I/O
std::string age, name;
std::getline(std::cin, age);
std::getline(std::cin, name);
 

или

 // Formatted I/O
int age;
std::string firstName, lastName;
std::cin >> age >> firstName >> lastName;
 

Если вы решите читать все как строки, используя неформатированные операции, вы можете впоследствии преобразовать их в соответствующие типы.

Комментарии:

1. Почему не просто if (getline(std::cin, name) amp;amp; getline(std::cin, state)) ?

2. @FredLarson Хорошая мысль. Хотя это не сработало бы, если первое извлечение состоит из целого числа или чего-либо, что не является строкой.

3. Конечно, здесь это не так, и нет смысла делать одно и то же двумя разными способами. Для целого числа вы могли бы превратить строку в строку, а затем использовать std::stoi() , но тогда не так ясно, что есть преимущество. Но я, как правило, предпочитаю просто использовать std::getline() для ввода, ориентированного на строку, а затем обрабатывать синтаксический анализ строки любым способом, который имеет смысл. Я думаю, что это менее подвержено ошибкам.

4. @FredLarson согласился. Может быть, я добавлю это, если у меня будет время.

5. @Albin Причина, по которой вы можете захотеть использовать std::getline() , заключается в том, что вы хотите захватить все символы вплоть до заданного разделителя и ввести их в строку, по умолчанию это новая строка. Если это X количество строк представляет собой всего лишь отдельные слова / токены, то эту работу можно легко выполнить >> . В противном случае вы бы ввели первое число в целое число с >> помощью, вызвали cin.ignore() следующую строку, а затем запустили цикл, в котором вы используете getline() .

Ответ №2:

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

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

Комментарии:

1. Спасибо. Это также будет работать, потому get() что использует следующий символ. Есть также (std::cin >> name).ignore() то, что я предлагал ранее в своем ответе.

2. «.. работает, потому что get() …» Да, точно. Извините, что даю ответ без подробностей.

3. Почему не просто if (getline(std::cin, name) amp;amp; getline(std::cin, state)) ?

Ответ №3:

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

Ответ №4:

Поскольку все вышеперечисленные ответили на проблему ввода 10nMr Whiskern , я хотел бы ответить на другой подход. все вышеприведенные решения опубликовали код для if буфера 10nMr Whiskern . но что, если мы не знаем, как пользователь будет вести себя при вводе. пользователь может ввести 10nnMr. Whiskern или 10 nn Mr. whiskern по ошибке. в этом случае приведенные выше коды могут не работать. итак, я использую приведенную ниже функцию для ввода строки для решения проблемы.

 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();
    
    std::cout << "My cat is " << age << " years old and their name is " << name << std::endl;
    
}
 

Дополнительно:

Если пользователь вводит a n10n nmr. whiskey ; Чтобы проверить, является ли int ввод допустимым или нет, эту функцию можно использовать для проверки int ввода (программа будет иметь неопределенное поведение, если char задано в качестве ввода вместо int ):

 
//instead of "std::cin>>age;" use "get_untill_int(amp;age);" in main function.
void get_Untill_Int(int* pInput)//keep taking input untill input is `int or float`
{
    cin>> *pInput;
    /*-----------check input validation----------------*/
    while (!cin) 
    {
        cin.clear();
        cin.ignore(100, 'n');
        cout<<"Invalid Input Type.nEnter again: ";
        cin >>*pInput;
    }
    /*-----------checked input validation-------------*/
}
 

Ответ №5:

Мне действительно интересно. C имеет специальную функцию для поглощения любых оставшихся или любых пробелов. Это называется std::ws . И затем вы можете просто использовать

 std::getline(std::cin >> std::ws, name);
 

Это должен быть идоматический подход. Для каждого перехода между форматированным и неформатированным вводом, который следует использовать.

Если мы говорим не о пробелах, а вводим, например, буквы там, где ожидается число, тогда мы должны следовать ссылке CPP и использовать

.ignore(std::numeric_limits<std::streamsize>::max(), 'n'); чтобы устранить неправильные вещи.

Пожалуйста, прочитайте здесь