跳转至

第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。