28-5 流状态和输入验证

流状态

ios_base 类包含多个状态标志,用于指示使用流时可能出现的各种情况

标志 意义
goodbit 一切都好
badbit 发生了某种致命错误(例如,程序试图读取文件末尾之外的内容)。
eofbit 流已到达文件末尾
failbit 发生了一个非致命错误(例如,用户输入了字母,而程序期望输入的是整数)。

虽然这些标志存在于 ios_base 中,但因为 ios 是从 ios_base 派生的,而且 ios 比 ios_base 需要的输入更少,所以它们通常是通过 ios 访问的(例如作为 std::ios::failbit)。

iOS 还提供了一系列成员函数,以便方便地访问这些状态:

成员函数 意义
good() 如果 goodbit 已设置(流正常),则返回 true。
bad() 如果设置了 badbit(发生致命错误),则返回 true。
eof() 如果设置了 eofbit(流位于文件末尾),则返回 true。
fail() 如果 failbit 已设置(发生非致命错误),则返回 true。
clear() 清除所有标志位并将流恢复到有效状态
clear(state) 清除所有标志位并设置传入的状态标志位
rdstate() 返回当前设置的标志
setstate(state) 设置传入的州旗

最常处理的位是 failbit,当用户输入无效内容时,该位会被置位。例如,考虑以下程序:

std::cout << "Enter your age: ";
int age {};
std::cin >> age;

请注意,此程序期望用户输入整数。但是,如果用户输入非数字数据,例如“Alex”,则 cin 将无法提取任何内容进行年龄计算,并且 failbit 将被置位。

如果发生错误且流被设置为除 goodbit 以外的任何值,则对该流的后续操作将被忽略。可以通过调用 clear() 函数来清除此状态。

输入验证(Input validation)

输入验证是指检查用户输入是否符合预设条件的过程。输入验证通常可以分为两种类型:字符串验证和数值验证。

字符串验证会将所有用户输入视为字符串,然后根据字符串的格式是否正确来决定是否接受或拒绝。例如,如果我们要求用户输入电话号码,我们可能需要确保他们输入的数据包含十位数字。在大多数语言(尤其是像 Perl 和 PHP 这样的脚本语言)中,这是通过正则表达式实现的。C++ 标准库也包含一个正则表达式库。由于正则表达式相比手动字符串验证速度较慢,因此只有在对性能(编译时和运行时)要求不高或手动验证过于繁琐的情况下才应使用正则表达式。

数值验证通常关注的是确保用户输入的数字在特定范围内(例如 0 到 20 之间)。但是,与字符串验证不同,用户有可能输入根本不是数字的内容——我们也需要处理这些情况。

为了帮助我们,C++ 提供了一些有用的函数,可以用来判断特定字符是数字还是字母。以下函数位于 cctype 头文件中:

功能 意义
std::isalnum(int) 如果参数是字母或数字,则返回非零值。
std::isalpha(int) 如果参数是字母,则返回非零值。
std::iscntrl(int) 如果参数是控制字符,则返回非零值。
std::isdigit(int) 如果参数是数字,则返回非零值。
std::isgraph(int) 如果参数是可打印字符(非空白字符),则返回非零值。
std::isprint(int) 如果参数是可打印字符(包括空格),则返回非零值。
std::ispunct(int) 如果参数既不是字母数字也不是空格,则返回非零值。
std::isspace(int) 如果参数为空格,则返回非零值。
std::isxdigit(int) 如果参数是十六进制数字(0-9、a-f、A-F),则返回非零值。

字符串验证

我们来做一个简单的字符串验证示例,要求用户输入姓名。验证标准是用户只能输入字母或空格。如果遇到其他任何字符,输入将被拒绝。

对于长度可变的输入,验证字符串的最佳方法(除了使用正则表达式库之外)是逐个检查字符串中的每个字符,确保其符合验证标准。这正是我们在这里要做的,或者更确切地说,这是它std::all_of为我们完成的工作。

#include <algorithm> // std::all_of
#include <cctype> // std::isalpha, std::isspace
#include <iostream>
#include <ranges>
#include <string>
#include <string_view>

