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::value和tuple_element::type,来查询tuple成员的数量和类型:

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;

控制浮点数格式
可以控制浮点数输出三种格式:

  1. 以多高精度(多少个数字)打印浮点数;
  2. 数值是打印为十六进制、定点十进制还是科学计数法形式;
  3. 对于没有小数部分的浮点值,是否打印小数点;

默认情况下,浮点数按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;
}
posted @ 2021-03-23 17:18  明明1109  阅读(105)  评论(0编辑  收藏  举报