
在我们的C++学习旅程中,到目前为止已经接触了大部分核心语言特性。然而,真正让C++强大的是其丰富的标准库。标准库占据了C++标准文档近三分之二的内容,这足以说明其重要性。 虽然我们无法深入讲解每一个库,但有一些专用的库组件在许多应用中都很有用,值得我们深入了解。
在这一部分,我们学习几个重要的专用库:元组(tuple)类型、位集(bitset)类型、正则表达式库和随机数生成库。这些工具各有其独特的用途,掌握它们将大大提高我们编写C++程序的效率和质量。
在编程实践中,我们经常需要将几个不同类型的数据组合在一起,但又不想专门为此定义一个类或结构体。这种需求在函数需要返回多个值时尤为常见。元组(tuple)正是为解决这类问题而设计的。
元组可以看作是pair的泛化版本。如果说pair总是包含两个成员,那么元组可以包含任意数量的成员。 每个元组类型的成员数量是固定的,但不同元组类型之间的成员数量可以不同。元组最适合用于将一些数据临时组合成单一对象,而无需为这些数据专门设计数据结构的场合。
当我们定义元组时,需要指定每个成员的类型。例如,假设我们要管理一个学生信息系统,需要同时处理学生的姓名、年龄和成绩,我们可以这样定义:
|std::tuple<std::string, int, double> student; // 所有成员都会被值初始化 std::tuple<std::string, int, double> alice("张三", 20, 85.5);
元组的构造函数是显式的,这意味着我们必须使用直接初始化语法:
|std::tuple<std::string, int, double> bob = {"李四", 21, 90.0}; // 错误 std::tuple<std::string, int, double> bob{"李四", 21, 90.0}; // 正确
与make_pair函数类似,标准库提供了make_tuple函数来生成元组对象:
|// 创建一个表示书籍信息的元组:书名、价格、库存数量 auto book = std::make_tuple("C++程序设计", 89.0, 150);
make_tuple函数会根据提供的初始值自动推导元组的类型。在这个例子中,book的类型是tuple<const char*, double, int>。
由于元组可以包含任意数量的成员,无法像pair那样给每个成员命名。因此,我们通过库函数模板get来访问元组的成员。使用get时必须指定一个显式的模板参数,表示要访问的成员位置:
|auto title = std::get<0>(book); // 获取第一个成员:书名 auto price = std::get<1>(book); // 获取第二个成员:价格 auto stock = std::get<2>(book); // 获取第三个成员:库存 std::get<1>(book) *= 0.8; // 对价格打八折
括号中的值必须是整型常量表达式,并且我们按照从0开始的方式计数。
当我们不确定元组的确切类型时,可以使用两个辅助类模板来查找元组的成员数量和类型:
|typedef decltype(book) BookInfo; size_t memberCount = std::tuple_size<BookInfo>::value; // 返回3 std::tuple_element<1, BookInfo>::type newPrice = std::get<1>(book); // newPrice是double类型
元组支持相等性和关系运算符,这些运算符逐对比较左侧和右侧元组的成员。两个元组只有在成员数量相同且对应成员都相等时才相等。关系运算符使用字典序比较:
|std::tuple<std::string, int> student1("王五", 85); std::tuple<std::string, int> student2("赵六", 92); bool result = (student1 < student2); // 按字典序比较,result为true
由于元组定义了比较运算符,我们可以将元组序列传递给算法,也可以将元组用作有序容器的键类型。
元组的一个常见用途是从函数返回多个值。假设我们有一个图书管理系统,需要在多个书店的销售记录中查找某本书的销售情况。每个书店都有自己的交易记录文件,我们希望找到销售了指定图书的书店,并返回相关信息。
我们可以设计一个函数,返回一个元组向量,其中每个元组包含书店的索引和该书店中相关交易记录的迭代器范围:
|// 定义返回类型:书店索引、开始迭代器、结束迭代器 using BookSearchResult = std::tuple< std::vector<BookSale>::size_type, std::vector<BookSale>::const_iterator, std::vector<BookSale>::const_iterator >; std::vector<BookSearchResult> findBookInStores(const std::vector<std
使用这个函数的代码可能是这样的:
|void generateSalesReport(std::istream& input, std::ostream& output, const std::vector<std::vector<BookSale>>& allStores) { std::string isbn; while (input >> isbn) { auto searchResults = findBookInStores(allStores, isbn); if
在某些应用中,我们需要处理二进制位的集合。虽然我们可以使用整型类型并结合位运算符来实现这种需求,但当需要处理的位数超过最长整型的位数时,这种方法就不够用了。标准库的bitset类为我们提供了更方便、更强大的位操作工具。
bitset是一个类模板,与array类似,它有固定的大小。当我们定义bitset时,需要指定它包含多少个位:
|std::bitset<32> flags; // 32位,全部初始化为0 std::bitset<32> quiz1(0xf); // 低4位为1,其余为0
大小必须是常量表达式。bitset中的位没有名字,我们通过位置来引用它们。位的编号从0开始,0位是低序位,最高编号的位是高序位。
当我们用无符号长整型值初始化bitset时,该值被当作位模式来使用。如果bitset的大小大于无符号长整型的位数,则剩余的高序位被设置为0。如果bitset的大小小于该整型值的位数,则只使用该值的低序位:
|std::bitset<16> quiz2(0xbeef); // 位模式:1011111011101111 std::bitset<32> quiz3(0xbeef); // 位模式:00000000000000001011111011101111
我们也可以用字符串来初始化bitset。字符串中的字符直接表示位模式。需要注意的是,字符串的索引和bitset的位编号是相反的关系:字符串中最高索引(最右边)的字符用来初始化bitset的0位(低序位):
|std::bitset<32> examScores("1100101010110011"); // 从右到左:位0到位15
我们也可以使用字符串的子串来初始化:
|std::string pattern("1111100000011001101"); std::bitset<16> quiz4(pattern, 5, 4); // 从pattern[5]开始的4个字符 std::bitset<16> quiz5(pattern, pattern.size()-4); // 最后4个字符
理解这种初始化方式的关键是记住字符串和bitset的索引关系是相反的。
bitset类提供了多种测试和设置位的方法。这些操作既可以作用于整个bitset,也可以作用于指定的位:
|std::bitset<32> quiz; // 测试操作 bool hasAnySet = quiz.any(); // 是否有任何位被设置 bool allSet = quiz.all(); // 是否所有位都被设置 bool noneSet = quiz.none(); // 是否没有位被设置 size_t setBits = quiz.count(); // 返回被设置的位数 size_t totalBits = quiz.size();
下标运算符被重载了。对于常量bitset,它返回bool值;对于非常量bitset,它返回一个特殊类型,允许我们操作指定位置的位:
|quiz[0] = 1; // 设置位0 quiz[10] = quiz[5]; // 将位5的值赋给位10 quiz[8].flip(); // 翻转位8 bool bit5 = quiz[5]; // 获取位5的值
to_ulong和to_ullong操作返回一个保存相同位模式的值。只有当bitset的大小小于或等于对应类型的大小时,才能使用这些操作:
|std::bitset<16> smallBits(0xffff); unsigned long ulValue = smallBits.to_ulong(); // 如果位模式无法放入指定类型,会抛出overflow_error异常
让我们用一个实际例子来展示bitset的用法。假设我们要为一个在线考试系统记录学生答题情况,每个学生有30道题,我们用bitset来记录哪些题答对了:
|class StudentExam { private: std::bitset<30> answers; // 30道题的答题情况 std::string studentId; public: StudentExam(const std::string& id) : studentId(id) {} void markCorrect(size_t questionNum) { if (questionNum < 30) { answers.set
正则表达式是描述字符序列的一种方式,是一个极其强大的计算工具。虽然描述正则表达式语言的细节超出了本书的范围,但我们可以学习如何使用C++的正则表达式库(RE库)来处理文本。
正则表达式库定义在regex头文件中,包含几个核心组件:regex类表示一个正则表达式;regex_match函数判断整个字符序列是否与正则表达式匹配; regex_search函数在输入序列中查找匹配正则表达式的子序列;regex_replace函数使用给定格式替换匹配的正则表达式。
让我们通过一个实际例子来学习正则表达式的使用。假设我们要在一段文本中查找所有的邮箱地址:
|#include <regex> #include <string> #include <iostream> void findEmailAddresses(const std::string& text) { // 定义一个简单的邮箱地址模式 std::string emailPattern(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"); std::regex emailRegex(emailPattern); std::smatch matchResults; std::string testText = "联系我们:support@example.com 或 admin@test.org";
在这个例子中,我们定义了一个正则表达式来匹配邮箱地址的基本格式。regex_search函数在输入字符串中查找第一个匹配的子字符串。
regex_match函数要求整个输入序列都与正则表达式匹配。例如,验证用户输入的手机号格式:
|bool validatePhoneNumber(const std::string& phone) { // 匹配中国大陆手机号格式:1开头,第二位是3-9,共11位数字 std::regex phonePattern(R"(1[3-9]\d{9})"); return std::regex_match(phone, phonePattern); } // 使用示例 std::string phone1 = "13812345678"; // 有效 std::string phone2 = "12345678901"; // 无效,第二位不符合规则 std::cout <<
正则表达式的真正威力在于它可以捕获匹配的子部分。我们用括号来标记想要捕获的子表达式:
|void parseDate(const std::string& dateStr) { // 匹配日期格式:YYYY-MM-DD,并捕获年、月、日 std::regex datePattern(R"((\d{4})-(\d{2})-(\d{2}))"); std::smatch dateMatch; if (std::regex_match(dateStr, dateMatch, datePattern)) { std::cout << "年份:" << dateMatch[1].str() << std::endl; std
在这个例子中,dateMatch[0]包含整个匹配,dateMatch[1]包含第一个子匹配(年份),依此类推。
当我们需要找到输入中所有匹配的部分时,可以使用sregex_iterator:
|void findAllNumbers(const std::string& text) { std::regex numberPattern(R"(\d+\.?\d*)"); // 匹配整数或小数 // 创建迭代器来遍历所有匹配 std::sregex_iterator numbersBegin(text.begin(), text.end(), numberPattern); std::sregex_iterator numbersEnd; std::cout << "找到的数字:"; for (auto it
regex_replace函数可以用指定的格式替换匹配的文本:
|std::string maskSensitiveInfo(const std::string& text) { // 匹配手机号并用*号遮掩中间4位 std::regex phonePattern(R"((1[3-9]\d)(\d{4})(\d{4}))"); std::string replacement = "$1****$3"; // $1表示第一个捕获组,$3表示第三个 return std::regex_replace(text, phonePattern, replacement); } // 使用示例 std::string message = "请联系张先生,电话:13812345678";
在传统的C和C++中,程序员通常使用C标准库的rand函数来生成随机数。然而,rand函数存在许多问题: 它只能生成固定范围内的整数,程序员在转换范围、类型或分布时容易引入非随机性,而且它无法处理非均匀分布的需求。
C++11引入的随机数库通过一组协作的类解决了这些问题:随机数引擎和随机数分布类。引擎生成无符号随机数序列,分布类使用引擎生成特定类型、特定范围、特定概率分布的随机数。
随机数引擎是函数对象类,它们定义了一个不接受参数并返回随机无符号数的调用运算符:
|std::default_random_engine generator; // 使用默认的随机数引擎 for (int i = 0; i < 10; ++i) { std::cout << generator() << " "; }
然而,引擎生成的原始随机数通常不能直接使用,因为它们的范围往往与我们需要的不同。正确地转换随机数的范围是出乎意料地困难的。这就是为什么我们需要分布类型:
|// 生成0到9之间的均匀分布整数 std::uniform_int_distribution<int> distribution(0, 9); std::default_random_engine generator; for (int i = 0; i < 10; ++i) { std::cout << distribution(generator) << " "; }
注意我们传递给分布对象的是引擎对象本身,而不是引擎生成的值。这是因为某些分布可能需要调用引擎多次。
随机数库提供了多种分布类型。让我们看几个常用的例子:
生成随机浮点数:
|void generateRandomScores() { std::random_device rd; // 用于获取种子 std::mt19937 gen(rd()); // 使用随机设备作为种子 std::uniform_real_distribution<double> scoreDistribution(0.0, 100.0); std::cout << "随机生成的考试成绩:"; for (int i = 0; i < 5; ++i) {
生成正态分布的随机数:
|void generateHeightData() { std::random_device rd; std::mt19937 gen(rd()); // 正态分布:平均身高170cm,标准差10cm std::normal_distribution<double> heightDistribution(170.0, 10.0); std::cout << "模拟身高数据(cm):"; for (int i = 0; i < 10; ++i) { std
生成布尔值:
|void simulateCoinFlip() { std::random_device rd; std::mt19937 gen(rd()); std::bernoulli_distribution coinFlip(0.5); // 50%的概率 int heads = 0, tails = 0; for (int i = 0; i < 100; ++i) { if (
随机数生成器有一个重要特性:给定的生成器在每次运行时会返回相同的数字序列。这在测试期间很有帮助,但在生产环境中,我们通常希望每次运行产生不同的随机结果。
一个常见的错误是在函数内部创建引擎和分布对象:
|// 错误的做法:每次调用都会返回相同的值 int badRandomNumber() { std::default_random_engine e; std::uniform_int_distribution<int> u(0, 9); return u(e); // 总是返回相同的值 }
正确的做法是将引擎和分布对象声明为静态的:
|// 正确的做法:使用静态对象保持状态 int goodRandomNumber() { static std::default_random_engine e; static std::uniform_int_distribution<int> u(0, 9); return u(e); // 每次调用返回序列中的下一个值 }
为了让程序每次运行都产生不同的随机结果,我们需要提供一个种子值:
|class RandomNumberGenerator { private: std::mt19937 generator; public: RandomNumberGenerator() { // 使用当前时间作为种子 generator.seed(std::chrono::system_clock::now().time_since_epoch().count()); } int getRandomInt(int min, int max) { std
让我们用一个完整的例子来展示随机数库的使用。假设我们要模拟一个简单的抽奖系统:
|#include <random> #include <vector> #include <string> #include <iostream> #include <algorithm> class LotterySystem { private: std::vector<std::string> participants; std::random_device rd; std::mt19937 gen; public: LotterySystem() : gen(rd()) {} void addParticipant
我们使用random_device获取真正的随机种子,使用mt19937作为随机数引擎(这是一个高质量的梅森旋转算法实现),并使用uniform_int_distribution生成均匀分布的整数。
现在让我们通过一些简单的练习题来巩固本章学到的知识。每道题都涵盖了C++标准库特殊工具类的核心概念,请先尝试独立完成,然后再查看答案。
使用tuple存储学生的信息(姓名、年龄、成绩),然后访问和修改这些信息。
|#include <iostream> #include <tuple> #include <string> using namespace std; int main() { // 创建元组:姓名、年龄、成绩 tuple<string, int, double> student("张三", 20, 85.5); // 使用get访问元素 cout << "姓名:" << get<0>(student) <<
使用bitset实现一个简单的权限管理系统,支持读取、写入、执行三种权限。
|#include <iostream> #include <bitset> #include <string> using namespace std; class PermissionManager { private: bitset<3> permissions; // 位0:读, 位1:写, 位2:执行 public: void grantRead() { permissions.set(0); } void grantWrite() { permissions.set(1); } void
使用正则表达式验证邮箱地址格式是否正确。
|#include <iostream> #include <regex> #include <string> using namespace std; bool validateEmail(const string& email) { // 简单的邮箱正则表达式 regex pattern(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"); return regex_match(email, pattern); } int main() { string emails[] = { "user@example.com"
使用现代C++的随机数库生成1到100之间的随机整数。
|#include <iostream> #include <random> using namespace std; int main() { // 创建随机数引擎 random_device rd; // 用于获取随机种子 mt19937 gen(rd()); // Mersenne Twister引擎 // 创建分布:1到100的均匀分布 uniform_int_distribution<> dis(1, 100); cout << "生成10个1-100之间的随机数:" << endl; for
使用tuple存储学生信息,使用bitset标记学生状态(是否在校、是否毕业等),使用随机数生成测试数据。
|#include <iostream> #include <tuple> #include <bitset> #include <string> #include <vector> #include <random> #include <algorithm> using namespace std; class StudentManager { private: // 元组:姓名、年龄、成绩 using StudentInfo = tuple<string, int, double>; vector<StudentInfo
元组可以存储不同类型的值,使用get<N>(tuple)访问元素,其中N必须是编译时常量。make_tuple可以自动推导类型,结构化绑定(C++17)可以方便地解包元组。
bitset提供了高效的位操作,比手动使用位运算符更安全和易读。set()设置位为1,reset()设置位为0,test()检查位的值。bitset的大小必须在编译时确定。
regex_match用于检查整个字符串是否完全匹配正则表达式。使用原始字符串字面量R"(...)"可以避免转义字符的问题,让正则表达式更易读。正则表达式是强大的文本匹配工具。
现代C++的随机数库将引擎和分布分离:引擎负责生成随机数,分布负责将随机数映射到特定范围。这种方式比rand()更灵活、更可控。random_device用于获取真随机数作为种子,mt19937是常用的高质量随机数引擎。
这个例子综合运用了:
这些工具类让代码更简洁、更高效。