前言
本章介绍了一些特殊IO操作和四个标准库类型:tuple、bitset、正则表达式和随机数。内容还是挺多的,如果想了解更多详细知识,建议自行查看书籍,这里主要介绍一些细节。
最后,如果有什么理解不对的地方,希望大家不吝赐教,谢谢!
十四、标准库特殊设施
标准库占据了新标准文本将近三分之二的篇幅,虽然我们不能详细介绍所有标准库设施,但仍然有一些标准库设施在很多应用中都是有用的:tuple、bitset、正则表达式以及随机数。还有一些IO库功能:格式控制、未格式化IO和随机访问。
tuple类型
tuple是类似于pair的模板。每个pair的成员类型都不相同,但每个pair都恰好有两个成员。不同tuple类型的成员类型也不相同,但一个tuple可以有任意数量的成员。每个确定的tuple类型的成员数目是固定的,但一个tuple类型的成员数目可以与另一个tuple类型不同。定义在头文件tuple中。
tuple<T1,T2,...,Tn> t;
我们可以将tuple看作一个“快速而随意”的数据结构,可以把tuple看成一个可以存储任意类型的容器。
定义和初始化tuple
touple<size_t,size_t,size_t> threeD; //三个成员都设置为0 touple<string,vector<double>,int,list<int>>someVal("constants",{3.14,2.718},42,{0,1,2})//只能用直接初始化方法 //或者用make_tuple auto item=make_tuple("0-999-78345-X",3,20.00);
访问tuple的成员
要访问一个tuple的成员,就要使用一个名为get的标准库函数模板。为了使用get,我们必须指定一个显式模板实参,它指出我们想要访问第几个成员。我们传递给get一个tuple对象,它返回指定成员的引用。
auto book=get<0>(item); //返回item的第一个成员
尖括号中的值必须是一个整型常量表达式。
还有两个辅助类模板来查询tuple成员的数量和类型:tuple_size和tuple_element,但我们需要知道一个tuple对象的类型,使用decltype来确定一个对象的类型。其中tuple_size有一个名为value的public static数据成员,它表示给定tuple中成员的数量。而tuple_element模板除了tuple类型外,还接受一个索引值。它有一个名为type的public类型的成员,表示给定tuple类型中指定成员的类型。类似get,tuple_element所使用的索引也是从0开始计数的。
typedef decltype(item) trans; size_t sz=tuple_size<trans>::value; tuple_element<1,trans>::type cnt=get<1>(item);
关系和相等运算符
只有两个tuple具有相同数量的成员时,我们才可以比较它们。
由于tuple定义了<和==运算符,我们可以将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开始的二进制位被称为低位,编号到31结束的二进制位被称为高位。
用unsigned值初始化bitset
当我们使用一个整型值来初始化bitset时,此值将被转换为unsigned long long类型并被当作位模式来处理。bitset中的二进制将是此模式的一个副本。
//bitvecl比初始值小,初始值中的高位被丢掉 bitset<13> bitvecl(0xbeef); //二进制位序列1111011101111 //bitvec2比初始值大,它的高位被置为0 bitset<20> bitvec2(0xbeef); //二进制位序列为00001011111011101111 //在64位机器中,long long 0ULL是64个0比特,因此~0ULL是64个1 bitset<128> bitvec3(~0ULL); //0~63位为1,63~127位为0
从一个string初始化bitset
我们可以从一个string或一个字符数组来初始化bitset。
bitset<32> bitvec4("1100"); //2、3两位为1,剩余两位为0
如果string包含的字符数比bitset少,则bitset的高位被置为0。string的下标编号习惯与bitset恰好相反:string中下标最大的字符(最右字符)用来初始化bitset中的低位(下标为0的二进制位)。
还可以不必使用整个string来作为bitset的初始值:
string str("1110101010101101"); bitset<32> bitvec5(str,2,3); //从str[2]开始的3个二进制位 bitset<32> bitvec6(str,str.size()-4); //使用最后4个字符
bitset操作
count、size、all、any和none等几个操作都不接受参数,返回整个bitset的状态。其他操作——set、reset和flip则改变bitset的状态。改变bitset状态的成员函数都是重载的。
提取bitset的值
to_ulong和to_ullong操作都返回一个值,保存了与bitset对象相同的位模式。只有当bitset的大小小于等于对应的大小(to_ulong为unsigned long,to_ullong为unsigned long long)时,我们才能使用这两个操作。
unsigned long ulong=bitvec3.to_ulong(); cout<<"ulong="<<ulong<<endl;
bitset的IO运算符
输入运算符从一个输入流读取字符,保存到一个临时的string对象中。直到读取的字符数达到对应bitset的大小时,或是遇到不是1或0的字符时,或是遇到文件尾或输入错误时,读取过程才停止。如果读取的字符小于bitset的大小,则与往常一样,高位被置为0。
正则表达式
正则表达式是一种描述字符序列的方法,是一种极其强大的计算工具。重点介绍使用C++正则表达式库(RE库)。它是新标准库的一部分。RE库定义在头文件regex中,它包含多个组件:
- regex:表示有一个正则表达式的类
- regex_match:将一个字符序列与一个正则表达式匹配
- regex_search:寻找第一个与正则表达式匹配的子序列
- regex_replace:使用给定格式替换一个正则表达式
- sregex_iterator:迭代器适配器,调用regex_search来遍历一个string中所有匹配的子串
- smatch:容器类,保存在string中搜索的结果
- ssub_match:string中匹配的子表达式的结果
regex_search和regex_match的参数:(seq,m,r,mft)在字符序列seq中查找regex对象r中的正则表达式。seq可以是一个string、表示范围的一对迭代器以及一个指向空字符结尾的字符数组的指针,m是一个match对象,用来保存匹配结果的相关细节。
(seq,r,mft):mft是一个可选的regex_constants::match_flag_type值。
string pattern("[^c]ei"); //[^c]没有c字符,[^c]ei表示ei的前一个字符不是c的字符串 pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";//[[:alpha:]]*表示任意0个或多个 regex r(pattern); //初始化正则表达式 smatch results; //定义搜索结果 string test_str = "receeipt freeind thceif receive"; //从字符串的第一个字符开始匹配,当找到满足条件时,立即停止 if (regex_search(test_str, results, r)) cout << results.str() << endl;
指定regex对象的选项
当我们定义一个regex或是对一个regex调用assign为其赋予新值时,可以指定一些标志来影响regex如何操作。这些标志控制regex对象的处理过程。
指定或使用正则表达式时的错误
一个正则表达式的语法是否正确是在运行时解析的。如果我们编写的正则表达式存在错误,则在运行时标准库会抛出一个类型为regex_error的异常。
建议:避免创建不必要的正则表达式。
正则表达式类和输入序列类型
输入可以是普通char数据或wchar_t数据,字符可以保存在标准库string中或是char数组。保存输出的有:smatch表示string类型的输入序列;cmatch表示字符数组序列;wsmatch表示宽字符串(wstring)输入;而wcmatch表示宽字符数组。重点在于我们使用的RE库类型必须与输入序列类型匹配。
匹配与Regex迭代器类型
对于regex_search只能匹配第一个匹配的单词,使用sregex_iterator来获得所有匹配。regex迭代器是一种迭代器适配器,被绑定到一个输入序列和一个regex对象上。当我们将一个sregex_iterator绑定到一个string和一个regex对象时,迭代器会自动定位到给定string中第一个匹配位置。当我们递增迭代器时,它调用regex_search在输入string中查找下一个匹配。
使用sregex_iterator
string pattern("[^c]ei"); //[^c]没有c字符,[^c]ei表示ei的前一个字符不是c的字符串 pattern = "[[:alpha:]]*" + pattern + "[[:alpha:]]*";//[[:alpha:]]*表示任意0个或多个 regex r(pattern,regex::icase); //初始化正则表达式,忽略大小写 smatch results; //定义搜索结果 string test_str = "receeipt freeind thceif receive"; /* //从字符串的第一个字符开始匹配,当找到满足条件时,立即停止 if (regex_search(test_str, results, r)) cout << results.str() << endl; */ //它将反复调用regex_search来寻找文件中的所有匹配 for (sregex_iterator it(test_str.begin(), test_str.end(), r), end_it; it != end_it; ++it) cout << it->str() << endl;
使用匹配数据
smatch操作,也适用于cmatch、wsmatch和对应的csub_match、wssub_match和wcsub_match:m.ready()、m.size()、m.empty()、m.prefix()、m.suffix()、m.format()等。
使用子表达式
正则表达式中的模式通常包含一个或多个子表达式,一个子表达式是模式的一部分,本身也具有意义。正则表达式语法通常用括号表示子表达式。
//r有两个子表达式:第一个是点之前表示文件名的部分,第二个表示文件扩展名 regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$",regex::icase);
现在我们的模式包含两个括号括起来的子表达式:
- ([[:alnum:]]+):匹配一个或多个字符的序列
- (cpp|cxx|cc):匹配文件扩展名
- \\表示括号是我们的模式的一部分而不是特殊字符
子表达式用于数据验证
子表达式的一个常见用途是验证必须匹配特定格式的数据。
使用regex_replace
当我们希望在输入序列中查找并替换一个正则表达式时,可以调用regex_replace。它接受一个输入字符序列和一个regex_replace,还接受一个描述我们想要的输出形式的字符串。
随机数
对于之前用rand来生成随机数,此函数生成均匀分布的伪随机整数,每个随机数的范围在0和一个系统相关的最大值(至少为32767)之间。rand函数有一些问题:即使不是大多数,也有很多程序需要不同范围的随机数。
定义在头文件random中的随机数库通过一组协作的类来解决这些问题:随机数引擎类和随机数分布类。一个引擎类可以生成unsigned随机数序列,一个分布类使用一个引擎类生成指定类型的、在给定范围内的、服从特定概率分布的随机数。
C++程序不应该使用库函数rand,而应使用default_random_engine类和恰当的分布类对象。
随机数引擎和分布
随机数引擎是函数对象类,它们可以定义一个调用运算符,该运算符不接受参数并返回一个随机unsigned整数。我们可以通过调用一个随机数引擎对象来生成原始随机数:
default_random_engine e; //生成随机无符号数 for(size_t i=0;i<10;i++) {//e()“调用”对象来生成下一个随机数 cout<<e()<<" "; }
对于大多数场合,随机数引擎的输出是不能直接使用的,这也是为什么早先我们称之为原始随机数。问题出在生成的随机数的值范围通常与我们需要的不符,而正确转换随机数的范围是极其困难的。
分布类型和引擎
为了得到在一个指定范围内的数,我们使用一个分布类型的对象:
//生成0到9之间(包含)均匀分布的随机数 uniform_int_distribution<unsigned> u(0,9); default_random_engine e; for(size_t i=0;i<10;i++) cout<<u(e)<<" ";
当我们说随机数发生器时,是指分布对象和引擎对象的组合。
引擎生成一个数值序列
一个给定的随机数发生器一直会生成相同的随机数序列。一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列。
设置随机数发生器种子
我们的程序调试完毕,我们通常希望每次运行程序都会生成不同的随机结果,可以通过提供一个种子(seed)来达到这一目的。种子就是一个数值,引擎可以利用它从序列中一个新位置重新开始生成随机数。为引擎设置种子有两种方式:在创建引擎对象时提供种子,或者调用引擎的seed成员。
选择一个好的种子,与生成好的随机数所涉及的其他大多数事情相同,是极其困难的。可能最常用的方法是调用系统函数time。这个函数定义在头文件ctime中,它返回从一个特定时刻到当前经过了多少秒。函数time接受单个指针参数,它指向用于写入时间的数据结构。
如果程序作为一个自动过程的一部分反复运行,将time的返回值作为种子的方式就无效了;它可能多次使用的都是相同的种子。
其他随机数分布
随机数引擎生成unsigned数,范围内的每个数被生成的概念都是相同的。而应用程序常常需要不同类型或不同分布的随机数。标准库通过定义不同随机数分布对象来满足这两方面的要求,分布对象和引擎对象协同工作,生成要求的结果。
生成随机实数
程序常需要一个随机浮点数的源,特别是,程序经常需要0到1之间的随机数。
使用分布的默认结果类型
分布类型都是模板,具有单一的模板类型参数,这些分布类型要么生成浮点类型,要么生成整数类型。每个分布模板都有一个默认模板实参,生成浮点值得分布类型默认生成double值,而生成整型值的分布默认生成int值。由于分布类型只有一个模板参数,因此当我们希望使用默认随机数类型时要记得在模板名之后使用空尖括号。
生成非均匀分布的随机数
除了正确生成在指定范围内的数之外,新标准库的另一个优势是可以生成非均匀分布的随机数。比如正态分布等。
bernoulli_distribution类
有一个分布不接受模板参数,即bernoulli_distribution,因为它是一个普通内,而非模板。此分布总是返回一个bool值。它返回true的概率是一个常数,此概率的默认值是0.5。
由于引擎返回相同的随机数序列,所以我们必须在循环外声明引擎对象。否则,每步循环都会创建一个新引擎,从而每步循环都会生成相同的值。类似的,分布对象也要保持状态,因此也应该在循环外定义。
bernoulli_distribution可以选择先方一方的概率:bernoulli_distribution b(.55);如果b这样定义,则程序有55/45的机会先行。
IO库再探
更特殊的IO库特性:格式控制、未格式化IO和随机访问。
格式化输入与输出
除了条件状态外,每个iostream对象还维护一个格式状态来控制IO如何格式化的细节。格式状态控制格式化的某些方面,如整型值是几进制、浮点值的精度、一个输出元素的宽度等。
标准库定义了一组操纵符来修改流的格式状态。一个操作符是一个函数或是一个对象,会影响流的状态,并能用作输入或输出运算符的运算对象。类似输入和输出运算符,操纵符也返回它所处理的流对象,因此我们可以在一条语句中组合操纵符和数据。endl就是一个操作符。
很多操纵符改变格式状态
操纵符用于两大类输出控制:控制数值的输出形式以及控制补白的数量和位置,大多数改变格式状态的操纵符都是设置/复原成对的:一个操纵符用来将格式状态设置为一个新值,而另一个用来将其复原,恢复为正常的默认格式。
当操作符改变流的格式状态时,通常改变后的状态对所有后续IO都生效。
控制布尔值的格式
操纵符改变对象的格式状态的一个例子是boolalpha操纵符。默认情况下,bool值打印为1或0.一个true值输出为整数1,而false输出为0。我们可以通过对流使用boolalpha操纵符来覆盖这种格式。即采用了boolalpha后,会打印出true和false而不是1和0。
指定整数值的进制
我们可以使用操纵符hex、oct和dec将其改为十六进制、八进制或是改回十进制。注意,类似于boolalpha,这些操纵符也会改变格式状态。它们会影响下一个和随后所有的整形输出,直至另一个操纵符又改变了格式为止。
操纵符hex、oct和dec只影响整形运算对象,浮点值的表示形式不受影响。
在输出中指出进制
如果我们需要打印八进制或十六进制值,应该使用showbase操纵符。当对流应用showbase操纵符时,会在输出结果中显式进制,它遵循与整型常量中指定进制相同的规范:
- 前导0x表示十六进制
- 前导0表示八进制
- 无前导字符串表示十进制
控制浮点数格式
可以控制浮点数输出三种格式:
- 以多高精度(多少个数字)打印浮点值
- 数值是打印为十六进制、定点十进制还是科学记数法形式
- 对于没有小数部分的浮点值是否打印小数点
默认情况下,浮点值按六位数字精度打印;如果浮点值没有小数部分,则不打印小数点;根据浮点数的值选择打印成定点十进制或科学计数法。
指定打印精度
当打印时,浮点值按当前精度舍入而非截断。我们可以通过调用IO对象的preision成员或使用setprecision操纵符来改变精度。precision成员是重载的,一个版本接受一个int值,将精度设置为此值,并返回旧精度值。另一个版本不接受参数,返回当前精度值,setprecision操纵符接受一个参数,用来设置精度。定义在头文件iomanip中。
指定浮点计数法
操纵符scientific改变流的状态来使用科学计数法。操纵符fixed改变流的状态来使用定点十进制。在新标准中,通过使用hexfloat也可以强制浮点数使用十六进制格式。还提供另一个名为defaultfloat的操纵符,它将流恢复到默认状态——根据要打印的值选择记数法。
打印小数点
showpoint操纵符强制打印小数点。
输出补白
当按列打印数据时,我们常常需要非常精细地控制数据格式。标准库提供了一些操纵符帮助我们完成所需的控制:
- setw指定下一个数字或字符串值的最小空间
- left表示左对齐输出
- right表示右对齐输出,右对齐是默认格式
- internal控制负数的符号的位置,它左对齐符号,右对齐值,用空格填满所有中间空间。
- setfill允许指定一个字符代替默认的空格来补白输出
setw类似endl,不改变输出流的内部状态。它只决定下一个输出的大小。
控制输入格式
默认情况下,输入运算符会忽略空白符。操纵符noskipws会令输入运算符读取空白符,而不是跳过它们。为了恢复默认行为,我们可以使用skipws操纵符。
未格式化的输入/输出操作
标准库还提供了一组低层操纵,支持未格式化IO。这些操纵允许我们将一个流当作一个无解释的字节序列处理。
单字节操作
有几个未格式化操作每次一个字符地处理流,它会读取而不是忽略空白符。例如可以使用IO操作get和put来读取和写入一个字符。
将字符放回输入流
有时我们需要读取一个字符才能指定还未处理好它,这时,我们希望将字符流放回流中。标准库提供了三种方法退回字符:
- peek返回输入流中下一个字符的副本,但不会将它从流中删除,peek返回的值仍然留在流中。
- unget使得输入流向后移动,从而最后读取的值又回到流中,即使我们不知道最后从流中读取什么值,仍然可以调用unget。
- putback是更特殊版本的unget:它退回从流中读取的最后一个值,但它接受一个参数,此参数必须与最后读取的值相同。
从输入操作返回的int值
函数peek和无参的get版本都以int类型从输入流返回一个字符。原因是:可以返回文件尾标记。
多字节操作
get和getline函数接受相同的参数,它们的行为类似但不相同,两个函数差别是处理分隔符的方式:get将分隔符留作istream中的下一个字符,而getline则读取并丢弃分隔符。
流随机访问
标准库提供了一对函数,来定位(seek)到流中给定的位置,以及告诉(tell)我们当前位置。
由于istream和ostream类型通常不支持随机访问,所以本节剩余内容只适用于fstream和sstream。
由于只有单一的标记,因此只要我们在读写操作间切换,就必须进行seek操作来重定位标记。
重定位标记
seek函数有两个版本:一个移动到文件中的“绝对”地址;另一个移动到一个给定位置的指定偏移量。
访问标记
函数tellg和tellp返回一个pos_type值,表示流的当前位置。tell函数通常用来记住一个位置,以便稍后再定位回来。