凡带用户界面的程序几乎都要处理用户输入。迄今我们编写的示例均使用 std::cin
向用户索取文本输入。由于文本输入形式极其自由(用户可键入任何内容),极易出现不符合预期的情况。
在编码过程中,应始终考虑用户(有意或无意)滥用程序的各种可能。优秀的程序能够预见这些滥用方式,并优雅地处理或事先避免(若可行)。妥善应对错误输入的程序被称为“健壮”程序。
本课专门探讨用户通过 std::cin
输入无效文本的情形,并演示多种处理手段。
std::cin
与 operator>>
行为回顾
在讨论其失败场景前,先回顾其工作流程(参见 —— iostream 简介)。
operator>>
输入流程(简化):
- 丢弃前导空白字符(空格、制表符、换行符)。
- 若缓冲区为空,则等待用户输入,并再次丢弃前导空白。
- 连续提取字符,直至遇到换行或不符合目标类型的字符为止。
提取结果:
- 若有字符被成功提取,则转换并赋值给变量;
- 若无字符可提取,则提取失败,变量被置 0(C++11 起),且后续提取立即失败(直至
std::cin
被清除)。
输入验证
检查用户输入是否符合预期称为“输入验证”。实现方式有三:
- 实时验证(边输入边校验):阻止无效字符录入。
- 事后验证(输入完成后):
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”,干扰后续输入。
无效文本输入的四种类型
- 提取成功但语义无效(如运算符输入 ‘k’)。
- 提取成功但含多余字符(如 “*q hello”)。
- 提取失败(如向数字变量输入 ‘q’)。
- 提取成功但数值溢出。
为增强健壮性,每处输入都应评估上述四种可能,并编写对应处理代码。下文逐一说明。
错误场景 1:提取成功但语义无效
用户输入 ‘k’ 而非 ‘+’、‘-’、‘*’、‘/’。程序输出:
5 k 7 is
解决:输入验证三步法:
- 检查输入是否符合预期;
- 符合则返回;
- 不符则提示并重新输入。
更新后的 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
进入失败状态;- 后续提取一律静默失败。
恢复步骤:
- 检测失败(
!std::cin
); std::cin.clear()
恢复状态;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';
}
最后,用循环要求用户重新输入无效数据。
作者注:输入验证重要且实用,但常使示例复杂。后续课程若无特别需求,一般不再展示验证代码。