9-5 std::cin 与处理无效输入
大多数具有某种用户界面的程序都需要处理用户输入。在您编写的程序中,一直使用 std::cin 请求用户输入文本。由于文本输入形式自由(用户可输入任意内容),用户极易输入出预料之外的数据。
编写程序时,你应始终考虑用户可能(有意或无意)如何误用程序。优秀的程序会预判用户误用场景,并通过优雅处理异常情况或(在可能时)从源头阻止异常发生。能妥善处理错误情况的程序被称为健壮程序robust。
本节课将重点探讨用户通过 std::cin 输入无效文本的各种方式,并展示不同的处理方案。
在讨论 std::cin 和 operator>> 可能出现的故障前,让我们回顾其工作原理。相关知识已在第 1.5 课——iostream 介绍:cout、cin 和 endl中讲解过。
以下是运算符operator>>在输入操作中工作原理的简化视图:
- 首先,输入缓冲区中的前导空白字符(缓冲区开头的空格、制表符和换行符)会被丢弃。这将清除任何来自前一行输入的未提取换行符。
- 若此时输入缓冲区为空,operator>>将等待用户输入更多数据。此时又会再次丢弃前导空格。
- 随后operator>>会尽可能提取连续字符,直至遇到换行符(表示输入行结束)或遇到无法赋值给当前变量的无效字符。
提取结果如下:
- 若在上文步骤3中提取到任何字符,则提取成功。提取的字符将转换为数值并赋值给变量。
- 若在上文步骤3中未能提取任何字符,则提取失败。目标对象将被赋值为0(自C++11起),且后续所有提取操作将立即失败(直至std::cin被清空)。
输入验证
检查用户输入是否符合程序预期的过程称为输入验证。
输入验证有三种基本方式:
内联验证(用户输入时实时验证):
- 首先阻止用户输入无效内容。
输入后处理(用户输入完成后):
- 允许用户将任意内容输入字符串,随后验证字符串是否正确,若正确则将字符串转换为最终变量格式。
- 允许用户自由输入内容,由 std::cin 和 operator>> 尝试提取数据,并处理错误情况。
某些图形用户界面和高级文本界面允许在用户输入时(逐字符)进行验证。通常,程序员会提供一个验证函数,该函数接收用户当前输入的内容,若输入有效则返回true,否则返回false。每次用户按下键盘按键时都会调用此函数。若验证函数返回true,则接受用户刚按下的按键。若验证函数返回false,则用户刚输入的字符将被丢弃(且不会显示在屏幕上)。通过这种方式,可确保用户输入的任何内容都绝对有效,因为所有无效键击都会被立即发现并丢弃。遗憾的是,std::cin不支持此类验证方式。
由于字符串对输入字符无限制,提取操作必然成功(但需注意 std::cin 会在遇到首个非首字符空格时停止提取)。字符串输入完成后,程序可解析字符串以验证其有效性。然而字符串解析及转换(如转换为数字类型)存在挑战性,因此仅在特殊情况下采用此方法。
最常见的做法是让 std::cin 和提取运算符承担繁重工作。该方法允许用户自由输入内容,由 std::cin 和 operator>> 尝试提取有效数据,并在提取失败时处理后续问题。这是最简便的方法,下文将对此进行详细探讨。
一个示例程序
考虑以下没有错误处理的计算器程序:
#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;
}
这个简单的程序要求用户输入两个数字和一个数学运算符。

