为什么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

Answers:


122

为什么会这样?

这与您自己提供的输入无关,而与默认行为std::getline()显示有关。当您输入名称(std::cin >> name)时,您不仅提交了以下字符,而且还向流添加了隐式换行符:

"John\n"

选择EnterReturn从终端提交时,换行符总是附加到输入中。它也用于文件中以移至下一行。提取后,换行符将保留在缓冲区中,name直到下一个I / O操作被丢弃或消耗为止。当控制流到达时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 )

此函数的另一个重载采用type的定界符charT。分隔符是代表输入序列之间边界的字符。input.widen('\n')由于没有提供,默认情况下,此特殊重载会将分隔符默认设置为换行符。

现在,这些是std::getline()终止输入的一些条件:

  • 如果流提取了最大数量的字符,则std::basic_string<charT>可以容纳
  • 如果找到文件结尾(EOF)字符
  • 如果找到分隔符

第三个条件是我们正在处理的条件。您输入的内容将state这样表示:

"John\nNew Hampshire"
     ^
     |
 next_pointer

next_pointer下一个要解析的字符在哪里。由于存储在输入序列中下一个位置的字符是定界符,因此std::getline()将安静地丢弃该字符,递增next_pointer到下一个可用字符,然后停止输入。这意味着您提供的其余字符仍保留在缓冲区中,以便进行下一个I / O操作。您会注意到,如果您从对的行中进行另一次读取,则state提取操作将产生正确的结果,作为最后一次调用std::getline()该分隔符的调用。


您可能已经注意到,使用格式化的输入运算符(operator>>())进行提取时,通常不会遇到此问题。这是因为输入流使用空格作为输入的分隔符,并且默认情况下启用了std::skipws1个操纵器。当开始执行格式化输入时,流将丢弃流中的前导空白。2

与格式化输入运算符不同,std::getline()它是未格式化的输入函数。所有未格式化的输入函数都有一些共同的以下代码:

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

上面是一个哨兵对象,它在标准C ++实现中的所有格式化/未格式化I / O函数中实例化。Sentry对象用于为I / O准备流并确定其是否处于故障状态。您只会发现在未格式化的输入函数中,岗亭构造函数的第二个参数是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_limitsstd::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 '16

当然,这里不是这种情况,以两种不同的方式来做同一件事毫无意义。对于整数,您可以将行放入字符串中,然后使用std::stoi(),但是并不清楚是否有优势。但是我倾向于只使用std::getline()面向行的输入,然后以任何有意义的方式处理解析行。我认为它不太容易出错。
弗雷德·拉森

@FredLarson同意。如果有时间的话,也许我会补充。
0x499602D2 '16

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 2014年

“ ..work因为get()...”是的,完全正确。很抱歉没有给出答案。
鲍里斯(Boris)2014年

4
为什么不简单if (getline(std::cin, name) && getline(std::cin, state))
弗雷德·拉尔森

0

发生这种情况的原因是,隐式换行符(也称为换行符)\n被附加到来自终端的所有用户输入,因为它告诉流开始新行。您可以通过std::getline检查多行用户输入来安全地解决此问题。的默认行为是从输入流对象std::getline读取所有内容,包括换行符,包括本例中的换行符。\nstd::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"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.