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

0x4*_*2D2 92 c++ iostream input c++-faq istream

我有以下代码提示用户输入他们的名字和状态:

#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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我发现该名称已被成功提取,但不是州.这是输入和结果输出:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "
Run Code Online (Sandbox Code Playgroud)

为什么输出中省略了状态名称?我给出了正确的输入,但代码忽略了它.为什么会这样?

0x4*_*2D2 110

为什么会这样?

这与您自己提供的输入几乎没有关系,而是与默认行为std::getline()展示有关.当您提供名称(std::cin >> name)的输入时,您不仅提交了以下字符,而且还在流中附加了隐式换行符:

"John\n"
Run Code Online (Sandbox Code Playgroud)

当您选择EnterReturn从终端提交时,新行始终会附加到您的输入中.它也用于文件中以移动到下一行.在提取之后,换行符将保留在缓冲区中,name直到下一个I/O操作被丢弃或消耗为止.当控制流到达时std::getline(),换行符将被丢弃,但输入将立即停止.发生这种情况的原因是因为此函数的默认功能指示它应该(它尝试读取一行并在找到换行符时停止).

因为这个领先的换行符会抑制程序的预期功能,所以必须以某种方式忽略它们.一种选择是std::cin.ignore()在第一次提取后调用.它将丢弃下一个可用字符,以便换行符不再具有侵入性.


深入解释:

这是std::getline()你所谓的超载:

std::getline(std::cin.ignore(), state)
Run Code Online (Sandbox Code Playgroud)

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

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

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

第三个条件是我们正在处理的问题.您的输入state如此表示:

template<class charT>
std::basic_istream<charT>& getline( std::basic_istream<charT>& input,
                                    std::basic_string<charT>& str )
Run Code Online (Sandbox Code Playgroud)

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


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

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

"John\nNew Hampshire"
     ^
     |
 next_pointer
Run Code Online (Sandbox Code Playgroud)

以上是一个sentry对象,它在标准C++实现中的所有格式化/未格式化的I/O函数中实例化.Sentry对象用于为I/O准备流并确定它是否处于失败状态.你只能在未格式化的输入函数中找到iftry构造函数的第二个参数true.该参数意味着不会从输入序列的开头丢弃前导空格.以下是标准[§27.7.2.1.3/ 2]的相关引用:

typename std::basic_istream<charT>::sentry ok(istream_object, true);
Run Code Online (Sandbox Code Playgroud)

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

由于上述条件为假,因此哨兵对象不会丢弃空格.这个函数noskipws设置的原因是true因为std::getline()要将原始的,未格式化的字符读入std::basic_string<charT>对象.


解决方案:

没有办法阻止这种行为std::getline().您需要做的是在std::getline()运行之前自己丢弃新行(但在格式化提取执行此操作).这可以通过使用ignore()丢弃输入的其余部分来完成,直到我们达到一个新的新行:

 explicit sentry(basic_istream<charT, traits>& is, bool noskipws = false);
Run Code Online (Sandbox Code Playgroud)

你需要包括<limits>使用std::numeric_limits.std::basic_istream<...>::ignore()是一个丢弃指定数量的字符的函数,直到找到分隔符或到达流的末尾(ignore()如果找到分隔符,也会丢弃该分隔符).该max()函数返回流可以接受的最大字符数.

丢弃空格的另一种方法是使用std::ws函数,该函数是一个操纵器,用于从输入流的开头提取和丢弃前导空格:

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

有什么不同?

区别在于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,但是你想要丢弃它们.

  • @Albin您可能想要使用`std::getline()`的原因是如果您想捕获给定分隔符之前的所有字符并将其输入到字符串中,默认情况下这是换行符。如果这些“X”个字符串只是单个单词/标记,那么可以使用“&gt;&gt;”轻松完成这项工作。否则,您可以使用“&gt;&gt;”将第一个数字输入到整数中,在下一行调用“cin.ignore()”,然后运行一个使用“getline()”的循环。 (2认同)

小智 10

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

if ((cin >> name).get() && std::getline(cin, state))
Run Code Online (Sandbox Code Playgroud)

  • 为什么不简单地`if(getline(std :: cin,name)&& getline(std :: cin,state))`? (4认同)
  • 谢谢.这也有效,因为`get()`会消耗下一个字符.还有我在回答中提到的`(std :: cin >> name).ignore()`. (2认同)