bool isValidName(std::string_view name)
{
  return std::ranges::all_of(name, [](char ch) {
    return std::isalpha(ch) || std::isspace(ch);
  });

  // Before C++20, without ranges
  // return std::all_of(name.begin(), name.end(), [](char ch) {
  //    return std::isalpha(ch) || std::isspace(ch);
  // });
}

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

  do
  {
    std::cout << "Enter your name: ";
    std::getline(std::cin, name); // get the entire line, including spaces
  } while (!isValidName(name));

  std::cout << "Hello " << name << "!\n";
}

img

请注意,这段代码并不完美:用户可能会输入“asf w jweo s di we ao”或其他乱码,甚至更糟的是,可能只是一串空格。我们可以通过改进验证标准来部分解决这个问题,使其只接受至少包含一个字符且至多包含一个空格的字符串。

作者注:
读者“Waldo”提供了一个C++20解决方案(使用std::ranges),解决了这些不足之处。

现在我们来看另一个例子,这次我们要让用户输入电话号码。与用户名不同,用户名长度可变,且每个字符的验证标准都相同;而电话号码长度固定,但验证标准会根据字符的位置而变化。因此,我们需要采用不同的方法来验证电话号码输入。在这个例子中,我们将编写一个函数,将用户的输入与预先设定的模板进行比对,看是否匹配。模板的工作原理如下:

符号将匹配用户输入中的任何数字。

@ 符号将匹配用户输入中的任何字母字符。
_ 符号将匹配任何空格。
? 符号将匹配任何字符。
除此之外,用户输入和模板中的字符必须完全匹配。

