一、流状态和输入验证
1、Stream states
ios_base 类包含几个状态标志,用于指示使用流时可能发生的各种情况:
虽然这些标志存在于 ios_base 中,但因为 ios 是从 ios_base 派生的,而且 ios 比 ios_base 需要更少的输入,所以它们通常通过 ios 访问(例如,作为 std::ios::failbit)。
ios 还提供了一些成员函数以便方便地访问这些状态:
最常处理的位是failbit,它在用户输入无效输入时设置。 例如,考虑以下程序:
std::cout << "Enter your age: ";
int age;
std::cin >> age;
请注意,该程序希望用户输入一个整数。 但是,如果用户输入非数字数据,例如“Alex”,cin 将无法提取任何年龄的数据,并且会设置故障位。
如果发生错误并且流设置为 goodbit 以外的任何值,则该流上的进一步流操作将被忽略。 这种情况可以通过调用 clear() 函数来清除。
2、输入验证
输入验证是检查用户输入是否符合某些标准的过程。 输入验证通常可以分为两种类型:字符串和数字。
通过字符串验证,我们将所有用户输入作为字符串接受,然后根据其格式是否正确接受或拒绝该字符串。 例如,如果我们要求用户输入电话号码,我们可能希望确保他们输入的数据有十位数字。 在大多数语言(尤其是 Perl 和 PHP 等脚本语言)中,这是通过正则表达式完成的。 C++ 标准库也有一个正则表达式库。 与手动字符串验证相比,正则表达式速度较慢,只有在不关心性能(编译时和运行时)或手动验证过于繁琐时才应使用它们。
对于数字验证,我们通常关心确保用户输入的数字在特定范围内(例如,0 到 20 之间)。 然而,与字符串验证不同的是,用户可以输入根本不是数字的东西——我们也需要处理这些情况。
为了帮助我们,C++ 提供了许多有用的函数,我们可以使用它们来确定特定字符是数字还是字母。 以下函数位于 cctype 标头中:
3、字符串验证
让我们通过要求用户输入他们的名字来做一个简单的字符串验证案例。 我们的验证标准将是用户只输入字母字符或空格。 如果遇到任何其他情况,输入将被拒绝。
对于可变长度输入,验证字符串的最佳方法(除了使用正则表达式库之外)是逐步检查字符串的每个字符并确保它符合验证标准。 这正是我们在这里要做的,或者更好的是,这就是 std::all_of 为我们所做的。
#include <algorithm> // std::all_of
#include <cctype> // std::isalpha, std::isspace
#include <iostream>
#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));
});
}
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";
}
现在让我们看另一个示例,我们将要求用户输入他们的电话号码。 与可变长度且每个字符的验证标准相同的用户名不同,电话号码是固定长度的,但验证标准会根据字符的位置而有所不同。 因此,我们将采用不同的方法来验证我们的电话号码输入。 在这种情况下,我们将编写一个函数,该函数将根据预先确定的模板检查用户的输入,以查看它是否匹配。 该模板将按如下方式工作:
# 将匹配用户输入中的任何数字。
@ 将匹配用户输入中的任何字母字符。
_ 将匹配任何空格。
? 将匹配任何东西。
否则,用户输入和模板中的字符必须完全匹配。
所以,如果我们要求函数匹配模板“(###) ###-####”,这意味着我们期望用户输入一个'('字符,三个数字,一个')'字符, 一个空格、三个数字、一个破折号和另外四个数字。 如果其中任何一项不匹配,则输入将被拒绝。
这是代码:
#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;
}
// We have to use a C-style function pointer, because std::isdigit and friends
// have overloads and would be ambiguous otherwise.
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 {
if (auto found{ validators.find(mask) }; found != validators.end())
{
// The pattern's current element was found in the validators. Call the
// corresponding function.
return (*found->second)(ch);
}
else
{
// The pattern's current element was not found in the validators. The
// characters have to be an exact match.
return (ch == mask);
}
});
}
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';
}
使用此功能,我们可以强制用户完全匹配我们的特定格式。 但是,这个函数仍然受到几个约束:if #、@、_ 和 ? 是用户输入中的有效字符,此功能将不起作用,因为这些符号已被赋予特殊含义。 此外,与正则表达式不同,没有模板符号表示“可以输入可变数量的字符”。 因此,这样的模板不能用于确保用户输入由空格分隔的两个单词,因为它无法处理单词长度可变的事实。 对于此类问题,非模板方法通常更合适。
4、数字验证
在处理数字输入时,显而易见的方法是使用提取运算符将输入提取为数字类型。 通过检查故障位,我们可以判断用户是否输入了数字。
#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';
}
如果用户输入一个数字,cin.fail() 将为 false,我们将点击 break 语句,退出循环。 如果用户输入以字母开头的输入,cin.fail() 将为真,我们将进入条件。
然而,还有一种情况我们没有测试过,那就是用户输入一个以数字开头但随后包含字母的字符串(例如“34abcd56”)。 在这种情况下,起始数字 (34) 将被提取到年龄中,字符串的其余部分 (“abcd56”) 将留在输入流中,并且不会设置故障位。 这会导致两个潜在问题:
如果您希望这是有效的输入,那么您现在的流中有错误数据。
如果您不希望这是有效输入,它不会被拒绝(并且您的流中有错误数据)。
让我们解决第一个问题。 这很简单:
#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';
}
5、作为字符串的数字验证
上面的例子只是为了得到一个简单的值而做了大量的工作! 处理数字输入的另一种方法是将其作为字符串读入,然后尝试将其转换为数字类型。 以下程序使用了该方法:
#include <charconv> // std::from_chars
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
std::optional<int> extractAge(std::string_view age)
{
int result{};
auto end{ age.data() + age.length() };
// Try to parse an int from age
if (std::from_chars(age.data(), end, result).ptr != end)
{
return {};
}
if (result <= 0) // make sure age is positive
{
return {};
}
return result;
}
int main()
{
int age{};
while (true)
{
std::cout << "Enter your age: ";
std::string strAge{};
std::cin >> strAge;
if (auto extracted{ extractAge(strAge) })
{
age = *extracted;
break;
}
}
std::cout << "You entered: " << age << '\n';
}
在 C++ 中进行输入验证需要大量代码工作。不过许多此类任务(例如,将数字验证作为字符串)的函数都可以重用。