这一章介绍了标准库中另外的一些之前没提到但也很实用的东西,读起来并不困难。其中17.1的tuple很适合快速组织数据,17.3的正则表达式可以快速处理字符串,17.4和17.5的随机数部分和IO流部分都是非常常用的特性,可以有效提高我们的开发效率。
17.1 tuple类型
- tuple是类似pair的类型,可以简单地保存类型不同的数量任意的对象,定义在头文件tuple中
- tuple最重要的用处就是用来当作一个简单快速的数据结构
- 我们需要用tuple<type1,type2,…,type3>来定义一个tuple,然后必须使用直接初始化法,对象参数可以输入对应类型需要放入的成员
- 类似pair,我们也有make_tuple函数可以生成对象,参数是需要放入的成员,会推断参数的类型并生成tuple返回
- 我们可以用get函数来取得tuple的元素,get的模板参数get<i>是目标元素在tuple中的序号,从0开始
- 如果不知道一个tuple的具体信息的话,可以用tuple_size<obj>::value返回目标tuple中的元素数量,然后可以用tuple_element<idx,obj>::type返回目标tuple中序号idx的成员的类型
- tuple也可以进行比较运算,但是必须元素数量相同时才能比较
- tuple的常见用途是从一个函数返回多个值,类似于在外部定义一个struct,但是更加方便且低耦合
17.2 bitset类型
- bitset类型可以很好地处理位运算问题,比直接使用位操作符清晰方便很多
- bitset类似array,定义的时候模板参数是这个bitset的位数
- 我们可以在定义时构造参数输入一个不大于unsigned long long类型的整型值作为初始值,没有内容的部分会置0,超长的部分会截断
- 也可以构造参数使用string或字符数组拷贝,此时通过参数控制代表0和1的字符,然后利用字符串生成bitvec。这里要注意string的下标编号习惯与bieset正好相反,string的内容会初始化在bitset的右侧,因为bitset的低位在右侧
- bitset的操作很多,具体在书中表17.3有,个人认为比较关键的是用any查看是否有被置位的位,all查看是否都被置位,size返回bitset的位数,test(pos)返回某个位是否被置位,set(pos,v)将某个位置位为v,to_string(zero,one)将bitset转换回字符串string
- bitset也可以直接与IO流协作,cin时最多接受到bitset满载
17.3 正则表达式
- 正则表达式是一个非常强大的字符序列处理工具,具体的使用方式不适合在这里写,此书只介绍了C 的正则表达式库RE,在头文件regex中
- regex的核心是判断是否匹配的函数regex_match,搜索第一个匹配串的函数regex_search,用新输入的结果替换匹配到的串的函数regex_replace和用来匹配的迭代器适配器sregex_iterator
- 默认情况下regex使用的是ECMAScript正则语言
- 匹配的方法通常是构造一个string类型的匹配模式,然后用这个模式构造一个正则表达式regex,接着定义一个smatch类型用来保存匹配的结果,准备好string类型的匹配文本,最后选用适合的regex函数来匹配
- 正则表达式regex在构造的时候可以附加参数,例如icase参数会忽略大小写,basic参数将语法改为POSIX等等
- 正则表达式是一种简单的程序语言,一个regex对象被初始化或赋予新模式时才会被“编译”,而且也可能发生编写错误甚至内存错误之类
- 正则表达式出现错误时会以regex_error的异常抛出,所以使用时需要try-catch
- 正则表达式的编译是非常慢的过程,所以应该避免创建不必要的表达式
- 正则表达式也有很多个其他的类型版本,可以把string改为char数组或wchar_t数组,wstring中,方法就是把相应的函数名的首字符更改,例如smatch改为cmatch表示char数组版本
- 正则表达式的迭代器通过递增操作可以切换到下一个匹配,要注意没有递减操作,解引用迭代器可以得到最后一个匹配的结果,指向一个smatch对象,对smatch对象取其str成员可以得到匹配的字符串
- 匹配类型smatch中有prefix和suffix成员,可以取得当前匹配的前缀和后缀指针,指向整个匹配串的头和尾,返回类型是当前匹配的子匹配式ssub_match
- 所谓子匹配式,在正则表达式中有一个叫子表达式的部分,通常在匹配模式中以括号()括起来,子表达式会在匹配时被存入子匹配中,即每个match中都存有n 1个sub_match,其中位置0是整个元素的匹配式,其他的依次是子表达式的匹配结果
- 当我们需要在序列中替换一个正则表达式时,应该使用regex_replace函数来处理,这个函数接收待处理串dest,格式化字符串fmt和正则表达式r,返回时函数先用正则表达式r对dest进行处理然后对其中需要替换的子表达式按照格式化字符串fmt处理后输出
- 上面说到的格式化字符串fmt的写法和我们C语言中处理printf的参数相类似,用号表示占位符,后面接子表达式的序号,然后将其组合成想要的式子就行。例如"
- 标准库还定义了一系列用来在替换过程中控制匹配和格式的标志,但是使用的时候我们要在std命名空间中的regex_constants命名空间中使用,通过给regex_replace额外加上这些标志参数就可以修改具体匹配的情况
- 最后这里总结一下书中这一部分简单提到的一些正则表达式的语法项:
- ^x指出我们希望匹配不是x的字符
- []xx指出我们想要匹配在字符后面接xx的字符串
- []表示匹配这里面字符的任意一个
- [[::alpha::]]匹配任意字母
- 表示我们希望这部分有一个或多个的匹配
- *表示我们希望这部分有零个或多个的匹配
- .匹配任意字符
- 反斜杠代表去掉特殊含义
- $表示到此终止
- ()可以标记出子表达式
- {d}表示单个数字
- {d}{x}表示x个数字
- ?表示前面的组件时可选的
17.4 随机数
- C语言中我们往往用rand函数来获取随机数,但这个方法有很多问题和局限性,例如我们通常用rand的返回值除我们想要的随机数上限来得到一个范围内的随机数,但是这个方法会由于rand的位数有限导致一些数字不会被产生。作为改进,在C 中我们应该使用随机数库来生成更好的随机数
- 随机数库包含了生成随机unsigned整数序列的随机数引擎和利用引擎生成符合特定分布随机数的随机数分布器
- 随机数引擎是函数对象类,重载了一个不需要参数的调用运算符,我们可以调用这个类来生成一个unsigned的原始随机数。我们通常不会直接使用这个数,因为范围常常和我们需要的不同
- 随机数引擎依赖于“种子”来从伪随机数序列中选择一个位置开始生成随机数,这就是通常我们说到的"计算机生成的是伪随机数"。伪随机数序列是随机数引擎生成数值的核心,是使用特定的方法如对某个数学公式(例如平方取中法)的计算,生成的一个有周期性规律的数字序列,这个序列的数字在单个周期内各方面来看都接近一个真正的随机数序列,生成方法可以理解为在这个序列中从某个位置开始一个一个取出数字
- 因此伪随机序列的特点就是这第一个参数"种子"会决定这个序列开始的位置,是随机数生成的随机性的最重要来源,如果我们输入的种子是相同的那么接下来生成的随机数序列都会是相同的
- C 中default_random_engine是默认的随机数引擎,不同的引擎有不同的随机性质量,我们在构造引擎实例的时候构造函数参数可以传入一个整数值s,或者后期调用函数seed重新指定s作为种子,这之后每次调用引擎都会生成一个随机数
- 当没有指定种子时引擎使用的是内置的默认种子,因此我们空构造的引擎得到的序列总是相同的,这一点可以很方便地用来调试系统,但是记得实际使用的时候要指定好种子
- 在我们实际使用时,最常用的种子是使用定义在头文件ctime中的系统函数time来作种子,这个函数返回从一个特定时刻到现在经过的秒数,用现实世界的时间来做种显然比较符合真随机。但是这个方法也有缺点,一个缺点就是time的返回值是秒数,因此如果想要在一秒内返回多个随机数则需要对种子进行进一步的处理,防止多次调用都是同样的种子
- 还有一个重要的对引擎的处理,就是一般我们在程序运行开始时实例化一个随机数引擎,然后设置为static,尔后我们的随机数都从这个引擎中取数,让引擎保持状态让我们从序列中取的数至少会符合序列设计时的随机性
- 当我们想要从一个分布和一个范围中生成随机数时,我们应该使用随机数分布器,常用的随机数分布器就是uniform_int_distribution均匀整数分布器和uniform_real_distribution均匀实数分布器,初始化分布器的时候模板参数是目标分布的最大值和最小值,实例化完成后我们调用时给分布器传递随机数引擎作为参数即可,注意需要直接传递引擎因为分布器可能在内部需要多次调用引擎
- 新标准库还可以生成非均匀分布的随机数,最典型的是正态分布normal_distribution和伯努利分布bernoulli_distribution,都可以指定自己的参数非常方便,还有一些其他的分布需要自己去查阅
- 分布器一样需要保证好引擎的种子,而且为了保持分布的正常,分布对象需要保存状态,也就是要在循环外定义
17.5 IO库再探
- 第8章中介绍了基本的IO库操作,这里要再介绍一些IO库的控制方法
- 标准库定义了一组修改流状态的操作符,操作符是函数或者对象,在输入输出的时候将其传入可以改变接下来的格式状态,最常用的是endl操作符用来换行并刷新缓冲区
- 大多数操作符都是成对的,一个设置一个复原,且操作符分为两大类,一类控制输出的数值的格式,一类控制补白等格式
- 很多操作符会永久修改当前IO流的状态直到复原设置,因此使用了特殊格式的操作符后最好尽快复原防止之后格式不如人意
- boolalpha操作符可以让之后的bool1输出为true,bool0输出为false,用noboolalpha复原
- hex,oct,dec三个操作符可以让之后数值都自动转换为对应进制输出,如cout<<hex<<20会输出14,这个改变只会影响浮点型
- showbase操作符会让接下来的整型输出时在和上一种的操作符合作时附加进制显示,如十六进制输出为0x14
- setprecision(n)操作符可以改变流输出浮点值时的小数位数,也可以用cout.precision(n)控制,默认情况下浮点值按照6位数字(总位数)打印,没有小数点则不打印小数点,非常大或非常小的数以科学计数法表示
- scientific操作符会强制变为科学计数法输出,fixed操作符强行用定点十进制输出,defaultfloat复位
- hexadecimal可以用十六进制打印浮点数
- uppercase可以让数值的字符改为大写如0x14f变为0X14F
- showpoint操作符让浮点数即便小数为0也打出小数点并用0填满位数,noshowpoint复位
- setw(n)可以控制输出的补白,也就是控制输出的内容需要在第几位的地方右对齐,默认使用空格将内容前推到右对齐第n位为止,然后可以用setfill(c)改变填充用的字符,用left和right改变对齐的方向
- noskipws可以让流忽略空白符而不是默认的跳过它们,用skipws复原
- 平时常见的是格式化IO操作,而未格式化IO操作允许我们将一个流当作一个无解释的字节序列处理,最常用的就是读取一个字符的get函数和输出一个字符的put函数,然后对于istream,我们可以用get将下一个字节作为int返回,putback(ch)可以将任意一个字符放回流中,peek可以将下一个字节作为int返回但不会从流中拿走它,unget会自动将最后一个取出的字符放回。这些对流的操作要注意我们只能读取或退回一个值,不能连续调用
- 上一点的函数返回int主要是int类型可以保证所有的字符都能被涵盖而且cstdio中有EOF的常量代表文件尾,这个常量不属于任何字符,不容易出问题。一个很常见的错误就是将get,peek之类的函数返回值赋值给char而不是int,当读取到EOF时赋值给char得到的值会与int型的EOF不同,这很容易产生一些错误的判断
- 一些操作可以进行多字节的未格式化IO,但是要注意操作越多犯错的机会也就越多,get,getline,read,write都是多字节的操作,ignore函数可以忽略流中的一定数目的字符
- 其中get和getline最大的区别是get会将分隔符保留为流的下一个字符,getline则读取并抛弃分隔符
- 我们可以对流进行随机访问,因为流中实际上由一个标记位置的变量控制,用tell可以得到这个变量,seek可以改变这个变量的位置。注意流并没有区分读标记和写标记,因此我们在切换读写的时候需要自己保存好tell返回的值
- tell和seek返回的标记时机器相关的类型,大小不一定,但是我们可以使用其中的beg得到流的开始处,cur得到流的当前位置,end得到流的结束位置