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


105

У мене є такий код, який спонукає користувача до їх імені та штату:

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

Чому назву держави було виключено з результатів? Я дав належний вклад, але код якось ігнорує його. Чому це відбувається?


Я вважаю, що std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)також слід працювати, як очікувалося. (Окрім наведених нижче відповідей).
jww

Відповіді:


122

Чому це відбувається?

Це мало стосується вкладеного вами вкладу, а швидше до поведінки за замовчуванням std::getline(). Коли ви надали вхід для імені ( 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якщо ви не знаєте, скільки символів є перед роздільником, але ви хочете їх відкинути.


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"
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.