现在,考虑无效用户输入可能导致程序崩溃的情况。
首先,我们要求用户输入数字。若用户输入非数字内容(如‘q’),则提取操作将失败。
其次,我们要求用户输入四个可能符号中的一个。若用户输入的字符不在预期符号范围内?虽然能提取输入内容,但当前程序未处理后续情况。
第三,若用户输入类似“q hello”的字符串,虽然能提取所需的''字符,但缓冲区中残留的额外输入可能引发后续问题。
无效文本输入类型
输入文本错误通常可分为四类:
- 输入提取成功但输入对程序毫无意义(例如将字母‘k’作为数学运算符输入)。
- 输入提取成功但用户输入了额外内容(例如将字符串‘*q hello’作为数学运算符输入)。
- 提取失败(例如在数值输入框中输入字母'q')。
- 提取成功但用户输入导致数值溢出。
因此,为增强程序健壮性,每次请求用户输入时,我们应预判上述情况是否可能发生,并编写相应处理代码。
下面我们将深入探讨每种情况,并说明如何使用 std::cin 进行处理。
错误情况1:提取成功但输入无意义
这是最简单的案例。考虑上述程序的以下执行过程:
Enter a decimal number: 5
Enter one of the following: +, -, *, or /: k
Enter a decimal number: 7
在此情况下,我们要求用户输入四个符号中的任意一个,但用户却输入了字母‘k’。由于‘k’是有效字符,std::cin顺利将其提取到变量op中,并返回给main函数。但程序并未预料到这种情况,因此未能正确处理该情形。结果输出如下:
5 k 7 is
解决方案很简单:进行输入验证。这通常包括三个步骤:
- 检查用户的输入是否符合预期。
- 若符合,将值返回给调用方。
- 若不符合,告知用户操作失败并请其重试。
以下是经过更新的 getOperator() 函数,该函数实现了输入验证功能。
char getOperator()
{
while (true) // Loop until user enters a valid input
{
std::cout << "Enter one of the following: +, -, *, or /: ";
char operation{};
std::cin >> operation;
// Check whether the user entered meaningful input
switch (operation)
{
case '+':
case '-':
case '*':
case '/':
return operation; // return it to the caller
default: // otherwise tell the user what went wrong
std::cout << "Oops, that input is invalid. Please try again.\n";
}
} // and try again
}
如您所见,我们使用了一个while循环,持续运行直至用户提供有效输入。若未提供有效输入,我们将要求用户重新尝试,直到他们提供有效输入、关闭程序,或是彻底搞垮他们的电脑。
错误情况2:提取成功但包含多余输入
考虑上述程序的以下执行情况:
Enter a decimal number: 5*7
你觉得接下来会发生什么?
Enter a decimal number: 5*7
Enter one of the following: +, -, *, or /: Enter a decimal number: 5 * 7 is 35
程序输出正确答案,但格式完全混乱。让我们深入探究原因。
当用户输入57时,该输入进入缓冲区。随后运算符>>将5提取至变量x,缓冲区中剩余7\n。接着程序输出“请输入以下运算符之一:+, -, * 或 /:”。然而当提取运算符被调用时,它发现缓冲区中正等待提取的7\n,于是直接使用该内容而非再次请求用户输入。结果它提取了‘’字符,使缓冲区中剩余7\n。
在要求用户输入另一个十进制数后,缓冲区中的7被直接提取而未再次询问用户。由于用户从未获得输入额外数据并按回车(产生换行符)的机会,所有输出提示语最终挤在同一行显示。
尽管上述程序能运行,但执行过程混乱不堪。若能直接忽略任何多余字符输入会更理想。所幸忽略字符操作非常简单:
std::cin.ignore(100, '\n'); // clear up to 100 characters out of the buffer, or until a '\n' character is removed
此调用最多可删除100个字符,但若用户输入超过100个字符,输出仍会混乱。要忽略直至下一个‘\n’的所有字符,可将std::numeric_limitsstd::streamsize::max()传递给std::cin.ignore()。该值表示std::streamsize类型变量能存储的最大数值。将此值传递给 std::cin.ignore() 可使其禁用计数检查。
要忽略直至下一个 ‘\n’ 字符(含该字符)的所有内容,我们调用
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
由于这行代码实现的功能相对简单,却相当冗长,因此将其封装成函数会更方便,这样就能直接调用该函数替代 std::cin.ignore()。
#include <limits> // for std::numeric_limits
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
由于用户输入的最后一个字符通常是‘\n’,我们可以让 std::cin 忽略缓冲区中的字符,直到遇到换行符(该换行符也会被移除)。
现在更新 getDouble() 函数以忽略任何多余输入:
double getDouble()
{
std::cout << "Enter a decimal number: ";
double x{};
std::cin >> x;
ignoreLine();
return x;
}
现在我们的程序将按预期运行,即使首次输入为5*7——程序会提取数字5,并将剩余字符从输入缓冲区移除。由于输入缓冲区现已清空,下次执行提取操作时系统将正确提示用户输入!
提示
在某些情况下,将无效输入视为错误情况(而非直接忽略)可能更为妥当。此时我们可以要求用户重新输入数据。
以下是 getDouble() 函数的变体版本,当检测到无效输入时会提示用户重新输入:
// returns true if std::cin has unextracted input on the current line, false otherwise bool hasUnextractedInput() { return !std::cin.eof() && std::cin.peek() != '\n'; } double getDouble() { while (true) // Loop until user enters a valid input { std::cout << "Enter a decimal number: "; double x{}; std::cin >> x; // NOTE: YOU SHOULD CHECK FOR A FAILED EXTRACTION HERE (see section below) // If there is extraneous input, treat as failure case if (hasUnextractedInput()) { ignoreLine(); // remove extraneous input continue; } return x; } }上述代码片段使用了两个我们尚未见过的函数:
- std::cin.eof() 函数在最后一次输入操作(本例中为向量 x 的提取操作)到达输入流末尾时返回 true。
- std::cin.peek() 函数允许我们查看输入流中的下一个字符而不提取它。
该函数的工作原理如下:当用户输入被提取到变量x后,std::cin中可能还存在未提取的字符。
首先调用std::cin.eof()判断提取操作是否已到达输入流末尾。若返回true,则表示所有字符均已成功提取。
否则,std::cin 中必然存在待提取的额外字符。此时调用 std::cin.peek() 预览下一个待提取字符(不实际提取)。若该字符为 ‘\n’,则表示当前行所有字符均已提取至 x,同样属于成功情况。
然而,若下一个字符不是 ‘\n’,则说明用户输入了未被提取到 x 中的冗余内容。这是失败情况。此时需清除所有冗余输入,并返回循环顶部重新尝试。
若难以理解hasUnextractedInput()中布尔表达式的评估逻辑,实属正常——含否定形式的布尔表达式往往难以理解。此时运用德摩根定律可助解惑。等效表达式为 return !(std::cin.eof() || std::cin.peek() == ‘\n’);。这更清晰地表明我们正在检测文件结束标志或换行符。若任一条件为真,则表示已提取全部输入。随后通过运算符!判断是否存在未提取输入,即是否仍有未处理的输入数据。
错误情况 3:提取失败
当无法将任何输入提取到指定变量时,提取操作将失败。
现在考虑我们更新后的计算器程序的以下执行过程:
Enter a decimal number: a
程序未能按预期运行本不该令人惊讶,但其失败的方式却耐人寻味:
Enter a decimal number: a
Enter one of the following: +, -, *, or /: Oops, that input is invalid. Please try again.
Enter one of the following: +, -, *, or /: Oops, that input is invalid. Please try again.
Enter one of the following: +, -, *, or /: Oops, that input is invalid. Please try again.
最后一行会持续打印直至程序关闭。
这看似与多余输入的情况相似,但存在细微差别。让我们深入分析:
当用户输入'a'时,该字符会被放入缓冲区。随后operator>>运算符试图将'a'提取至双精度变量x中。由于'a'无法转换为double类型,operator>>操作符无法完成提取。此时发生两件事:'a'保留在缓冲区中,且std::cin进入“失败模式”。
一旦进入“失败模式”,后续的输入提取请求将悄然失败。因此在计算器程序中,输出提示仍会显示,但后续提取请求均被忽略。这意味着系统不再等待用户输入运算符,而是跳过输入提示,导致程序陷入无限循环——因为无法进入任何有效运算状态。
要使 std::cin 恢复正常工作,通常需要执行三项操作:
- 检测到先前提取操作失败。
- 将 std::cin 恢复至正常操作模式。
- 移除导致失败的输入内容(以避免后续提取请求以相同方式失败)。
具体效果如下:
if (std::cin.fail()) // If the previous extraction failed
{
// Let's handle the failure
std::cin.clear(); // Put us back in 'normal' operation mode
ignoreLine(); // And remove the bad input
}
由于 std::cin 具有布尔转换,可指示上次输入是否成功,因此将上述代码改写为以下形式更符合惯例:
if (!std::cin) // If the previous extraction failed
{
// Let's handle the failure
std::cin.clear(); // Put us back in 'normal' operation mode
ignoreLine(); // And remove the bad input
}
关键洞察
一旦提取失败,后续的所有输入提取请求(包括调用 ignore())都将静默失败,直至调用 clear() 函数。因此,在检测到提取失败后,通常应首先调用 clear()。
让我们将它整合到我们的getDouble()函数中:
double getDouble()
{
while (true) // Loop until user enters a valid input
{
std::cout << "Enter a decimal number: ";
double x{};
std::cin >> x;
if (!std::cin) // If the previous extraction failed
{
// Let's handle the failure
std::cin.clear(); // Put us back in 'normal' operation mode
ignoreLine(); // And remove the bad input
continue;
}
// Our extraction succeeded
ignoreLine(); // Ignore any additional input on this line
return x; // Return the value we extracted
}
}
对于基本类型,若因输入无效导致提取失败,则该变量将被赋值为0(或0在该变量类型中转换后的值)。
即使提取未失败,调用clear()也无妨——它不会执行任何操作。当我们无论成功与否都将调用ignoreLine()时,本质上可将两种情况合并处理:
double getDouble()
{
while (true) // Loop until user enters a valid input
{
std::cout << "Enter a decimal number: ";
double x{};
std::cin >> x;
bool success { std::cin }; // Remember whether we had a successful extraction
std::cin.clear(); // Put us back in 'normal' operation mode (in case we failed)
ignoreLine(); // Ignore any additional input on this line (regardless)
if (success) // If we actually extracted a value
return x; // Return it (otherwise, we go back to top of loop)
}
}
检查文件结束标志
还有一种情况需要处理。
文件结束(EOF)是一种特殊错误状态,表示“无可用数据”。通常在输入操作因数据耗尽失败后after触发。例如,当你读取磁盘文件内容时,若在到达文件末尾后仍尝试读取更多数据,系统就会生成EOF来提示数据已耗尽。对于文件输入而言,这不成问题——我们只需关闭文件继续处理即可。
现在考虑 std::cin。当尝试从 std::cin 提取输入却无数据时,其设计不会触发 EOF,而是等待用户输入更多内容。但std::cin在特定情况下仍会生成EOF,最常见的是用户输入操作系统的特殊组合键。Unix系统(通过Ctrl-D)和Windows系统(通过Ctrl-Z + ENTER)均支持从键盘输入“EOF字符”。
关键洞察
在C++中,EOF是一种错误状态,而非字符。不同操作系统会将特定字符组合视为“用户输入的EOF请求”,这些组合有时被称为“EOF字符”。
当向 std::cin 提取数据时,若用户输入了 EOF 字符,其行为取决于操作系统。通常会发生以下情况:
- 如果文件结束符不是输入的首个字符:所有在文件结束符之前的输入将被清除,且文件结束符将被忽略。在 Windows 系统中,除换行符外,文件结束符之后输入的任何字符均被忽略。
- 如果文件结束符是输入的首个字符:将设置文件结束符错误。输入流可能(也可能不会)被断开连接。
尽管std::cin.clear()能清除文件结束错误,但若输入流断开连接,下一次输入请求仍会引发新的文件结束错误。当输入操作位于while(true)循环内部时,这将导致程序陷入无限循环的文件结束错误漩涡。
鉴于键盘输入的 EOF 字符旨在终止输入流,最佳做法是检测 EOF(通过 std::cin.eof())后终止程序。
由于清除失败的输入流是需要频繁检查的操作,这非常适合封装为可复用函数:
#include <limits> // for std::numeric_limits
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
// returns true if extraction failed, false otherwise
bool clearFailedExtraction()
{
// Check for failed extraction
if (!std::cin) // If the previous extraction failed
{
if (std::cin.eof()) // If the user entered an EOF
{
std::exit(0); // Shut down the program now
}
// Let's handle the failure
std::cin.clear(); // Put us back in 'normal' operation mode
ignoreLine(); // And remove the bad input
return true;
}
return false;
}
错误情况4:提取成功但用户导致数值溢出
考虑以下简单示例:
#include <cstdint>
#include <iostream>
int main()
{
std::int16_t x{}; // x is 16 bits, holds from -32768 to 32767
std::cout << "Enter a number between -32768 and 32767: ";
std::cin >> x;
std::int16_t y{}; // y is 16 bits, holds from -32768 to 32767
std::cout << "Enter another number between -32768 and 32767: ";
std::cin >> y;
std::cout << "The sum is: " << x + y << '\n';
return 0;
}
如果用户输入的数字过大(例如40000),会发生什么情况?
Enter a number between -32768 and 32767: 40000
Enter another number between -32768 and 32767: The sum is: 32767
在上述情况下,std::cin 立即进入“失败模式”,但同时将最接近的有效值赋给变量。当输入值大于该类型可能的最大值时,最接近的有效值即为该类型的最大值。因此,x 被赋予了 32767 的值。后续输入将被跳过,y 保持初始化值 0。此类错误可按提取失败的方式处理。
整合所有内容
以下是我们示例计算器,已更新并添加了若干额外的错误检查功能:
#include <cstdlib> // for std::exit
#include <iostream>
#include <limits> // for std::numeric_limits
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
// returns true if extraction failed, false otherwise
bool clearFailedExtraction()
{
// Check for failed extraction
if (!std::cin) // If the previous extraction failed
{
if (std::cin.eof()) // If the stream was closed
{
std::exit(0); // Shut down the program now
}
// Let's handle the failure
std::cin.clear(); // Put us back in 'normal' operation mode
ignoreLine(); // And remove the bad input
return true;
}
return false;
}
double getDouble()
{
while (true) // Loop until user enters a valid input
{
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(); // Remove any extraneous input
return x; // Return the value we extracted
}
}
char getOperator()
{
while (true) // Loop until user enters a valid input
{
std::cout << "Enter one of the following: +, -, *, or /: ";
char operation{};
std::cin >> operation;
if (!clearFailedExtraction()) // we'll handle error messaging if extraction failed below
ignoreLine(); // remove any extraneous input (only if extraction succeded)
// Check whether the user entered meaningful input
switch (operation)
{
case '+':
case '-':
case '*':
case '/':
return operation; // Return the entered char to the caller
default: // Otherwise tell the user what went wrong
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 << '\n';
return;
case '-':
std::cout << x - y << '\n';
return;
case '*':
std::cout << x * y << '\n';
return;
case '/':
if (y == 0.0)
break;
std::cout << x / y << '\n';
return;
}
std::cout << "???"; // Being robust means handling unexpected parameters as well, even though getOperator() guarantees operation is valid in this particular program
}
int main()
{
double x{ getDouble() };
char operation{ getOperator() };
double y{ getDouble() };
// Handle division by 0
while (operation == '/' && y == 0.0)
{
std::cout << "The denominator cannot be zero. Try again.\n";
y = getDouble();
}
printResult(x, operation, y);
return 0;
}
结论
编写程序时,请考虑用户可能如何误用程序,尤其在文本输入方面。针对每个文本输入点,需思考:
- 提取操作是否可能失败?
- 用户输入是否可能超出预期?
- 用户是否可能输入无意义内容?
- 用户是否可能导致输入溢出?
您可以使用if语句和布尔逻辑来检测输入是否符合预期且有意义。
以下代码将清除所有多余输入:
#include <limits> // for std::numeric_limits
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
以下代码将检测并修复提取失败或溢出情况(同时移除多余输入):
// returns true if extraction failed, false otherwise
bool clearFailedExtraction()
{
// Check for failed extraction
if (!std::cin) // If the previous extraction failed
{
if (std::cin.eof()) // If the stream was closed
{
std::exit(0); // Shut down the program now
}
// Let's handle the failure
std::cin.clear(); // Put us back in 'normal' operation mode
ignoreLine(); // And remove the bad input
return true;
}
return false;
}
我们可以测试是否存在未提取的输入(除换行符外),具体方法如下:
// returns true if std::cin has unextracted input on the current line, false otherwise
bool hasUnextractedInput()
{
return !std::cin.eof() && std::cin.peek() != '\n';
}
最后,使用循环让用户重新输入,如果原始输入无效的话。
作者注
输入验证很重要且很有用,但它往往会使示例更复杂、更难理解。因此,在后续课程中,除非涉及到需要讲解的内容,否则我们通常不会进行任何形式的输入验证。

浙公网安备 33010602011771号