std::cin输入处理与无效输入应对指南

凡带用户界面的程序几乎都要处理用户输入。迄今我们编写的示例均使用 std::cin 向用户索取文本输入。由于文本输入形式极其自由(用户可键入任何内容),极易出现不符合预期的情况。

在编码过程中,应始终考虑用户(有意或无意)滥用程序的各种可能。优秀的程序能够预见这些滥用方式,并优雅地处理或事先避免(若可行)。妥善应对错误输入的程序被称为“健壮”程序。

本课专门探讨用户通过 std::cin 输入无效文本的情形,并演示多种处理手段。

std::cinoperator>> 行为回顾

在讨论其失败场景前,先回顾其工作流程(参见 —— iostream 简介)。

operator>> 输入流程(简化):

  1. 丢弃前导空白字符(空格、制表符、换行符)。
  2. 若缓冲区为空,则等待用户输入,并再次丢弃前导空白。
  3. 连续提取字符,直至遇到换行或不符合目标类型的字符为止。

提取结果:

  • 若有字符被成功提取,则转换并赋值给变量;
  • 若无字符可提取,则提取失败,变量被置 0(C++11 起),且后续提取立即失败(直至 std::cin 被清除)。

输入验证

检查用户输入是否符合预期称为“输入验证”。实现方式有三:

  1. 实时验证(边输入边校验):阻止无效字符录入。
  2. 事后验证(输入完成后):
    a. 将整行读入字符串,校验后再转换为目标类型;
    b. 直接让 std::cin 尝试提取,失败后再处理。

图形界面或高级文本界面可逐字符实时验证;std::cin 不支持此方式。由于字符串无字符限制,提取必然成功(遇空白即停),之后再解析即可,但解析繁琐,故较少采用。

通常采用第三种方式:允许用户随意输入,由 std::cin 尝试提取,失败时再处理。下文重点讨论此法。

示例程序

以下计算器示例无任何错误处理:

#include <iostream>

double getDouble()
{
    std::cout << "Enter a decimal number: ";
    double x{};
    std::cin >> x;
    return x;
}

char getOperator()
{
    std::cout << "Enter one of the following: +, -, *, or /: ";
    char op{};
    std::cin >> op;
    return op;
}

void printResult(double x, char operation, double y)
{
    std::cout << x << ' ' << operation << ' ' << y << " is ";

    switch (operation)
    {
    case '+': std::cout << x + y << '\n'; return;
    case '-': std::cout << x - y << '\n'; return;
    case '*': std::cout << x * y << '\n'; return;
    case '/': std::cout << x / y << '\n'; return;
    }
}

int main()
{
    double x{ getDouble() };
    char operation{ getOperator() };
    double y{ getDouble() };

    printResult(x, operation, y);
    return 0;
}

正常运行:

Enter a decimal number: 5
Enter one of the following: +, -, *, or /: *
Enter a decimal number: 7
5 * 7 is 35

考虑何处会因无效输入崩溃:

  • 用户输入非数字(如 ‘q’)→ 提取失败;
  • 用户输入非指定运算符 → 提取成功但无意义;
  • 用户输入 “q hello” → 提取 ‘’ 后,缓冲区残留 “q hello\n”,干扰后续输入。

无效文本输入的四种类型

  1. 提取成功但语义无效(如运算符输入 ‘k’)。
  2. 提取成功但含多余字符(如 “*q hello”)。
  3. 提取失败(如向数字变量输入 ‘q’)。
  4. 提取成功但数值溢出。

为增强健壮性,每处输入都应评估上述四种可能,并编写对应处理代码。下文逐一说明。

错误场景 1:提取成功但语义无效

用户输入 ‘k’ 而非 ‘+’、‘-’、‘*’、‘/’。程序输出:

5 k 7 is

解决:输入验证三步法:

  1. 检查输入是否符合预期;
  2. 符合则返回;
  3. 不符则提示并重新输入。

更新后的 getOperator()

char getOperator()
{
    while (true)
    {
        std::cout << "Enter one of the following: +, -, *, or /: ";
        char operation{};
        std::cin >> operation;

        switch (operation)
        {
        case '+': case '-': case '*': case '/':
            return operation;
        default:
            std::cout << "Oops, that input is invalid. Please try again.\n";
        }
    }
}

错误场景 2:提取成功但含多余字符