因此,如果我们要求函数匹配模板“(###) ###-####”,这意味着我们期望用户输入一个括号、三个数字、一个短横线、一个空格、三个数字、一个短横线和四个数字。如果其中任何一项不匹配,输入将被拒绝。

以下是代码:

#include <algorithm> // std::equal
#include <cctype> // std::isdigit, std::isspace, std::isalpha
#include <iostream>
#include <map>
#include <ranges>
#include <string>
#include <string_view>

bool inputMatches(std::string_view input, std::string_view pattern)
{
    if (input.length() != pattern.length())
    {
        return false;
    }

    // This table defines all special symbols that can match a range of user input
    // Each symbol is mapped to a function that determines whether the input is valid for that symbol
    static const std::map<char, int (*)(int)> validators{
      { '#', &std::isdigit },
      { '_', &std::isspace },
      { '@', &std::isalpha },
      { '?', [](int) { return 1; } }
    };

    // Before C++20, use
    // return std::equal(input.begin(), input.end(), pattern.begin(), [](char ch, char mask) -> bool {
    // ...

    return std::ranges::equal(input, pattern, [](char ch, char mask) -> bool {
        auto found{ validators.find(mask) };

        if (found != validators.end())
        {
            // The pattern's current element was found in the validators. Call the
            // corresponding function.
            return (*found->second)(ch);
        }

        // The pattern's current element was not found in the validators. The
        // characters have to be an exact match.
        return ch == mask;
        }); // end of lambda
}

int main()
{
    std::string phoneNumber{};

    do
    {
        std::cout << "Enter a phone number (###) ###-####: ";
        std::getline(std::cin, phoneNumber);
    } while (!inputMatches(phoneNumber, "(###) ###-####"));

    std::cout << "You entered: " << phoneNumber << '\n';
}

img

使用此函数,我们可以强制用户输入完全符合我们特定格式的字符。但是,此函数仍存在一些限制:如果用户输入中包含 #、@、_ 和 ? 等有效字符,则此函数将无法正常工作,因为这些符号具有特殊含义。此外,与正则表达式不同,没有模板符号表示“可以输入可变长度的字符”。因此,此类模板无法用于确保用户输入两个以空格分隔的单词,因为它无法处理单词长度可变的情况。对于此类问题,通常更适合使用非模板方法。

数值验证

处理数值输入时,最直接的方法是使用提取运算符将输入提取为数值类型。通过检查 failbit,我们可以判断用户输入的是否为数字。

我们来试试这种方法:

#include <iostream>
#include <limits>

int main()
{
    int age{};

    while (true)
    {
        std::cout << "Enter your age: ";
        std::cin >> age;

        if (std::cin.fail()) // no extraction took place
        {
            std::cin.clear(); // reset the state bits back to goodbit so we can use ignore()
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out the bad input from the stream
            continue; // try again
        }

        if (age <= 0) // make sure age is positive
            continue;

        break;
    }

    std::cout << "You entered: " << age << '\n';
}

img

img

如果用户输入的是整数,则提取操作成功。std::cin.fail() 的值为 false,跳过条件判断,并且(假设用户输入的是正数)会执行到 break 语句,退出循环。

如果用户输入的内容以字母开头,则提取操作将失败。std::cin.fail() 的值为真,我们将进入条件判断。条件判断结束后,我们将执行 continue 语句,该语句会跳回 while 循环的开头,并再次提示用户输入内容。

然而,还有一种情况我们尚未测试,那就是用户输入的字符串以数字开头,但后面包含字母(例如“34abcd56”)。在这种情况下,开头的数字(34)会被提取到年龄中,字符串的其余部分(“abcd56”)会保留在输入流中,并且 failbit 不会被设置。这会导致两个潜在问题:

1.如果你想让这段文字成为有效输入,那么你的数据流中现在就充斥着垃圾信息。
2.如果你不希望这是有效输入,它不会被拒绝(但你的数据流中会有垃圾数据)。
我们先来解决第一个问题。这很简单:

#include <iostream>
#include <limits>

int main()
{
    int age{};

    while (true)
    {
        std::cout << "Enter your age: ";
        std::cin >> age;

        if (std::cin.fail()) // no extraction took place
        {
            std::cin.clear(); // reset the state bits back to goodbit so we can use ignore()
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out the bad input from the stream
            continue; // try again
        }

        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out any additional input from the stream

        if (age <= 0) // make sure age is positive
            continue;

      break;
    }

    std::cout << "You entered: " << age << '\n';
}

如果您不希望此类输入有效,我们需要做一些额外的工作。幸运的是,之前的解决方案已经帮我们解决了一半的问题。我们可以使用 gcount() 函数来确定有多少个字符被忽略。如果输入有效,gcount() 应该返回 1(被丢弃的换行符)。如果返回值大于 1,则说明用户输入的内容未被正确提取,我们应该要求他们重新输入。以下是一个示例:

#include <iostream>
#include <limits>

int main()
{
    int age{};

    while (true)
    {
        std::cout << "Enter your age: ";
        std::cin >> age;

        if (std::cin.fail()) // no extraction took place
        {
            std::cin.clear(); // reset the state bits back to goodbit so we can use ignore()
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out the bad input from the stream
            continue; // try again
        }

        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // clear out any additional input from the stream
        if (std::cin.gcount() > 1) // if we cleared out more than one additional character
        {
            continue; // we'll consider this input to be invalid
        }

        if (age <= 0) // make sure age is positive
        {
            continue;
        }

        break;
    }

    std::cout << "You entered: " << age << '\n';
}

img

将数值验证作为字符串

上面的例子仅仅为了得到一个简单的值就花费了不少功夫!处理数值输入的另一种方法是将其读取为字符串,然后尝试将其转换为数值类型。下面的程序就采用了这种方法:

#include <charconv> // std::from_chars
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>

// std::optional<int> returns either an int or nothing
std::optional<int> extractAge(std::string_view age)
{
    int result{};
    const auto end{ age.data() + age.length() }; // get end iterator of underlying C-style string

    // Try to parse an int from age
    // If we got an error of some kind...
    if (std::from_chars(age.data(), end, result).ec != std::errc{})
    {
        return {}; // return nothing
    }

    if (result <= 0) // make sure age is positive
    {
        return {}; // return nothing
    }

    return result; // return an int value
}

int main()
{
    int age{};

    while (true)
    {
        std::cout << "Enter your age: ";
        std::string strAge{};

        // Try to get a line of input
        if (!std::getline(std::cin >> std::ws, strAge))
        {
            // If we failed, clean up and try again
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            continue;
        }

        // Try to extract the age
        auto extracted{ extractAge(strAge) };

        // If we failed, try again
        if (!extracted)
            continue;

        age = *extracted; // get the value
        break;
    }

  std::cout << "You entered: " << age << '\n';
}

img

这种方法的工作量是比直接提取数值更多还是更少,取决于您的验证参数和限制条件。

如您所见,在 C++ 中进行输入验证是一项繁琐的工作。幸运的是,许多此类任务(例如将数字验证为字符串)可以轻松地转换为可在各种情况下重复使用的函数。

posted @ 2025-12-08 00:07  游翔  阅读(18)  评论(0)    收藏  举报