提问者:小点点

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


下面的代码提示用户输入他们的名称和状态:

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

为什么在输出中省略了州名? 我给出了正确的输入,但代码不知怎么忽略了它。 为什么会出现这种情况?


共3个答案

匿名用户

这与您自己提供的输入几乎没有关系,而是与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 )

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

下面是std::getLine()终止输入的几个条件:

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

第三个条件是我们要处理的。 您对state的输入如下所示:

"John\nNew Hampshire"
     ^
     |
 next_pointer

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

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

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

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

上面是一个sentry对象,它在标准C++实现中的所有格式化/非格式化I/O函数中被实例化。 哨兵对象用于为I/O准备流,并确定流是否处于失败状态。 您只会发现在未格式化的输入函数中,sentry构造函数的第二个参数是true。 该参数意味着不会从输入序列的开始丢弃前导空格。 以下是标准[§27.7.2.1.3/2]:

 explicit sentry(basic_istream<charT, traits>& is, bool noskipws = false);

[...] 如果noskipws为零且为.flags()& IOS_BASE::skipws为非零,则只要下一个可用输入字符C是空白字符,函数就提取并丢弃每个字符。 [...]

由于上述条件为false,哨兵对象不会丢弃空格。 此函数之所以将noskipws设置为true,是因为std::getLine()的作用是将未格式化的原始字符读入std::basic_string对象。

没有办法停止std::getLine()的这种行为。 您必须做的是在std::getLine()运行之前自己丢弃新行(但在格式化提取之后执行)。 这可以通过使用ignore()丢弃输入的其余部分来实现,直到我们到达一个新的新行:

if (std::cin >> name &&
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n') &&
    std::getline(std::cin, state))
{ ... }

您需要包含才能使用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::max()作为count的值。

匿名用户

如果您按以下方式更改初始代码,一切都将正常:

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

匿名用户

发生这种情况的原因是一个隐式换行符(也称为换行符\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"