为什么std:::getline()在格式化提取后会跳过输入?

c++ input iostream istream c++-faq


我有以下代码,提示用户的名字和状态。

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

为什么在输出中省略了国家的名称?我已经给了正确的输入,但代码却莫名其妙地忽略了它。为什么会出现这种情况?





Answer 1 0x499602D2


为什么会出现这种情况?

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

"John\n"

当你选择了 "输入法 "时,总会在你的输入中附加一个新行。EnterorReturn从终端提交时。它也用于文件中以移至下一行。提取 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 )

此函数的另一个重载采用 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::skipws 1操作器。当开始执行格式化输入时,流将丢弃流中的前导空白。2

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

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

上面是一个哨兵对象,它在标准C ++实现中的所有格式化/未格式化I / O函数中实例化。 Sentry对象用于为I / O准备流并确定其是否处于故障状态。您只会发现在未格式化的输入函数中,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_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() 。在我们的示例中,我们只需要清除空白,因为流消耗了您对 name 变量的 "John" 输入。剩下的只是换行符。


1: std::skipws 是操纵器,它告诉输入流在执行格式化输入时放弃前导空白。可以使用 std::noskipws 机械手将其关闭。

2:默认情况下,输入流将某些字符视为空格,例如空格字符,换行符,换页符,回车符等。

3:这是 std::basic_istream<...>::ignore() 的签名。您可以使用零个参数来调用它,以从流中丢弃单个字符,一个参数来丢弃流中的某些字符,或者使用两个参数来丢弃 count 字符,或者直到它到达 delim 为止,以先到者为准。如果您不知道在定界符之前有多少个字符,则通常使用 std::numeric_limits<std::streamsize>::max() 作为 count 的值,但是无论如何您都希望丢弃它们。




Answer 2 Boris


如果你用下面的方法改变你的初始代码,一切都会OK。

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



Answer 3 Justin Randall


发生这种情况的原因是,在隐式换行符(也称为换行符 \n 被附加到来自终端的所有用户输入时,因为它告诉流开始新行。您可以在检查多行用户输入时使用 std::getline 安全地解决这一问题。 std::getline 的默认行为将从输入流对象(在本例中为 std::cin 读取直到包括换行符 \n 在内的所有内容。

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