用户输入 5*7

  • 5 被提取至 x,缓冲区残留 *7\n
  • 后续提取运算符时直接取出 * 而未再次提示;
  • 最终提示与输出混杂一行。

解决:忽略多余字符。标准做法:

std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

封装为函数:

#include <limits>

void ignoreLine()
{
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

更新 getDouble()

double getDouble()
{
    std::cout << "Enter a decimal number: ";
    double x{};
    std::cin >> x;
    ignoreLine();
    return x;
}

若需将多余字符视为失败而非忽略,可检测缓冲区是否仍有字符并让用户重输,示例略。

错误场景 3:提取失败

用户输入非数字:

Enter a decimal number: a
Enter one of the following: +, -, *, or /: Oops, that input is invalid. Please try again.
...(无限循环)

失败时:

  • 无效字符留在缓冲区;
  • std::cin 进入失败状态;
  • 后续提取一律静默失败。

恢复步骤:

  1. 检测失败(!std::cin);
  2. std::cin.clear() 恢复状态;
  3. ignoreLine() 清除无效字符。

封装函数:

bool clearFailedExtraction()
{
    if (!std::cin)
    {
        if (std::cin.eof()) std::exit(0);
        std::cin.clear();
        ignoreLine();
        return true;
    }
    return false;
}

更新 getDouble()

double getDouble()
{
    while (true)
    {
        std::cout << "Enter a decimal number: ";
        double x{};
        std::cin >> x;

        if (clearFailedExtraction())
        {
            std::cout << "Oops, that input is invalid. Please try again.\n";
            continue;
        }

        ignoreLine();
        return x;
    }
}

错误场景 4:提取成功但数值溢出

示例:

std::int16_t x{};
std::cin >> x;

若用户输入 40000(超出范围),std::cin 置失败位,并赋最接近的合法值(32767)。处理方式同场景 3。

完整示例

综合以上措施后的完整计算器:

#include <cstdlib>
#include <iostream>
#include <limits>

void ignoreLine()
{
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

bool clearFailedExtraction()
{
    if (!std::cin)
    {
        if (std::cin.eof()) std::exit(0);
        std::cin.clear();
        ignoreLine();
        return true;
    }
    return false;
}

double getDouble()
{
    while (true)
    {
        std::cout << "Enter a decimal number: ";
        double x{};
        std::cin >> x;

        if (clearFailedExtraction())
        {
            std::cout << "Oops, that input is invalid. Please try again.\n";
            continue;
        }
        ignoreLine();
        return x;
    }
}

char getOperator()
{
    while (true)
    {
        std::cout << "Enter one of the following: +, -, *, or /: ";
        char operation{};
        std::cin >> operation;

        if (!clearFailedExtraction())
            ignoreLine();

        switch (operation)
        {
        case '+': case '-': case '*': case '/':
            return operation;
        default:
            std::cout << "Oops, that input is invalid. Please try again.\n";
        }
    }
}

void printResult(double x, char operation, double y)
{
    std::cout << x << ' ' << operation << ' ' << y << " is ";
    switch (operation)
    {
    case '+': std::cout << x + y; break;
    case '-': std::cout << x - y; break;
    case '*': std::cout << x * y; break;
    case '/': std::cout << x / y; break;
    default: std::cout << "???";
    }
    std::cout << '\n';
}

int main()
{
    double x{ getDouble() };
    char operation{ getOperator() };
    double y{ getDouble() };

    while (operation == '/' && y == 0.0)
    {
        std::cout << "The denominator cannot be zero. Try again.\n";
        y = getDouble();
    }

    printResult(x, operation, y);
    return 0;
}

结论

编写程序时,应预判用户如何滥用输入,尤其是文本输入。每处输入都应评估:

  • 提取是否会失败?
  • 是否有多余字符?
  • 输入是否无意义?
  • 是否可能溢出?

使用条件判断与布尔逻辑即可检测并应对。

常用清理代码:

#include <limits>
void ignoreLine()
{
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

bool clearFailedExtraction()
{
    if (!std::cin)
    {
        if (std::cin.eof()) std::exit(0);
        std::cin.clear();
        ignoreLine();
        return true;
    }
    return false;
}

检测多余字符:

bool hasUnextractedInput()
{
    return !std::cin.eof() && std::cin.peek() != '\n';
}

最后,用循环要求用户重新输入无效数据。

作者注:输入验证重要且实用,但常使示例复杂。后续课程若无特别需求,一般不再展示验证代码。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"