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

147
我有以下一段代码,用于提示用户输入他们猫的年龄和名字:
#include <iostream>
#include <string>

int main()
{
    int age;
    std::string name;

    std::cin >> age;
    std::getline(std::cin, name);
    
    if (std::cin)
    {
        std::cout << "My cat is " << age << " years old and their name is " << name << std::endl;
    }
}

我发现的是年龄已经成功读取,但是姓名没有。这是输入和输出的内容:
Input:

"10"
"Mr. Whiskers"

Output:

"My cat is 10 years old and their name is "
为什么输出中省略了名字?我已经提供了正确的输入,但代码却不知道它。为什么会发生这种情况?

1
我相信 std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state) 也应该可以正常工作。 (除了下面的答案之外). - jww
5个回答

178

为什么会发生这种情况?

这与您提供的输入本身几乎没有关系,而更多地与std::getline()的默认行为有关。当您为年龄提供输入(std::cin >> age)时,您不仅提交了以下字符,而且在您键入Enter时,还会将一个隐式换行符附加到流中:

"10\n"
当你从终端提交时,选择 EnterReturn 键时,输入的末尾将始终添加一个换行符。在文件中,它也用于移动到下一行。换行符会被留在缓冲区中,直到下一个 I/O 操作,然后被丢弃或读取。当控制流到达 std::getline() 时,它将看到 "\nMr. Whiskers",并且开头的换行符将被丢弃,但是输入操作将立即停止。这种情况发生的原因是因为 std::getline() 的作用是尝试读取字符,并在找到换行符时停止。所以剩余的输入都留在了未读的缓冲区中。

解决方法

cin.ignore()

为了解决这个问题,其中一种方案就是在进行 std::getline() 前跳过换行符。你可以在第一个输入操作后调用 std::cin.ignore()。它将丢弃下一个字符(换行符),以便它不再妨碍后面的操作。
std::cin >> age;
std::cin.ignore();
std::getline(std::cin, name);

assert(std::cin); 
// Success!

std::ws

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

std::cin >> age;
std::getline(std::cin >> std::ws, name);

assert(std::cin);
// Success!

std::cin >> std::ws表达式在std::getline()调用之前执行(并且在std::cin >> age调用之后执行),以便删除换行符。

不同之处在于ignore()仅丢弃1个字符(或在给定参数时丢弃N个字符),而std::ws会继续忽略空格,直到找到非空格字符为止。因此,如果您不知道下一个标记之前将有多少空格,请考虑使用它。

匹配操作

当您遇到这样的问题时,通常是因为您将格式化输入操作与非格式化输入操作组合在一起。格式化输入操作是指将输入格式化为特定类型的操作。这就是operator>>()的作用。非格式化输入操作是除此之外的任何操作,如std::getline()std::cin.read()std::cin.get()等。这些函数不关心输入的格式,只处理原始文本。

如果您坚持使用单一类型的格式,则可以避免这个烦人的问题:

// Unformatted I/O
std::string age, name;
std::getline(std::cin, age);
std::getline(std::cin, name);
或者
// Formatted I/O
int age;
std::string firstName, lastName;
std::cin >> age >> firstName >> lastName;

如果您选择使用未格式化操作将所有内容读取为字符串,那么您可以在之后将它们转换为相应的类型。


1
为什么不直接使用 if (getline(std::cin, name) && getline(std::cin, state)) 呢? - Fred Larson
1
当然,在这里并非如此,重复做同一件事情是没有意义的。对于整数类型,您可以将行读入字符串,然后使用std::stoi()进行转换,但是这样会使得优点不那么明显。但我倾向于只使用std::getline()进行面向行的输入,然后根据需要以合适的方式解析该行。我认为这样更少出错。 - Fred Larson
如果我有一个程序,首先输入一个数字X,然后运行一个循环,输入字符串X次 - 我该怎么做?我应该在循环外编写第一次迭代代码,并使用cin.ignore(),然后运行X-1次循环吗? - Albin
2
@Albin 如果你想捕获到给定分隔符之前的所有字符并将其输入到一个字符串中,那么你可能需要使用 std::getline()。默认情况下,分隔符是换行符。如果这些 X 个字符串只是单词/标记,那么可以很容易地使用 >> 完成此任务。否则,你需要使用 >> 将第一个数字输入到整数中,然后在下一行上调用 cin.ignore(),然后运行一个循环,在其中使用 getline() - David G
难以置信,这么多赞。没人听说过“std::ws”吗?为什么不直接写“std::getline(std::cin>>std::ws, name);”呢?这里到底发生了什么? - A M
显示剩余4条评论

14

如果按照以下方式更改您的初始代码,一切都会没问题:

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

3
谢谢你。这也会起作用,因为 get() 会消耗下一个字符。 我之前在我的答案中还建议了 (std::cin >> name).ignore() - David G
"因为get()函数的原因..." 是的,没错。抱歉没有提供详细信息就直接给出答案。 - Boris
4
为什么不直接使用 if (getline(std::cin, name) && getline(std::cin, state)) - Fred Larson

4
这是因为从终端输入的所有用户输入都附加了一个隐式换行符,也称为换行符\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"

2

由于以上所有人都已经回答了输入10\nMr Whisker\n的问题,我想提供一种不同的方法。以上所有的解决方案都发布了如果缓冲区像10\nMr Whisker\n这样的代码。但是,如果我们不知道用户在输入时会表现出什么行为呢?用户可能会错误地输入10\n\nMr. Whisker\n10 \n\n Mr. whisker\n。在这种情况下,上面的代码可能无法工作。因此,我使用以下函数来获取字符串输入以解决问题。

string StringInput()  //returns null-terminated string
{
    string input;
    getline(cin, input);
    while(input.length()==0)//keep taking input as long as valid string is taken
    {
        getline(cin, input);
    }
    return input.c_str();
}

因此,答案是:
#include <iostream>
#include <string>

int main()
{
    int age;
    std::string name;

    std::cin >> age;
    name = StringInput();
    
    std::cout << "My cat is " << age << " years old and it's name is " << name << std::endl;
    
}

额外信息:

如果用户输入 a \n10\n \nmr. whiskey; 为了检查int输入是否有效,可以使用此函数来检查int输入(如果输入的是char而不是int,程序将具有未定义的行为):


//instead of "std::cin>>age;" use "get_untill_int(&age);" in main function.
void get_Untill_Int(int* pInput)//keep taking input until input is `int or float`
{
    cin>> *pInput;
    /*-----------check input validity----------------*/
    while (!cin) 
    {
        cin.clear();
        cin.ignore(100, '\n');
        cout<<"Invalid Input Type.\nEnter again: ";
        cin >>*pInput;
    }
    /*-----------checked input validity-------------*/
}

2

我很好奇。C++有一个专门用于消除任何剩余或其他空格的函数,它被称为std::ws。然后,您可以简单地使用。

std::getline(std::cin >> std::ws, name);

那应该是惯用的方法。对于格式化到未格式化输入之间的每个转换,都应使用该方法。
如果我们不讨论空格,而是在期望数字的位置输入字母,那么我们应该遵循 CPP 参考,并使用 .ignore(std::numeric_limits<std::streamsize>::max(), '\n'); 来消除错误的内容。
请参阅此处

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接