在C++中,使用cin进行输入验证的最佳方法是什么?

8

我的兄弟最近开始学习C ++。他告诉我在尝试验证简单程序中的输入时遇到的问题。他有一个文本菜单,用户输入整数choice,如果他们输入无效的选项,将要求他们再次输入(do while循环)。但是,如果用户输入字符串而不是int,则代码将中断。 我阅读了stackoverflow上的各种问题,并告诉他按照以下方式重写代码:

#include<iostream>
using namespace std;

int main()
{
    int a;
    do
    {
    cout<<"\nEnter a number:"
    cin>>a;
        if(cin.fail())
        {
            //Clear the fail state.
            cin.clear();
            //Ignore the rest of the wrong user input, till the end of the line.
            cin.ignore(std::numeric_limits<std::streamsize>::max(),\
                                                    '\n');
        }
    }while(true);
    return 0;
}

虽然这种方法可以正常工作,但我还尝试了其他几个想法:
1. 使用try catch块。它没有起作用。我认为这是因为由于错误输入没有引发异常。 2. 我尝试了if(! cin){//Do Something}也不起作用。我还没弄清楚这个。 3. 第三,我尝试输入一个固定长度的字符串,然后解析它。我会使用atoi()。这符合标准并且可移植吗?或者应该编写自己的解析函数? 4. 如果编写一个使用cin的类,但动态执行此类错误检测,例如通过在运行时确定输入变量的类型,是否会有太多开销?这可能吗?
我想知道做这种检查的最佳方法是什么,有哪些最佳实践?
我想补充一点,虽然我不是新手编写C++代码,但我是新手编写好的符合标准的代码。我正在努力消除不良做法并学习正确的方法。如果答案者能给出详细的解释,我将不胜感激。
编辑:我看到litb回答了我的先前编辑之一。我将在此处发布该代码以供参考。
#include<iostream>
using namespace std;

int main()
{
    int a;
    bool inputCompletionFlag = true;
    do
    {
    cout<<"\nEnter a number:"
    cin>>a;
        if(cin.fail())
        {
            //Clear the fail state.
            cin.clear();
            //Ignore the rest of the wrong user input, till the end of the line.
            cin.ignore(std::numeric_limits<std::streamsize>::max(),\
                                                    '\n');
        }
        else
        {
            inputCompletionFlag = false;
        }
    }while(!inputCompletionFlag);
    return 0;
}

这段代码在输入类似于"1asdsdf"的情况下会失败。我不知道如何修复它,但litb发布了一个很好的答案。:)

8个回答

19

这里是一些代码,您可以使用它来确保您还拒绝像

42crap
如果数字后面有非数字字符,如果您读取整行,然后解析并适当执行操作,可能需要更改程序的工作方式。如果您的程序直到现在从不同的位置读取数字,则必须将其放入一个中央位置,解析一行输入,并决定采取的操作。但也许这也是一件好事-通过分离内容,您可以以这种方式增加代码的可读性:输入-处理-输出。
无论如何,以下是如何拒绝上述数字-非数字的方法。将一行读入字符串,然后使用stringstream进行解析:
std::string getline() {
  std::string str;
  std::getline(std::cin, str);
  return str;
}

int choice;
std::istringstream iss(getline());
iss >> choice >> std::ws;
if(iss.fail() || !iss.eof()) {
  // handle failure
}

它会吃掉所有尾随的空白字符。当它在读取整数或尾随空格时达到stringstream的文件末尾,它会设置eof位,然后我们检查这一点。如果一开始就无法读取任何整数,则fail或bad位将被设置。

此答案早期版本直接使用std::cin - 但是std::ws与连接到终端的std::cin不兼容(它将阻塞等待用户输入),因此我们使用stringstream来读取整数。


回答一些您的问题:

问题:1. 使用try catch块。它没有起作用。我认为这是因为由于错误输入而未引发异常。

答案: 好吧,你可以告诉流在读取内容时抛出异常。您可以使用istream::exceptions函数,告知要抛出哪种类型的错误:

iss.exceptions(ios_base::failbit);

我从未使用过它。如果你在 std::cin 上这样做,你将不得不记住恢复标志以供依赖于它不抛出异常的其他读取器使用。发现使用函数 fail, bad 来询问流的状态更容易。

