第十七章 标准库特殊设施
17.1 tuple类型
- 当希望将一些数据合成单一对象,但又不想麻烦地定义一个新数据结构来表示这些数据时,tuple非常有用。
- tuple是类似pair的模板。
tuple<size_t, size_t, size_t> threeD; //三个成员都设置为0//为每个成员提供初始值
tuple<string, vector<double>, int, list<int>>someVal("constants", {3.14,2,718},42,{0,1,2,3,4,5});//tuple的这个构造函数是explicit的
tuple<size_t,size_t,size_t> threeD = {1,2,3}; //错误
tuple<size_t,size_t,size_t> threeD{1,2,3}; //正确//表示书的交易记录的tuple,包含:ISBN、数量和每册书的价格
auto item = make_tuple("0-999-78345-X",3,20.00);
17.1.1 定义和初始化tuple
- 要访问一个tuple的成员,就要使用一个名为get的标准库函数模板。为了使用get,必须指定一个显式模板实参,它指出想要访问第几个成员。
auto book = get<0>(item); //返回item的第一个成员
auto cnt = get<1>(item); //返回item的第二个成员
auto price = get<2>(item)/cnt; //返回item的最后一个成员
get<2>(item) *= 0.8; //打折20%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
- 由于tuple定义了<和==运算符,可以将tuple序列传递给算法,并且可以在无序容器中将tuple作为关键字类型。
tuple<string, string> duo("1","2");
tuple<size_t,size_t> twoD(1,2);
bool b = (duo == twoD); //错误:不能比较size_t和string
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的一个常见用途是从一个函数返回多个值。
17.2 bitset类型
17.2.1 定义和初始化bitset
- bitset类是一个类模板,类似array类,具有固定的大小,当定义一个bitset时,需要声明它包含多少个二进制位。
bitset<32> bitvec(1U); //32位;低位为1,其他位为0
- 当使用一个整型值来初始化bitset时,此值将被转换为unsigned long long类型并被当作位模式来处理。
//bitvec1比初始值小;初始值中的高位被丢弃
bitset<13> bitvec1(0xbeef); //二进制位序列为1111011101111
//bitvec2比初始值大;它的高位被置为0
bitset<20> bitvec2(0xbeef); //二进制位序列00001011111011101111
//在64位机器中,long long OULL是一个64个0比特,因此~0ULL是64个1
bitset<128> bitvect3(~0ULL); //0~63位为1;64~127位为0
- string的下标编号习惯与bitset恰好相反:string中下标最大的字符(最右字符)用来初始化bitset中的低位(下标为0的二进制位)。当用一个string初始化一个bitset时,要记住这个差别。
bitset<32> bitvec4("1100"); //2,3两位为1,剩余两位为0string str("1111111000000011001101");
bitset<32> bitvec5(str,5,4); //从str[5]开始的四个二进制位,1100
bitset<32> bitvec6(str,str.size()-4); //使用最后四个字符
17.2.2 bitset操作
- 如果bitset中的值不能放入给定类型中,to_ulong和to_ullong操作会抛出一个overflow_error异常。
bitset<32> bitvec(1U); //32位;低位为1,剩余位为0
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[0] = 0; //将第一位复位
bitvec[31] = bitvec[0]; //将最后一位设置为与第一位一样
bitvec[0].flip(); //翻转第一位
~bitvec[0]; //等价操作,也是翻转第一位
bool b = bitvec[0]; //将bitvec[0]的值转换为bool类型
unsinged long ulong = bitvec3.to_ulong();
cout<<"ulong = " << ulong <<endl;
//bitset中的值的大小,不能大于转换的类型
//否则会抛出overflow_error异常
bitset<16> bits;
cin >> bits; //从cin读取最多16个0或1
cout << "bits: "<< bits <<endl; //打印刚刚读取的内容
bool status;
//使用位运算符的版本
unsigned long quizA = 0; //此值被当做位集合使用
quizA |= 1UL << 27; //指出第27个学生通过了测试
status = quitzA & (1UL <<27); //检查第27个学生是否通过了测试
quizA &= ~(1UL << 27); //第27个学生未通过测试//使用标准库类bitset
bitset<30> quizB; //每个学生分配一位,所有位都被初始化为0
quizB.set(27); //指出第27个学生通过了测试
status = quizB[27]; //检查第27个学生是否通过了测试
quizB.reset(27); //第27个学生未通过测试
17.3 正则表达式
17.3.1 使用正则表达式库
- 正则表达式是一种描述字符序列的方法,是一种及其强大的计算工具。
//查找不在字符c之后的字符串ei
string pattern("[^c]ei");
//我们需要包含pattern的整个单词
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
//regex:表示有一个正则表达式的类
regex r(pattern); //构造一个用于查找模式的regex
//smatch:容器类,保存在string中搜索的结果
smatch results; //定义一个对象保存搜索结果string test_str = "receipt freind theif receive";
//regex_search:寻找第一个与正则表达式匹配的子序列
//用r在test_str中查找与pattern匹配的子串
if(regex_search(test_str, results, r))//如果有匹配子串cout << results.str() << endl; //打印匹配的单词
- 指定regex对象的选项:指定一些标志来影响regex如何操作。
//识别扩展名
//一个或多个字母或数字字符后接一个'.'再接"cpp"或"cxx"或"cc"
//regex::icase在匹配中忽略大小写
regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$", regex::icase);
smatch results;
string filename;while(cin >> filename)if(regex_search(filename, results, r))cout << results.str() << endl;//打印匹配结构
- 一个正则表达式的语法是否正确是在运行时解析的。如果编写的正则表达式存在错误,则在运行时标准库会抛出一个类型为regex_error的异常。
try {//错误:alnum漏掉了右括号,构造函数会抛出异常regex r("[[:alnum:]+\\.(cpp|cxx|cc)$", regex::icase);
} catch (regex_error e){ cout << e.what() << "\ncode:" << e.code() << endl; }
- 正则表达式类和输入序列类型
regex r("[[:alnum:]]+\\.(cpp|cxx|cc)$",regex::icase);
smatch results; //将匹配string输入序列,而不是char*
//cmatch results; //将匹配字符数组输入序列
if( regex_search("myfile.cc", results, r)) //错误:输入为char*cout << results.str() << endl;
- 避免创建不必要的正则表达式,特别是在一个循环中使用正则表达式,应该在循环外创建它,而不是在每步迭代时都编译它。
17.3.2 匹配与Regex迭代器类型
- 使用sregex_iterator来获得所有匹配。
string file("Hello cei world freind meizhu");
//查找前一个字符不是c的字符串ei
string pattern("[^c]ei");
pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";
regex r(pattern, regex::icase);
//它将反复调用regex_search来查找文件中的所有匹配
for(sregex_iterator it(file.begin(), file.end(), r), end_it; it != end_it; ++it)cout << it->str() << endl; //匹配的单词
17.3.3使用子表达式
- 正则表达式语法通常用括号表示子表达式。
//两个子表达式:1、点之前表示文件名的部分,2、表示文件扩展名
regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$",regex::icase);
smatch results;
string filename;
while(cin >> filename)if(regex_search(filename, results, r))cout << results.str(1) << endl; //打印第一个子表达式
- 子表达式的一个常见用途是验证必须匹配特定格式的数据。
"(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ])?(\\d{4})";
17.3.4使用regex_replace
- 正则表达式不仅可以用来查找一个给定序列,还可以用来将找到的序列替换为另一个序列,可以调用regex_replace。
using namespace regex_constants;
int main() {string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ])? (\\d{4})"; regex r(phone); //regex对象,用于查找我们的模式string fmt = "$2.$5.$7"; //将号码格式改为ddd.ddd.ddddstring fmt2 = "$2. $5. $7"; string number = "(908) 555-1800";cout << regex_replace(number, r, fmt) << endl;cout << regex_replace(number, r, fmt2, format_no_copy) << endl;number = "(08) 555-1800";cout << regex_replace(number, r, fmt) << endl;//format_no_copy默认不匹配就原样输出cout << regex_replace(number, r, fmt2, format_no_copy) << endl;
}
17.4 随机数
- C++程序不应该使用库函数rand,而应该使用default_random_engine类和恰当的分布类对象。
#include <iostream>
#include <random>
using namespace std;int main()
{default_random_engine e; //生成随机无符号数for(size_t i = 0; i < 5; ++i)cout << e() << " ";cout << endl;default_random_engine e2;for(size_t i = 0; i < 5; ++i)cout << e2() << " ";
}
17.4.1 随机数引擎和分布
-
随机数引擎是函数对象类,它们定义了一个调用运算符,该运算符不接受参数并返回一个随机unsigned整数。
-
分布类型定义了一个调用运算符,接受一个随机引擎作为参数。分布对象使用它的引擎参数生成随机数,并将其映射到指定的分布。
//生成0到9之间(包含)均匀分布的随机数
uniform_int_distribution<unsigned> u(0,9);
default_random_engine e; //生成无符号随机整数
for(size_t i = 0; i < 10; ++i)//将u作为随机数源//每个调用返回执行范围内并服从均匀分布的值cout << u(e) << " ";
- 当我们说随机数发生器时,是指分布对象和引擎对象的组合。
- 一个给定的随机数发生器一直会生成相同的随机数序列。一个函数如果定义了局部的随机数发生器,应将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列。
//每次调用这个函数都会生成相同的100个数!
vector<unsigned> bad_randVec() {default_random_engine 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;
}int main() {vector<unsigned> v1(bad_randVec());vector<unsigned> v2(bad_randVec());//将打印“equal”cout << ((v1 == v2) ? "equal":"not equal") << endl;
}
- 为引擎设置种子有两种方式:在创建引擎对象时提供种子,或者调用引擎的seed成员。
default_random_engine e1; //使用默认种子
default_random_engine e2(3147442); //使用给定的种子
default_random_engine e3; //使用默认种子
e3.seed(32767); //调用seed设置一个新种子
default_random_engine e4(32767); //将种子设置为32767
for(size_t i = 0; i != 100; ++i) {if(e1() == e2())cout << "unseeded match at literation:" << i << endl;if(e3() != e4())cout << "seeded differs at literation:" << i << endl;
}
- 如果程序作为一个自动过程的一部分反复运行,将time的返回值作为种子的方式就无效了;它可能多次使用的都是相同的种子。
//以时间为种子:只适用于间隔为秒级或更长的应用
default_random_engine e(time(0));
17.4.2 其他随机数分布
- 当希望使用默认随机数类型时要记得在模板名之后使用空尖括号。
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值
//生成非均匀分布的随机数
default_random_engine e; //生成随机整数
normal_distribution<> n(4, 1.5); //均值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;
- 由于引擎返回相同的随机数序列,所以必须在循环外声明引擎对象。否则,每步循环都会创建一个新引擎,从而每步循环都会生成相同的值。类似的,分布对象也要保持状态,因此也应该在循环外定义。
string resp;
default_random_engine e; //e应保持状态,所以必须在循环外定义
bernoulli_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 again?Enter 'yes' or 'no'" << endl;
} while(cin >> resp&&resp[0] == 'y');
17.5 IO库再探
17.5.1 格式化输入与输出
- 操纵符(如endl)用于两大类输出控制:控制数值的输出形式以及控制补白的数量和位置。
- 当操纵符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。
- 有些情况下,将流的状态置于一个非标准状态可能会导致错误。因此,通常最好在不再需要特殊格式时尽快将流恢复到默认状态。
- 操纵符boolalpha/noboolalpha控制布尔值的格式,使其输出true/false或1/0。
cout << "default bool values: " << true << " " << false<< "\nalpha bool values: " << boolalpha << true << " " << false << endl;
bool bool_val = false;
cout << bool_val << noboolalpha; // 恢复默认状态
cout << endl;
- 操纵符hex、oct和dec指定整型值的进制,但只影响整型运算对象,浮点值的表示形式不受影响。
cout << "default: " << 20 << " " << 1024 << endl;
cout << "octal: " << oct << 20 << " " << 1024 << endl;
cout << "hex: " << hex << 20 << " " << 1024 << endl;
cout << "decimal: " << dec << 20 << " " << 1024 << endl;
- 当对流应用showbase操纵符时,会在输出结果中显示进制,它遵循与整型常量中指定进制相同的规范。操纵符noshowbase恢复cout的状态,从而不再显示整型值的进制。
cout << showbase; // show the base when printing integral values
cout << "default: " << 20 << " " << 1024 << endl;
cout << "in octal: " << oct << 20 << " " << 1024 << endl;
cout << "in hex: " << hex << 20 << " " << 1024 << endl;
cout << "in decimal: " << dec << 20 << " " << 1024 << endl;
cout << noshowbase; // reset the state of the stream
cout << uppercase << showbase << hex<< "printed in hexadecimal: " << 20 << " " << 1024 << nouppercase << noshowbase << dec << endl;
- 通过调用IO对象的precision成员或使用setprecision操纵符来改变精度,其定义在头文件iomanip中。
//cout.precision返回当前精度值
cout << "Precision: "<< cout.precision() << ", Value: " << sqrt(2.0)<< endl;
//cout.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;
- 除非需要控制浮点数的表示形式(如按列打印数据或打印表示金额或百分比的数据,否则由标准库选择记数法是最好的方式。)
cout << "default format: " << 100 * sqrt(2.0) << '\n'<< "scientific: " << scientific << 100 * sqrt(2.0) << '\n'<< "fixed decimal: " << fixed << 100 * sqrt(2.0) << '\n'<< "hexadecimal: " << hexfloat << 100 * sqrt(2.0) << '\n'<< "use defaults: " << defaultfloat << 100 * sqrt(2.0) << "\n\n";cout << uppercase<< "scientific: " << scientific << sqrt(2.0) << '\n'<< "fixed decimal: " << fixed << sqrt(2.0) << '\n'<< "hexadecimal: " << hexfloat << sqrt(2.0) << "\n\n"<< nouppercase;
- C++11:使用hexfloat强制浮点数使用十六进制格式,使用defaultfloat将流恢复到默认状态——根据要打印的值选择记数法。
double pi = 3.14;
cout << pi << " " << hexfloat << pi << defaultfloat << " " << pi << endl;
- showpoint强制打印小数点,noshowpoint恢复默认行为。
- 输出补白:
setw:指定下一个数字或字符串值的最小空间。不改变输出流的内部状态,值决定下一个输出的大小。
left:左对齐输出。
right:右对齐输出。(默认格式)
internal:控制负数的符号的位置,它左对齐符号,右对齐值,用空格填满所有中间空间。
setfill:允许指定一个字符代替默认的空格来补白输出。
int i = -16;
double d = 3.14159;// pad the first column to use a minimum of 12 positions in the output
cout << "i: " << setw(12) << i << "next col" << '\n'<< "d: " << setw(12) << d << "next col" << '\n';// pad the first column and left-justify all columns
cout << left << "i: " << setw(12) << i << "next col" << '\n'<< "d: " << setw(12) << d << "next col" << '\n'<< right; // restore normal justification// pad the first column and right-justify all columns
cout << right<< "i: " << setw(12) << i << "next col" << '\n'<< "d: " << setw(12) << d << "next col" << '\n';// pad the first column but put the padding internal to the field
cout << internal<< "i: " << setw(12) << i << "next col" << '\n'<< "d: " << setw(12) << d << "next col" << '\n';// pad the first column, using # as the pad character
cout << setfill('#')<< "i: " << setw(12) << i << "next col" << '\n'<< "d: " << setw(12) << d << "next col" << '\n'<< setfill(' '); // restore the normal pad character
- noskipws会令输入运算符读取空白符,使用skipws恢复默认行为。
char ch;
cin >> noskipws; //设置cin读取空白符
while (cin >> ch)cout << ch;
cout << endl;
cin >> skipws; //将cin恢复到默认状态
17.5.2 未格式化的输入/输出操作
- 标准库提供了三种方法退回字符:
peek
unget
putback - 标准库不保证在中间不进行读取操作的情况下能连续调用putback或unget。
- 小心:低层函数容易出错。一般情况下,主张使用标准库提供的高层抽象。
17.5.3 流随机访问
- 随机IO本质上是依赖于系统的。为了理解如何使用这些特性,必须查询系统文档。
- 由于只有单一的标记,因此只要在读写操作间切换,就必须进行seek操作来重定位标记。