第17章 标准库特殊设施
tuple类型
tuple是类似pair的模板,但一个tuple可以有任意数量的成员。
当我们希望将一些数据组合成单一对象,但又不想麻烦地定义一个新数据结构来表示这些数据时,tuple是非常有用的。
tuple的一个常见用途是从一个函数返回多个值。书中给出的案例见p638。
定义和初始化tuple
当我们定义一个tuple时,需要指出每个成员的类型:
tuple<size_t, size_t, size_t> threeD; // 三个成员都设置为0
tuple<string, int> someVal("constants", 42);
当我们创建一个tuple对象时,可以使用tuple的默认构造函数,它会对每个成员进行值初始化。也可以为每个成员提供一个初始值。tuple的构造函数是explicit的,必须使用直接初始化语法。
类似make_pair,标准库定义了make_tuple函数,用来生成tuple对象:
// 表示书店交易记录的tuple,包含: ISBN, 数量和每册书的价格
auto item = make_tuple("0-999-78345-X", 3, 20.00);
make_tuple函数使用初始值的类型来推断tuple类型。本例中,item类型是tuple<const char*, int, double>
。
访问tuple的成员
tuple的成员都是未命名的,要访问它们,就要使用get标准库函数模板,我们传递给get一个tuple对象,它返回指定成员的引用。
auto book = get<0>(item); // 返回item的第一个成员
auto cnt = get<1>(item); // 返回item的第二个成员
my note: 如果引用索引超出范围,那么编译阶段会报错。
可以通过两个辅助类模板来查询tuple成员的数量和类型:
typedef decltype(item) trans; // trans是itme的类型
// 返回trans类型对象中成员的数量
size_t sz = tuple_size<trans>::value; // 返回3
// cnt的类型与item中第二个成员相同
tuple_element<1, trans>::type cnt = get<1>(item); // cnt是一个int
关系和相等运算符
tuple的关系和相等运算符的行为类似容器的对应操作,这些运算符逐对比较左侧tuple和右侧tuple的成员。只有两个tuple具有相同数量的成员时并且成员也支持比较操作时,我们才能比较它们。
bitset类型
标准库定义了bitset类,使得位运算的使用更为容易,并且能够处理超过最长整型类型大小的位集合。bitset类定义在头文件bitset中。
定义和初始化bitset
bitset类是一个类模板,它类似array类,具有固定的大小。当我们定义一个bitset时,需要声明它包含多少个二进制位。
bitset<32> bitvec(1U); // 32位;低位为1,其他位为0
二进制位的位置是从0开始编号的,因此,bitvec包含编号从0到31的32个二进制位。编号从0开始的二进制位被称为低位(low-order),编号到31结束的二进制位被称为高位(high-order)。
p641页列出了初始化一个bitset的方法。
使用unsigned值初始化bitset
当我们使用一个整型值来初始化bitset时,此值将被转换为unsigned long long类型并被当作位模式来处理。bitset中的二进制位将是此模式的一个副本。如果bitset的大小大于unsigned long long的位数,则剩余高位被置为0。如果小于,则只使用给定值中的低位,超出bitset大小的高位被丢弃。
从一个string初始化bitset
我们可以从一个string或一个字符数组指针来初始化bitset。两种情况下,字符都直接表示位模式。当我们使用字符串表示数时,字符串中下标最小的字符对应高位:
bitset<32> bitvec4("1100"); // 2、3两位为1,剩余两位为0
如果string包含的字符数比bitset少,则bitset的高位被置为0。
Note
string的下标编号习惯于bitset恰好相反:string中下标最大的字符(最右)用来初始化bitset中的低位。
bitset操作
bitset操作定义了多种检测或设置一个或多个二进制位的方法。见p643。
bitset<32> bitvec(1U);
bool is_set = bitvec.any(); // true,因为有1位置位
bool is_not_set = bitvec.none(); // false,因为有1位置位了
bool all_set = bitvec.all(); // false,因为只有1位置位了
size_t onBits = bitvec.count(); // 返回1
size_t sz = bitvec.size(); // 返回32
bitvec.flip(); // 翻转bitvec中的所有位
bitvec.reset(); // 将所有位复位
bitvec.set(); // 将所有位置位
bitvec.flip(0); // 翻转第一位
bitvec.set(0); // 置位第一位
bitvec.reset(i); // 复位第i位
bitvec.test(0); // 返回false,因为第一位已复位
bitvec[0] = 0; // 将第一位复位
bitvec[31] = bitvec[0]; // 将最后一位设置为与第一位一样
~bitvec[0]; // 翻转第一位
提取bitset的值
to_ulong和to_ullong操作都返回一个值,保存了与bitset对象相同的位模式,只有当bitset的大小小于等于对应的大小时,我们才能使用这两个操作,否则将会抛出overflow_error异常。
unsigned long ulong = bitvec3.to_ulong();
cout << "ulong = " << ulong << endl;
bitset的IO运算符
输入运算符从一个输入流读取字符,保存到一个临时的string对象中。直到读取的字符数达到对应bitset的大小时,或是遇到不是1或0的字符时,或是遇到文件尾或输入错误时,读取过程才停止。随即用临时string对象来初始化bitset。如果读取的字符数小于bitset的大小,高位被置为0。
bitset<16> bits;
cin >> bits; // 从cin读取最多16个0或1
cout << "bits: " << bits << endl;
正则表达式
正则表达式(regular expression)是一种描述字符序列的方法,是一种极其强大的计算工具。本章节主要是介绍如何使用C++正则表达式库(RE库),它定义在头文件regex中,它包含多个组件:
组件 | 说明 |
---|---|
regex | 表示有一个正则表达式的类 |
regex_match | 将一个字符序列与一个正则表达式匹配 |
regex_search | 寻找第一个与正则表达式匹配的子序列 |
regex_replace | 使用给定格式替换一个正则表达式 |
sregex_iterator | 迭代器适配器,调用regex_search来遍历一个string中所有匹配的子串 |
smatch | 容器类,保存在string中搜索的结果 |
ssub_match | string中匹配的子表达式的结果 |
my note: 正则表达式库需要高级的gcc版本支持,我使用gcc5.3.1可以通过测试。
函数regex_match和regex_search确定一个给定字符序列与一个给定regex是否匹配。如果整个输入序列与表达式匹配,则regex_match返回true;如果输入一个序列中一个子串与表达式匹配,则regex_search返回true。
见书本p646更详细的讨论。
随机数
程序通常需要一个随机数源。在新标准出现之前,C和C++都依赖于一个简单的C库函数rand来生成随机数。此函数生成均匀分布的伪随机整数,每个随机数的范围在0和一个系统相关的最大值(至少为32767)之间。
rand函数有一些问题:即使不是大多数,也有很多程序员需要不同范围的随机数。一些应用需要随机浮点数。一些程序需要非均匀分布的数。而程序员为了解决这些问题而试图转换rand生成的随机数的范围、类型或分布时,常常会引入非随机性。
定义在头文件random中的随机数库通过一组协作的类来解决这些问题:随机数引擎(random-number engines)和随机数分布类(random-number distribution)。
组件 | 说明 |
---|---|
引擎 | 类型,生成随机unsigned整数序列 |
分布 | 类型,使用引擎返回服从特定概率分布的随机数 |
Note
C++程序不应该使用库函数rand,而应使用default_random_engine类和恰当的分布类对象。
随机数引擎和分布
随机数引擎是函数对象类,它们定义了一个调用运算符,该运算符不接受参数并返回一个随机unsigned整数。我们可以通过调用一个随机数引擎对象来生成原始随机数。
default_random_engine e;
cout << e() << endl; // 生成一个随机无符号数
标准库定义了多个随机数引擎类,区别在于性能和随机质量不同。
分布类型和引擎
为了得到一个指定范围内的数,我们使用一个分布类型的对象:
// 生成0到9之间(包含)均匀分布的随机数
uniform_int_distribution<unsigned> u(0, 9);
default_random_engine e;
cout << u(e) << endl;
分布类型也是函数对象类。分布类型定义了一个调用运算符,它接受一个随机数引擎作为参数。分布对象使用它的引擎参数生成随机数,并将其映射到指定的分布。
Note
当我们说随机数发生器时,是指分布对象和引擎对象的组合。
引擎生成一个数值序列
随机数发生器有一个特性经常会使新手迷惑:即使生成的数看起来是随机的,但对于一个给定的发生器,每次运行程序它都会返回相同的数值序列。一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列。
my note: 但是我实验发现,使用random_device引擎,如果不定义成static的,仍然可以生成不同的序列。而且分布类型不定义成static的,也可以生成不同的序列。
设置随机数发生器种子
我们通常希望每次运行程序都会生成不同的随机结果,可以通过提供一个种子(seed)来达到这一目的。种子就是一个数值,引擎可以利用它从序列中一个新位置重新开始生成随机数。
default_random_engine e1; // 使用默认种子
e1.seed(42); // 调用seed设置一个种子值
default_random_engine e2(42); // 使用给定的种子值
如果引擎种子相同,将生成相同的序列。
选择一个好种子,是极其困难的,可能最常用的方法是调用系统函数time。它定义在头文件ctime中,它返回从一个特定时刻到当前经过了多少秒。
default_random_engine e(time(0)); // 稍微随机些的种子
my note: 使用random_device引擎为另一个引擎创建一个种子也是一种方法。
后续内容讨论了其他随机数的分布,比如:生成随机实数、生成非均匀分布随机数等。
IO库再探
格式化输入与输出
除了条件状态外,每个iostream对象还维护一个格式状态来控制IO如何格式化的细节。格式状态控制格式化的某些方面,如整型是几进制、浮点值的精度、一个输出元素的宽度等。
标准库定义了一组操纵符来修改流的格式状态。一个操纵符是一个函数或是一个对象,会影响流的状态。
Warning
当操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。
控制布尔值的格式
默认情况下,bool值打印为1或0,通过对流使用boolalpha操纵符来修改原有格式:
cout << boolalpha << true << " " << false << endl; // 输出:true false
一旦向cout“写入”了boolalpha,我们就改变了cout打印bool值的方式。后续打印bool值的操作都会打印true或false,为了取消格式的改变,noboolalpha:
cout << noboolalpha;
指定整型值的进制
默认情况下,整型值的输入输出使用十进制。我们可以使用操纵符hex、oct、dec将其改为十六进制、八进制或是改回十进制。
cout << "default: " << 20 << endl;
cout << "octal: " << oct << 20 << endl;
cout << "hex: " << hex << 20 << endl;
cout << "decimal: " << dec << 20 << endl;
在输出中指出进制
当对流应用showbase操纵符时,会在输出结果中显示进制:
-
前导0x表示十六进制。
-
前导0表示八进制。
-
无前导字符串表示十进制。
cout << showbase; // 打印整型值时显示进制
cout << "default: " << 20 << endl;
cout << "octal: " << oct << 20 << endl;
cout << "hex: " << hex << 20 << endl;
cout << "decimal: " << dec << 20 << endl;
cout << noshowbase; // 恢复流状态
指定打印精度
setprecision操纵符接受一个参数,用来设置精度。它定义在头文件iomanip中。
cout << setprecision(3);
cout << sqrt(2.0) << endl; // 输出:1.41
更多操纵符见p669。
未格式化的输入/输出操作
标准库提供了一组低层操作,支持未格式化IO(unformatted IO)。这组操作允许我们将一个流当作一个无解释的字节序列来处理。
单字节操作
有几个未格式化操作每次一个字节地处理流,它们会读取而不是忽略空白符。
// 读写一个字符
char ch;
while (cin.get(ch))
cout.put(ch);
操作 | 说明 |
---|---|
is.get(ch) | 从istream is读取下一个字节存入字符ch中。返回is |
os.put(ch) | 将字符ch输出到ostream os。返回os |
is.get() | 将is的下一个字节作为int返回 |
is.putback(ch) | 将字符ch放回is。返回is |
is.unget() | 将is向后移动一个字节。返回is |
is.peek() | 将下一个字节作为int返回,但不从流中删除它 |
详细讨论见p673。
多字节操作
一些未格式化IO操作一次处理大块数据。如果速度是要考虑的重点问题的话,这些操作是很重要的,这些操作要求我们自己分配并管理用来保存和提取数据的字符数组。
书中未给出代码案例讲解,具体操作见p674。
流随机访问
标准库提供了一对函数,来定位(seek)到流中给定的位置,以及告诉(tell)我们当前的位置。
在大多数系统中,绑定到cin、cout、cerr和clog的流不支持随机访问,因为这种操作对它们没有意义。对这些流调用seek和tell会导致运行时出错,将流置于一个无效状态。
seek和tell函数
为了支持随机访问,IO类型维护一个标记来确定下一个读写操作要在哪里进行。标准库实际上定义了两对seek和tell函数,差别在于名字的后缀是g还是p,g版本表示我们正在读取数据,而p版本表示我们正在写入数据。
操作 | 说明 |
---|---|
tellg() tellp() | 返回一个输入流中(tellg)或输出流中(tellp)标记的当前位置 |
seekg(pos) seekp(pos) | 在一个输入流或输出流中奖标记重定位到给定的绝对地址。pos通常是前一个tell返回的值 |
seekp(off, from) seekg(off, from) | 在一个输入流或输出流中,奖标记定位到from之前或之后off个字符,from可以是:beg(流开始位置), cur(流当前位置), end(流结尾位置) |
从逻辑上讲,我们只能对istream使用g版本,对ostream使用p版本。iostream则可以使用g版本又可以使用p版本。
详细案例及讨论见书本p677。