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

Вопрос или проблема

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

#include <iostream>
#include <string>

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

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

    if (std::cin)
    {
        std::cout << "Моему коту " << age << " лет, и его имя " << name << std::endl;
    }
}

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

Ввод:

"10"
"Мистер Усы"

Вывод:

"Моему коту 10 лет, и его имя "

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

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

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

"10\n"

Символ новой строки всегда добавляется к вашему вводу, когда вы выбираете 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); 
// Успех!

std::ws

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

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

assert(std::cin);
// Успех!

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

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

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

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

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

// Неформатированный ввод-вывод
std::string age, name;
std::getline(std::cin, age);
std::getline(std::cin, name);

или

// Форматированный ввод-вывод
int age;
std::string firstName, lastName;
std::cin >> age >> firstName >> lastName;

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

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

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 << "Ваше имя " << name << " и вы живете в " << state;
    }
    return 0;
}
Ввод:

"Джон"
"Нью-Гэмпшир"

Вывод:

"Ваше имя Джон, и вы живете в Нью-Гэмпшир"

Я действительно удивляюсь. В C++ есть специальная функция, которая позволяет игнорировать любые оставшиеся или возможные пробелы. Она называется std::ws. И тогда вы можете просто использовать

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

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

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

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

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

Поскольку все вышеупомянутые ответы уже рассмотрели проблему с вводом 10\nMr Whisker\n, мне хотелось бы предложить альтернативный подход. Все решения выше опубликовали код, предполагая, что буфер будет выглядеть как 10\nMr Whisker\n. Но что, если мы не знаем, как себя поведет пользователь, вводя данные? Пользователь может случайно ввести 10\n\nMr. Whisker\n или 10 \n\n Mr. whisker\n. В таком случае указанные выше коды могут не сработать. Поэтому я использую функцию ниже для получения строкового ввода, чтобы решить эту проблему.

string StringInput()  //возвращает строку с нулевым окончанием
{
    string input;
    getline(cin, input);
    while(input.length()==0)//продолжает запрашивать ввод, пока не будет введена действительная строка
    {
        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 << "Моему коту " << age << " лет, и его имя " << name << std::endl;

}

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

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

//вместо "std::cin>>age;" используйте "get_untill_int(&age);" в основной функции.
void get_Untill_Int(int* pInput)//продолжает запрашивать ввод до тех пор, пока ввод не будет `int или float`
{
    cin>> *pInput;
    /*-----------проверка валидности ввода----------------*/
    while (!cin) 
    {
        cin.clear();
        cin.ignore(100, '\n');
        cout<<"Некорректный тип ввода.\nВведите снова: ";
        cin >>*pInput;
    }
    /*-----------проверена валидность ввода-------------*/
}

Ответ или решение

Использование функции std::getline() в C++ может оказаться не таким очевидным, как кажется на первый взгляд, особенно после использования форматированных операций ввода, таких как оператор >>. Давайте подробнее разберёмся, почему std::getline() пропускает ввод после форматированного извлечения и как можно этого избежать.

Почему std::getline() пропускает ввод

Когда вы вводите данные через std::cin >> age, в поток добавляется и символ новой строки (\n), идущий после нажатия клавиши "Enter". На этом этапе в буфере остаётся:

"10\n"

В результате, когда вы вызываете std::getline(std::cin, name), происходит следующее:

  1. std::getline() начинает считывать символы с текущей позиции в потоке, и её целью является чтение строки до символа новой строки (\n).
  2. Поскольку после считывания age в буфере остался символ новой строки, std::getline() сразу же наталкивается на него, интерпретируя это как конец строки, и завершает операцию, не получая никаких данных для переменной name.

Таким образом, вы видите, что переменная name остаётся пустой, что и приводит к неполным выводимым данным:

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

Решение проблемы

Есть несколько способов справиться с этой ситуацией:

1. Использование std::cin.ignore()

Одним из самых простых решений является использование std::cin.ignore() после извлечения целого числа. Это приведёт к игнорированию оставшегося символа новой строки в потоке:

std::cin >> age;
std::cin.ignore(); // Игнорируем символ новой строки
std::getline(std::cin, name);

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

2. Использование std::ws

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

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

Этот метод также эффективен и позволяет избавиться от необходимости делать дополнительный вызов функции ignore().

3. Избегать смешивания типов ввода

Если возможно, лучше придерживаться одного стиля ввода, в зависимости от желаемого результата. Например:

  • Если вы хотите использовать только ненаправленный ввод, можете использовать только std::getline():
std::string age_str;
std::getline(std::cin, age_str);
int age = std::stoi(age_str); // Преобразование строки в целое число
  • В случае, если предпочитаете форматированный ввод, используйте только >> оператор и избегайте getline().

Заключение

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

Следование этим рекомендациям упростит вашу работу с потоками ввода-вывода в C++ и улучшит читаемость и надежность вашего кода.

Оцените материал
Добавить комментарий

Капча загружается...