C++ Primer学习笔记 - 第17章 标准库特殊实施
本章介绍4个标准库设施:tuple, bitset, 随机数生成, 正则表达式。还有IO库具有一些特殊目的的部分。
17.1 tuple类型
头文件:tuple
tuple 元组,类似于pair的模板。联系是pair的成员类型都可以不一样,区别是pair恰好有2个成员,而tuple可以有任意数量成员( >= 0)。
17.1.1 定义和初始化tuple
需要指出每个tuple类型,可以用tuple默认构造函数,但无法使用拷贝构造函数。
// 构造tuple
tuple<size_t, size_t, size_t> threeD; // 3个成员都进行值初始化,设置为0
tuple<string, vector<double>, int, list<int>> someVal("constants", {3.14, 2.718}, 42, {0,1,2,3,4,5}); // 调用tuple的构造函数,对成员进行直接初始化
// 由于tuple的构造函数的explicit的,因此必须使用直接初始化语法,而不能进行拷贝赋值
tuple<size_t, size_t, size_t> threeD = {1,2,3}; // 错误
tuple<size_t, size_t, size_t> threeD{1,2,3}; // 正确
// make_tuple构造tuple
auto item = make_tuple("12345678", 3, 20.00);
17.1.2 访问tuple成员
pair是用first, second进行访问。而tuple由于成员数不是固定的,可以利用标准库函数模板get进行访问。
// 读
auto t = get<0>(threeD); // 返回threeD的第1个成员
auto val_4 = get<2>(someVal); // 返回someVal的第3个成员
// 写
get<2>(item) *= 0.8; // item的第3个成员 * 0.8
问题:如何获取一个实例化的tuple准确的类型细节信息?
使用2个辅助类模板tupe_size
typedef decltype(item) trans; // trans是item的类型
// 返回trans的类型对象中成员的数量
size_t sz = tuple_size<trans>::value; // 数量是3
// cnt与item第二个成员类型相同
tuple_element<1, trans>::type cnt = get<1>(item); // cnt 是一个int
<=> auto cnt = get<1>(item);
关系和相等运算符
成员类型、个数完全相同的两个tuple对象,才能进行比较;否则,就是错误行为。
注意:tuple定义了< 和 == 运算符,可以将tuple序列传递给算法,而且可以在无序容器中将tuple作为关键字类型。
tuple<string, string> duo("1", "2");
tuple<size_t, size_t> twoD(1,2);
bool b = (duo == twoD); // 错误,不能比较string和size_t -- 成员类型不同
tuple<size_t, size_t, size_t> threeD(1,2,3);
b = (twoD < threeD); // 错误,成员数量不同
tuple<size_t, size_t> origin(0, 0);
b = (origin < twoD); // 正确:b为true
17.1.2 使用tuple返回多个值
tuple有什么用途?
一个很重要的用途就是作为函数返回值,一次返回多个值。我们知道,函数是无法返回一个数组的,pair和tuple就成了一次返回多个类型不同的值的很好的方式。
17.2 bitset类型
头文件:bitset
bitset类专门用于处理二进制位集合,还能处理超过最长整型类型大小的位集合(超过64bit)。
17.2.1 定义和初始化bitset
bitset类是一个类模板,类似于array类,有固定大小。定义一个bitset时,通过模板参数指明它包含多少个二进制位。
bitset<32> bitvec(1u); // 32bit; 对应二进制数:0b 00..0 1 (0b表示是二进制数,数的前面31个0,最后一个1)
bitset的初始化方法
bitset<n> b; // b有n位,每一位均为0。该构造函数是一个constexpr
bitset<n> b(u);// b是unsigned long long值u的低n位的拷贝
bitset<n> b(s, pos, m, zero, one); // b是string s从位置pos开始m个字符的拷贝。s只能包含字符zero或one;如果s包含任何其他字符,构造函数抛出invalid_argument异常。
// 字符在b中分别保存为zeor和one。pos默认为0,m默认string::npos,zero默认为'0',one默认'1'
bitset<n> b(cp, pos, m, zero, one); // 与上一个构造函数相同。但从cp指向的字符数组中拷贝字符。如果m未提供,则cp必须指向一个C风格字符串('\0'结尾)。如果提供了m,则cp必须保证字符串至少包含m个zero或one字符
用unsigned值初始化bitset
用unsigned值初始化bitset,值会转化unsigned long long类型,并被当做位模式来处理。bitset中二进制位将是此模式的一个副本。
bitset值初始化,自动转换原则:bitset使用最少bit位,尽量确保信息不丢失;超过64bit的部分的高位会清0
示例
// 0xbeef = 0b 1011 1110 1110 1111 (实际占用2byte, 共16bit)
bitset<13> bitvec1(0xbeef); // 值超过存储位数的高位部分截断
bitset<20> bitvec2(0xbeef); // 超过值大小的位填充0
bitset<32> bitvec3(0xbeef); // 超过值大小的位填充0
bitset<128> bitvec4(~0ull); // 超过64bit的高位清0
cout << bitvec1 << endl; // 1111011101111
cout << bitvec2 << endl; // 00001011111011101111
cout << bitvec3 << endl; // 00000000000000001011111011101111
cout << bitvec4 << endl; // 00000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111,32个0,32个1
从一个string初始化bitset
二进制string或C字符串,初始化bitset 。
注意下标:string对应字符串,就是bitset二进制形式。
// 用整个字符串
bitset<32> bitvec5 ("1100"); // bitvec5 = 0b0..0 1100 (28个前导0) = 0xC
cout << bitvec5 << endl;
string str("1111111000000011001101");
bitset<32> bitvec6(str, 5, 4); // 用从str[5]开始的4个字符,作为二进制位,1100
bitset<32> bitvec7(str, str.size()-4); // 用从str[str.size()-4]开始的4个字符,也就是最后4个字符,作为二进制位,1101
17.2.2 bitset操作
bitset类支持位运算符,含义等同作用于unsigned。另外,bitset操作定义多种检测和设置1个或多个二进制位的方法。
// 位检测操作
b.any(); // b中是否存在置位的二进制位
b.all(); // b中所有位都置位了吗?
b.none(); // b中不存在置位的二进制位吗?
b.count(); // b中置位的位数
b.size(); // 一个constexpr函数,返回b中的(所有)位数
b.test(pos); // 若pos位置的位是置位的,则返回true;否则,返回false
// 置位、清除操作
b.set(pos, v); // 将pos处的位设置为v。v默认true
b.set(); // 将b所有位置位
b.clear(pos); // 清除pos处的位
b.clear(); // 清除b所有位
b.flip(pos); // 改变位置pos处的位状态,或改变b中每一位的状态
b.flip(); // 求b所有位的反
// 访问位
b[pos]; // <=> b.test(pos)
// 转换
b.to_ulong(); // 返回一个unsigned long,如果b中位模式不能放入指定结果类型,抛出overflow_error异常
b.to_ullong(); // 返回一个unsigned long long值,如果b中位模式不能放入指定结果类型,抛出overflow_error异常
b.to_string(zero, one); // 返回b对应的二进制字符串,zero、one默认为0和1
// 输入输出
os << b; // 将b中二进制打印为字符1或0,打印到流os
is >> b; // 从is读取字符存入b。当下一个字符不是1或0时,或者已经读入b.size()个位数时,读取停
示例
bitset<32> bitvec(1u); // 32位;低位为1,其余为0
bool is_set = bitvec.any(); // true
bool is_not_set = bitvec.none(); // false
bool all_set = bitvec.all(); // false
size_t onBits = bitvec.count(); // 返回1,总共只有1个bit位置位
size_t sz = bitvec.size(); // 返回32,总共的位数
bitvec.flip(); // 翻转bitvec中的所有位
bitvec.reset(); // 清除所有位
bitvec.set(); // 将所有位置位
// bitset单个位访问及操作
bitvec[0] = 0; // 复位第一位
bitvec[31] = bitvec[0]; // 将最后一位设置为与第一位一样
bitvec[0].flip(); // 翻转第一位
~bitvec[0]; // 取出第一位,然后翻转(注意不会影响原bitset对象)
bool b = bitvec[0]; // 将bitvec[0]转换为bool类型
// bitset提取值
unsigned long ulong = bitvec3.to_ulong();
cout << "ulong = " << ulong << endl;
// bitset的IO运算
bitset<16> bits;
cin >> bins;
cout << "bits: " << bits << endl; // 打印刚刚读取的内容
17.3 正则表达式
略
17.4 随机数
新标准出现前,C/C++依赖C库函数rand生成随机数,生成均匀分布的伪随机整数,范围:0~和系统相关最大值(>=32767)。
rand的问题:有许多程序需要不同范围的随机数,有些应用需要随机浮点数,有些需要非均匀分布的数。为解决这些问题而转换rand生成的随机数的范围、类型、分布时,常常会引入非随机性。
通过一组协作的类来解决这些问题:随机数引擎类(random-number engines)和随机数分布类(random-number distribution)
注意:C++不应该使用rand,而应该使用default_random_engine类和恰当的分别类对象。
17.4.1 随机数引擎和分布
头文件:random
随机数引擎是函数对象类,定义了一个调用运算符。运算符不接受参数,返回一个随机unsigned整数。
标准库定义了多个随机数引擎类,区别在于性能和随机性质量不同。每个编译器指定其中一个作为default_random_engine类型。
随机数引擎操作
Engine e; // 默认构造函数;使用该引擎类型默认的种子
Engine e(s); // 使用整型值s作为种子
e.seed(s); // 使用种子s重置引擎状态
e.min(); // 此引擎可生成的最小值和最大值
e.max();
Engine::result_type // 此引擎生成的unsigned整数类型
e.discard(u); // 将引擎推进u步;u类型为unsigned long long
使用示例
default_random_enginee e; // 随机数引擎对象
// 读取此引擎可生成的最小值和最大值
auto d1 = e.min();
auto d2 = e.max();
cout << "min = " << hex << d1;
cout << ", max = " << hex << d2 << endl;
// 生成10个随机数
for (size_t i = 0; i < 10; ++i) {
// 通过e()调用对象来生成下一个随机数
count << e() << " ";
}
打印结果
min = 1, max = 7ffffffe
41a7 10d63af1 60b7acd9 3ab50c2a 4431b782 1c06dac8 6058ed8 56e509fe 56f32f43 77a4044d
存在问题:可以看到,随机数引擎的输出值范围很大,是不能直接使用的。
分布类型和引擎
使用分布类型对象解决不能得到指定范围的数的问题。
注意:随机数发生器,通常指分布对象和引擎对象的组合。
uniform_int_distribution<unsigned> u(0, 9); // 生成[0, 9]均匀分布的随机数
default_random_engine e;
for (size_t i = 0; i < 10; ++i) {
// 将u作为随机数源
// 每个调用返回在指定范围内并服从均匀分布的值
cout << u(e) << " "; // 注意传递给分布对象的是引擎对象本身,如果传递e(),会出现编译错误
}
cout << endl;
运行结果:
0 1 7 4 5 2 0 6 6 9
比较随机数引擎和rand函数
default_random_engine对象输出类似random输出,不过随机数引擎生成的unsigned在一个系统定义的范围内,而rand生成的数字0~RAND_MAX之间。
引擎生成一个数值序列
一个给定的随机数发生器一直会发生相同的随机数序列。一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列。
示例:
// 这是一个错误的随机数序列生成方法
// 每次调用该函数都会生成相同的100个数
vector<unsigned> bad_randVec()
{
default_random_engin e;
uniform_int_distribution<unsigned> u(0, 9);
vector<unsigned> ret;
for (size_t i = 0; i < 100; ++i)
ret.push_back(u(e));
return ret;
}
vector<unsigned> v1(bad_randVec());
vector<unsgined> v2(bad_randVec());
cout << ((v1 == v2) ? "equal" : "not equal") << endl; // 打印"equal"
// 正确的随机数序列生成方法
vector<unsigned> good_randVec()
{
static default_random_engine e; // 注意这里的static
static uniform_int_distribution<unsigned> u(0, 9); // 注意这里的static
vector<unsigned> ret;
for (size_t i = 0; i < 100; ++i)
ret.push_back(u(e));
return ret;
}
设置随机数发生种子
随机数发生器如果使用相同(默认或指定)的种子,会生成相同的随机数序列。可以通过指定不同的种子,产生不同的随机数序列。
default_random_engine e1; // 使用默认种子
default_random_engine_e2(2147483646); // 使用指定种子
default_random_engine = e3; // 使用默认种子
e3.seed(32767); // 设置新种子
default_random_engine e4(32767); // 使用指定种子,与e3新设置种子一样
for (size_t i = 0; i != 100; ++i) {
// e1 e2生成相同随机数序列,因为使用的种子相同
if (e1() == e2())
cout << "unseed match at iterator: " << i << endl;
// e3 e4生成不同的随机数序列,因为使用的种子不同
if (e3() == e4())
cout << "seeded differs at iterator: " << i << endl;
}
17.4.2 其他随机数分布
随机数引擎(default_random_engine)生成的unsigned数,范围内的每个数被生成的概率都是相同的。而APP常需要不同类型、不同分布的随机数。标准库通过定义不同随机数分布对象来满足这两方面的要求。
生成随机数实数
生成[0, 1]的随机浮点数,可以使用uniform_real_distribution类型对象,用法与uniform_int_distribution类似。
default_random_engine e; // 生成无符号随机整数
// 0~1(包含)的均匀分布
uniform_real_distribution<double> u(0, 1);
for (size_t i = 0; i < 10; ++i)
cout << u(e) << " ";
使用分布的默认结果类型
每个分布模板都有一个默认的模板实参。
// 空<>表示希望使用默认结果类型
uniform_real_distribution<> u(0,1); // 默认生成double值
uniform_int_distribution<> u(0,100); // 默认生成int值
生成非均匀分布的随机数
normal_distribution 正态分布
default_random_engine e;
normal_distribution<> n(4, 1.5);
vector<unsigned> vals(9); // 9个元素均为0
for (size_t i = 0; i != 200; ++i) {
unsigned v = lround(n(e)); // 生成的随机数,舍入到最接近的整数
// 统计每个在范围内的数,出现的次数
if (v < vals.size())
++vals[v];
}
for (size_t j = 0; j != vals.size(); ++j)
cout << j << ": " << string(vals[j], '*') << endl;
bernouli_distribution类
bernouli_distribution是一个普通类,不接受模板参数。此分布总返回一个bool值,返回true概率是一个常数,默认0.5
由于引擎返回相同的随机数序列,所以必须在循环外声明引擎对象。否则,每步循环都会创建一个新引擎,从而每步都会生成相同的值。分布对象也是类似,需要保持状态。
string resp;
default_random_engine e; // e应保持状态,所有必须在循环外定义
bernouli_distribution b; // 默认是50/50的机会
do {
bool first = b(e); // 如果为true,则程序先行
cout << (first ? "We go first" : "You get to go first") << endl;
// 传递谁先行的指示,进行游戏
cout << ((play(first) ? "sorry, you lost" : "congrats, you won") << endl;
cout << "play gain? Enter 'yes' or 'no'" << endl;
} while(cin >> resp && resp[0] == 'y);
17.5 IO库再探
格式化字符串同C,略
控制bool类型及整型打印格式
// 控制bool格式
cout << true << endl; // 输出1
cout << boolalpha << true << endl; // 输出true
// 指定整型的进制,注意没有二进制控制方式。要输出二进制串,可以用bitset
cout << "default: " << 20 << " " << 1024 << endl; // default: 20 1024
cout << "octal: " << oct << 20 << " " << 1024 << endl; // octal: 24 2000
cout << "hex: " << hex << 20 << " " << 1024 << endl; // hex: 14 400
cout << "demical: " << dec << 20 << " " << 1024 << endl; // demical: 20 1024
cout << showbase; // 当打印整型值时,显式进制(前缀)
cout << "default: " << 20 << " " << 1024 << endl; // default: 20 1024
cout << "in octal: " << oct << 20 << " " << 1024 << endl; // in octal: 024 02000
cout << "in hex: " << hex << 20 << " " << 1024 << endl; // in hex: 0x14 0x400
cout << "in demical: " << dec << 20 << " " << 1024 << endl; // in demical: 20 1024
cout << noshowbase; // 恢复流状态
// 进制前缀字母打印大写
cout << uppercase << showbase << hex
<< "printed in hexadecimal: " << 20 << " " << 1024 // printed in hexadecimal: 0X14 0X400
<< nouppercase << noshowbase << dec << endl;
控制浮点数格式
可以控制浮点数输出三种格式:
- 以多高精度(多少个数字)打印浮点数;
- 数值是打印为十六进制、定点十进制还是科学计数法形式;
- 对于没有小数部分的浮点值,是否打印小数点;
默认情况下,浮点数按6位数字精度打印;如过没有小数,则不打印小数点;根据浮点数的值选择打印成定点十进制或科学记数法形式。标准库会选择一种可读性更好的格式:非常大和非常小的值,打印位科学计数法,其他值打印为定点十进制形式。
这里只讲指定打印精度的方法,其他略。
指定打印精度
通过IO对象的precision成员,或使用setprecision来改变精度。
precision成员是重载的,一个版本接受一个int值,用于设置精度值,并返回旧的精度值。另一个版本是不接受参数,返回当前精度值。
注意:所谓精度,指的是有效数字的个数。
头文件:iomanip
示例
// count.precision 返回当前精度值
cout << "Precision: " << cout.precision()
<< ", Value: " << sqrt(2.0) << endl;
// count.precision(12)将打印精度设置为12位数字
cout.precision(12);
cout << "Precision: " << cout.precision()
<< ", Value: " << sqrt(2.0) << endl;
// 另一种设置精度方法是设置setprecision操纵符
cout << setprecision(3);
cout << "Precision: " << cout.precision()
<< ", Value: " << sqrt(2.0) << endl;
17.5.2 未格式化的输入/输出操作
略
17.5.3 流随机访问
各种流类型通常都支持对流中数据的随机访问,可以重定位流,使之跳过一些数据,首先读取最后一行,然后读取第一行,一次类推。
标准库提供了一对函数来定位到流中给定的位置:seek 定位,tell 告诉我们当前位置。
使用seek/tell是否有意义,取决于流绑定到哪个设备。在大多数系统中,绑定到cin/cout/cerr和clog的流,不支持随机访问。
注意:istream, ostream类型不支持随机访问,本节内容只适用于fstream, sstream类型。
seek和tell函数
seek 通过到一个给定位置来重定位;
tell 标记当前位置;
标准库定义了2个版本:g版本用于输入流,p版本用于输出流。
逻辑上,只能对istream和派生自istream的ifstream和istringstream使用g版本。一个iostream、fstream、stringstream既能读又能写关联的流,这些类型对象,既能使用g版本,又能使用p版本。
注意:虽然seek和tell有“放置”和“获得”2个版本,但是流只维护一个标记,不存在写标记和读标记之分。这也就是说,程序可能需要在读写操作间进行切换。
如,输入流ifstream只能使用g版本,输出流ostringstream只能使用p版本。
tellg(); // 返回一个输入流中标记的当前位置
tellp(); // 返回一个输出流中标记的当前位置
seekg(pos); // 在一个输入流中将标记重定位到给定的绝对地址。pos通常是前一个tellg返回的值
seekp(pos); // 在一个输出流中将标记重定位到给定的绝对地址。pos通常是前一个tellp返回的值
seekg(off, from); // 在一个输入流中,将标记定位到from之前或之后off个字符。
seekp(off, from); // 在一个输出流中,将标记定位到from之前或之后off个字符。
// from可以是下列值之一
// beg, 偏移量相对于流开始位置
// cur,偏移量相对于流当前位置
// end,偏移量相对于流结尾位置
示例:按行读取一个文件
fstream 一次按行循环读取完一个文件,适用于小文件。
#include <fstream>
#include <iostream>
using namespace std;
string readFileByStream(const char* filepath)
{
string content;
char buf[256];
ifstream in("test.txt"); // 只读模式打开文件
if (!in.is_open()) {
cerr << "open file error: " << filepath << endl;
return content;
}
while (!in.eof()) {
in.getline(buf, sizeof(buf));
content += buf;
if (!in.eof()) { // ifstream::getline 会替换\n为\0, 还原时可根据需要加上\n
content += '\n';
}
}
return content;
}
示例:读写同一个文件
示例,给定一个要读取的文件,要实现:在此文件的末尾写入新的一行,这一行包含文件中每行的相对起始位置。
#if 1 // 用于创建,并输入文件初始内容
fstream fs("copyOut", fstream::out); // 只写模式打开文件
if (fs) {
cout << "Success to open file." << endl;
fs << "abcd" << endl;
fs << "efg" << endl;
fs << "hi" << endl;
fs << "j" << endl;
}
else cout << "fail to open file." << endl;
fs.close();
#endif
fstream inOut("copyOut",
fstream::ate | fstream::in | fstream::out ); // 以ate模式打开, 一开始旧定位到文件尾
if (!inOut) {
cerr << "Unable to open file!" << endl;
return EXIT_FAILURE;
}
auto end_mark = inOut.tellg(); // 记住原文件尾位置
inOut.seekg(0, fstream::beg); // 重定位到文件开始
size_t cnt = 0; // 字节累加器
string line; // 保存输入中的每行
while (inOut && inOut.tellg() != end_mark && getline(inOut, line)) {
cnt += line.size() + 1;
auto mark = inOut.tellg();
inOut.seekp(0, fstream::end);
inOut << cnt;
// 如果不是最后一行,打印一个分隔符
if (mark != end_mark) inOut << " ";
inOut.seekg(mark); // 恢复读位置
}
inOut.seekp(0, fstream::end); // 定位到文件尾部
inOut << "\n"; // 文件尾输出一个换行符
运行结果:
abcd
efg
hi
j
5 9 12 14
示例:求一个文件长度
利用tellg读一个一个文件长度。需要以ifstream::ate
模式打开文件,否则不会定位到文件末尾。
#include <iostream>
#include <fstream>
#include <sstream>
using namespace std;
int main()
{
// 构造时默认打开文件
// 别忘了使用 ate 模式, 打开文件时立即定位到文件末尾
std::ifstream file("testcase.txt", ifstream::ate | ifstream::in); // "testcase.txt"是测试文件
auto file_size = file.tellg();
// IO操作失败, 读取不到文件长度
// 如果打开文件失败, 或者无法定位到文件末尾, 都可能导致IO失败
if (fstream::pos_type(-1) == file_size) {
cout << "Can't get file size" << endl;
}
else {
cout << "file size = " << file_size << endl;;
}
// 将file_size转换为string类型
std::stringstream ss;
ss << file_size;
// 将string类型转换为long类型
long len;
ss >> len;
cout << "length = " << len << endl;
// 直接将file_size转型为string类型
long size = static_cast<long>(file_size);
cout << "size = " << size << endl;
file.close(); // 别忘了关闭文件
return 0;
}