问题:2. 我尝试了if(!cin){ //Do Something },但也没用。我还没有弄清楚这个问题。

回答:这可能是因为你给它的是像 "42crap" 这样的东西。对于流来说,在将其转换为整数时,这是完全有效的输入。

问题:3. 第三点,我尝试输入一个固定长度的字符串,然后解析它。我会使用 atoi()。这符合标准并且可移植吗?我应该编写自己的解析函数吗?

回答:atoi 符合标准。但是当你想检查错误时,它就不好了。与其他函数不同,它没有进行错误检查。如果你有一个字符串,并想检查它是否包含数字,则可以像上面的初始代码中那样做。

有一些类似 C 的函数可以直接从 C 字符串中读取。它们存在的目的是允许与旧的遗留代码交互和编写高性能代码。在程序中应避免使用它们,因为它们工作在相当低级别上,并需要使用裸指针。基于其本质,它们无法增强以与用户定义类型一起使用。具体来说,这谈论了函数 "strtol"(字符串到长整数),它基本上是带有错误检查和能够使用其他进制(例如十六进制)的 atoi。

问题:4. 如果我编写一个使用 cin 的类,但动态进行这种类型的错误检测,也许通过在运行时确定输入变量的类型,那么会有太多开销吗?这是可能的吗?

回答:通常情况下,你不需要过多关注这里的开销(如果你指的是运行时开销)。但具体取决于你在哪里使用该类。如果你正在编写需要处理输入并需要高吞吐量的高性能系统,那么这个问题将非常重要。但是,如果你需要从终端或文件中读取输入,你已经看到这意味着什么:等待用户输入需要花费很长时间,因此在这个规模上,你不需要再考虑运行时成本。

如果你指的是代码开销 - 那就取决于代码的实现方式。你需要扫描你读取的字符串 - 它是否包含数字或任意字符串。根据你想要扫描的内容(也许你有一个 "日期" 输入,或者一个 "时间" 输入格式。可以查看 boost.date_time),你的代码可能会变得极为复杂。对于像分类数字或非数字这样的简单任务,我认为你可以通过少量的代码来完成。


感谢litb的详细解释,但我查了一下发现Boost不是标准库。鉴于此,如果可以的话,自己编写代码会更好还是应该坚持使用Boost? - batbrat
我刚刚注意到在C++0x中它将被大量标准化,所以我会使用Boost。如果您能确认一下,我会很高兴的。谢谢。 - batbrat
是的,下一个C++将包括一些基于boost库设计的库(shared_ptr、thread、system(error_code等)、array、bind、function)。但不包括date_time。然而,强烈建议使用boost。它比自己编写要好得多。 - Johannes Schaub - litb
感谢您的回复,litb。我会选择Boost。 - batbrat

12

这是我在 C 中所做的,但很可能也适用于 C++。

将所有输入都作为字符串。

然后,仅在此之后将字符串解析为所需内容。 有时最好编写自己的代码而不是试图将别人的代码强行改造成自己需要的样子。


谢谢您回复,Pax。我之前也想过您所说的方法,但一直在思考是否这是正确的方法。非常感谢您的解答。 - batbrat

4
  • 为了获得与iostreams相关的异常情况,您需要为流设置正确的异常标志。
  • 我会使用get_line来获取整行输入,然后根据需要处理它 - 使用lexical_cast、正则表达式(例如Boost RegexBoost Xpressive)、使用Boost Spirit解析它,或者只是使用一些适当的逻辑。

谢谢您的回答。我想知道是否有标准库等同于这里提到的boost替代品。 - batbrat

3

我会采取双重措施:首先,尝试使用正则表达式验证输入并提取数据,如果输入不太简单的话。即使输入只是一系列数字,这也非常有帮助。

然后,我喜欢使用boost::lexical_cast,如果输入无法转换,它可以引发bad_lexical_cast异常。

在您的示例中:

std::string in_str;
cin >> in_str;

// optionally, test if it conforms to a regular expression, in case the input is complex

// Convert to int? this will throw bad_lexical_cast if cannot be converted.
int my_int = boost::lexical_cast<int>(in_str);

2

请不要直接在真正的代码中使用格式化输入(>> 运算符)。您始终需要使用 std::getline 或类似函数读取原始文本,然后使用自己编写的输入解析例程(可以使用 >> 运算符)对输入进行解析。


2

试试结合多种方法:

  1. 使用std::getline(std::cin, strObj)std::cin中获取输入,其中strObj是一个std::string对象。

  2. 使用boost::lexical_caststrObj转换为最大宽度的有符号或无符号整数(例如,unsigned long long或类似类型)。

  3. 使用boost::numeric_cast将整数转换为预期范围内的类型。

你也可以使用std::getline获取输入,并根据要捕获错误的位置调用boost::lexical_cast将其转换为相应的窄整型。三步法的好处在于接受任何整数数据,并单独捕获窄化错误。


1

我同意Pax的观点,最简单的方法是将所有内容都读取为字符串,然后使用TryParse验证输入。如果输入格式正确,则继续进行,否则通知用户并在循环中使用continue。


TryParse是一个特定于.NET的习语。基于可移植性和标准的C++无法使用它。 - Harper Shelby
我一直在想那个哈珀。谢谢你的澄清。感谢回答Rekreativc。 - batbrat

1

还有一件事情尚未提到,那就是在使用从流中获取的变量之前,测试cin >>操作是否成功通常很重要。

这个例子与你的类似,但进行了测试。

#include <iostream>
#include <limits>
using namespace std;
int main()
{
   while (true)
   {
      cout << "Enter a number: " << flush;
      int n;
      if (cin >> n)
      {
         // do something with n
         cout << "Got " << n << endl;
      }
      else
      {
         cout << "Error! Ignoring..." << endl;
         cin.clear();
         cin.ignore(numeric_limits<streamsize>::max(), '\n');
      }
   }
   return 0;
}

这将使用通常的 >> 运算符语义;它将首先跳过空格,然后尝试读取尽可能多的数字,然后停止。因此,“42crap”将给您 42,然后跳过“crap”。如果这不是您想要的,那么我同意之前的答案,您应该将其读入字符串,然后验证它(也许使用正则表达式 - 但这可能对于简单的数字序列来说有点过度杀伤力)。


这正是我想要的。谢谢。然而,上面的答案让我重新思考了我的做事方式,我对如何编写符合良好标准的代码有了更好的想法。将来我会同时采纳你和其他人的建议。 - batbrat

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