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";
}

请注意,这段代码并不完美:用户可能会输入“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';
}

使用此函数,我们可以强制用户输入完全符合我们特定格式的字符。但是,此函数仍存在一些限制:如果用户输入中包含 #、@、_ 和 ? 等有效字符,则此函数将无法正常工作,因为这些符号具有特殊含义。此外,与正则表达式不同,没有模板符号表示“可以输入可变长度的字符”。因此,此类模板无法用于确保用户输入两个以空格分隔的单词,因为它无法处理单词长度可变的情况。对于此类问题,通常更适合使用非模板方法。
数值验证
处理数值输入时,最直接的方法是使用提取运算符将输入提取为数值类型。通过检查 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';
}


如果用户输入的是整数,则提取操作成功。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';
}

将数值验证作为字符串
上面的例子仅仅为了得到一个简单的值就花费了不少功夫!处理数值输入的另一种方法是将其读取为字符串,然后尝试将其转换为数值类型。下面的程序就采用了这种方法:
#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';
}

这种方法的工作量是比直接提取数值更多还是更少,取决于您的验证参数和限制条件。
如您所见,在 C++ 中进行输入验证是一项繁琐的工作。幸运的是,许多此类任务(例如将数字验证为字符串)可以轻松地转换为可在各种情况下重复使用的函数。

浙公网安备 33010602011771号