C++ Primer笔记(十七)

标准库特殊设施

tuple类型

一个tuple有任意数量的成员,成员类型可能不同,确定的tuple成员数目是固定的

tuple<T1,T2…,Tn> tt是一个tuple,成员数为n,第i个成员的类型为Ti
tuple<T1,T2…,Tn> t(v1,v2…vn)t是一个tuple,每个成员用对应的初始值进行初始化
make_tuple(v1,v2,…vn)返回一个用给定初始值初始化的tuple
t1!=t2当两个tuple具有相同数量的成员且成员对应相等,两个tuple才相等
get<i>(t)返回t的第i个数据成员的引用,并保留左右值特性
tuple_element<i,tupleType>::type通过整形常量和tuple类型来初始化的类模板,type表示tuple指定成员i的类型

定义和初始化tuple

tuple<size_t, size_t, size_t> threeD;//三个成员默认值初始化
auto item = make_tuple("0-999-78345-X",3,20.00);//用初始值推断tuple的类型
  1. 若想访问tuple的成员,使用get函数,显式模板实参指出想要访问第几个成员,并传递一个tuple对象auto book = get<0>(item)
  2. 若想查询tuple存储元素的信息,tuple_size<tuple>::value为显式模板实参指定的类型的数目 tuple_element<i,tuple>::type该成员为显式模板实参指定的第i个元素的类型
  3. 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返回多个值

tuple的一个常见用途是从一个函数返回多个值,假定每个书店有一个销售记录文件,每个文件保存所有书的销售记录。如果vector<vector<Sales_data>> tuple 保存所有书店的销售记录,我们写一个函数,在files中搜索出售过这本书的书店,对每家有匹配销售记录的书店,创建一个tuple保存这家书店的索引和两个迭代器。

typedef tuple<vector<Sales_data>::size_type, vector<Sales_data>::const_iterator,vector<Sales_data>::const_iterator> matches;
vector<matches> findBook(const vector<vector<Sales_data>> &files, const string &book)
{
	vector<matches> ret;
	for(auto it = files.cbegin();it!=files.cend();++it)
	{
		auto found = equal_range(it->cbegin(),ite->cend(),book,compareIsbn);
		if(found.first!=found.second)
			ret.push_back(make_tuple(it-files.cbegin(),found.first,found.second));
	}
	return ret;
}

for循环遍历files中的元素,循环内调用名为equal_range,返回一对迭代器,表示元素的范围,first成员表示第一条匹配的记录,second表示匹配的尾后位置。

bitset类型

bitset的构造函数为bitset<32> bitvec(1U) 定义bitvec为一个32位的bitset ,其中的每一个位也是未命名的,通过位置来访问,编号从0开始的二进制位是低位,31结束的二进制位是高位

bitset<n> bb有n位。每一位都是0
bitset<n> b(u)b是值u的低n位的拷贝
bitset<n> b(s,pos,m,zero,one)b是string s 从位置pos开始m个字符的拷贝,s只能包含zero或者one,如果包含其他字符会抛出invalid_argument异常
bitset<n> b(s,pos,m,zero,one)b是指针cp指向的字符数组从位置pos开始m个字符的拷贝,s只能包含zero或者one,如果包含其他字符会抛出invalid_argument异常
  1. 用unsigned来初始化bitset,当用整形值初始化bitset时,值被转换为unsigned long long 并按位模式处理,如果bitset的大小大于一个unsigned long long 则剩余的高位被置0,如果bitset大小小于一个unsigned long long 则超出bitset大小的高位被丢弃。
  2. 我们可以从一个string或者一个字符数组指针来初始化bitset,这两种情况下,字符都直接表示位模式,需要注意的是,string中下标最大的字符用来初始化bitset中的低位(下标位0的二进制位)

bitset操作

count、size、 all、any、none 不接受参数,返回整个bitset的状态,而set、reset、flip的重载版本是改变bitset状态的, 接受指定位的版本对指定位进行操作

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 因为只有1位置位
size_t onBits = bitvec.count();//返回1
size_t sz = bitvec.size();//返回32
bitvec.flip();//翻转bitvec中的所有位
bitvec.reset();//将所有位复位
bitvec.set();//将所有位置位

bitset对下标运算符进行了重载,非const版本允许我们操作指定位的值,const版本在指定位置位时返回true

  1. 若想提取bitset的值,用to_ulongto_ullong 返回一个值,保存了与bitset对象相同的位模式

  2. IO运算符 bitset<16> bits; cin>>bits; cout<<"bits:"<<bits<<endl; 从输入流中读取字符,存入一个临时的string 对象中,达到bitset对应的大小或者输入错误时停止 ,然后用这个临时对象初始化bitset

正则表达式

regex表示一个正则表达式类
regex_match将一个字符序列与一个正则表达式匹配
regex_search寻找第一个与正则表达式匹配的子序列
regex_replace用给定格式替换正则表达式

整个字符序列匹配,regex_match返回true,子串与表达式匹配 regex_search返回true ,这两个函数参数类似(seq,m,r,mft) 在字符序列seq中查找regex对象r中的正则表达式,m是一个match对象,保存匹配结果的相关细节

使用regex库

string pattern("[^c]ei");
pattern = "[[:alpha:]]*"+pattern+"[[:alpha:]]";
regex r(pattern);//构造查找模式的pattern
smatch results;
string test_str = "reciept friend theif receive";
if(regex_search(test_str,results,r))
	cout<<results.str()<<endl;

[^c] 表示不是c的字符,[^c] ei表示后面接ei的字串

regex r(re) re表示一个正则表达式,可以是string,表示字符范围的迭代器对,f是指出图像如何处理的标识
r=re将r中的正则表达式替换

regex中字符点匹配任意字符,可以放置反斜线去掉特殊含义,来表示.只是字符序列中一部分,又因为\也是c++中特殊字符,所以应当放置两个反斜线才能匹配句点\.

正则表达式是在初始化时才被编译的,即运行时编译,如果编写的正则表达式存在错误,则在运行时标准库抛出一个regex_error的异常,它有what操作来描述发生了什么错误

try{
	regex r("[[:alpha:]+\\.(cpp|cxx|cc)$",regex::icase));
}
catch(regex_error e)
{
	cout<<e.what()<<"\ncode:"<<e.code()<<endl;
}
  1. 正则表达式类和输入序列
    输入可以是char或者是wchar_t 字符可以保存在string或者char数组中,RE库为这些不同的输入序列类型都定义了对应类型
    匹配和迭代器类型区别在于序列是在string还是char数组中,如smatch表示string类型的输入序列,cmath表示字符数组序列,wsmatch表示宽字符串输入。重点在于使用的RE库版本与输入序列匹配:
regex r("[[:alpha::]]+\\.(cpp|cxx|cc)$",regex:icase);
smatch results;
if(regex_search("text.cpp",results,r))//错误,seq是个字符数组,smatch需要输入序列为string

regex迭代器

regex_match只找到了输入序列中匹配的第一个单词,可以用sregex_iterator来获得所有匹配,regex迭代器是一种迭代器适配器,绑定到一个输入序列和一个regex对象上。sregex_iterarpt it(b,e,r)调用sregex_search(b,e,r)将it定位到输入中第一个匹配的位置,b,e为两个输入序列的迭代器,。解引用迭代器,得到一个对应最近一次搜索结果的smatch对象,递增迭代器时调用regex_search在输入string中查找下一个匹配

string pattern("[^c]ei");
pattern = "[[:alpha:]]*"+pattern+"[[:alpha:]]";
regex r(pattern);//构造查找模式的pattern
smatch results;
string test_str = "reciept friend theif receive";
for(sregex_iterator it(test_str.begin(),test_str.end(),r),end_it;it!=end_it;++it)
{
	cout<<it->str();
}

循环遍历每个与r模式匹配的字串,end_it是一个空迭代器,起到尾后迭代器的作用,

  1. ssub_match和smatch可以获得匹配位置的上下文,match类型有名为prefix和suffix成员,返回输入序列中当前匹配之前和之后部分的ssub_match对象
smatch操作
m.ready()如果调用了regex_search或regex_match设置了m,返回true否则返回false
m.size()匹配失败返回0,否则返回最近匹配的子表达式数目
m.empty()size为0返回true
m.empty()size为0返回true
m.empty()size为0返回true

使用子表达式

正则表达式中的模式通常包含一个或者多个子表达式,子表达式是模式的一部分,如每次用括号分组多个可行选项时,同时也就声明了这些选项形成子表达式regex r("([[:alnum:]]+)\\.(cpp|cxx|cc)$",regex::icase); 其中有两个子表达式:([[:alnum:]]+)(cpp|cxx|cc)匹配文件扩展名。此外可以通过.str() 方法来访问子表达式的匹配结果

  1. 子表达式的第一个常用用途是匹配特定的格式的数据,对于匹配电话号码的表达式
"(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ])?(\\d{4})";

包含了七个子表达式:

  • (\\()表示可选的左括号
  • ((\\d{3})表示区号
  • ((\\))表示可选的右括号
  • ([-. ])?表示可选的分隔符
  • ((\\d{3})表示号码下三位数字
  • ([-. ])?表示可选的分隔符
  • ((\\d{4})表示号码的最后四位数字
bool valid(const smatch& m)
{
	if(m[1].matched)//如果区号前右一个左括号
		return m[3].matched&&(m[4].matched==0||m[4].str()==" ");//则区号后必须一个右括号
	else
		return !m[3].matched&&m[4].str()==m[6].str();//两个地方的分隔符必须要匹配
}

使用regex_replace

在查找序列的时候,如果想替换一个正则表达式,可以调用regex_replace它接受一个输入字符序列和一个regex对象,还接受一个描述输出形式的字符串

m.format(dest,fmt,mft)fmt生成格式化输出。dest表示写入迭代器指向的目标位置,可以申字符数组中范围的一对指针,或者一个string
regex_replace(dest,seq,r,fmt,mft)遍历seq,用regex_search查找与regex对象r匹配的子串,使用格式字符串fmt和可选的match_flag_type标志来生成输出 dest表示要写入的位置

替换字符串由我们想要的字符组合与匹配的字串对应的子表达式组成,子表达式用来形成号码的原格式而非新格式中的一部分,我们用一个符号$后跟子表达式的索引号来表示一个特定的子表达式。string fmt = "$2.$4.$7"

regex r(phone);
string number = "(908) 555-1800";
cout<<regex_replace(number,r,fmt)<<endl;

最终输出为908.555.1800

  1. 进一步,它可替换一个大文件中的电话号码,
string pattern = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ])?(\\d{4})";
regex r(pattern);
string s;
string fmt="$2.$5.$7";
while(getline(cin, s))
{
	cout<<regex_replace(s,r,fmt)<<endl;
}
return 0;
  1. mft match_flag_type 定义了控制匹配或者格式的标志,定义在regex_constants这一命名空间中,它是在std命名空间中的,为了使用,必须加两个命名空间的限定符using std::regex_constants::format_no_copy 默认情况下,regex_replace输出整个输入序列,未与正则表达式匹配的部分为原样输出,regex_replace 匹配按fmt指定的格式输出,
string fmt2 = "$2.$5.$7 ";
cout<<regex_replace(s,r,fmt2,format_no_copy)<<endl;

mft为format_no_copy意为不输出输入序列中未匹配的部分。

随机数

rand函数生成的随机数在0和32767之间,但是为伪随机数字,定义在头文件random中的随机数库通过一组协作的类来解决这些问题,随机数引擎类(生成随机unsigned整数序列)和随机数分布类(使引擎返回服从特定概率分布的随机数)

随机数引擎和分布

随机数引擎生成原始随机数 default _random_engine 通过函数调用运算符获得随机数,随机数引擎可以Engine e(s)以整型值s作为种子,或者e.seed(s)用种子重置引擎的状态,随机数引擎的输出一般不能直接使用

  1. 为了得到指定范围内的数,我们使用一个分布类型的对象
uniform_int_distribution<unsigned> u(0,9);//生成均匀分布随机数
default_random_engine e;//生成无符号随机整数
for(size_t i=0;i<10;++i)
	cout<<u(e)<<" ";//u来调用e,生成在范围内的随机数

定义该种类型对象的时候,可提供期望的最小值和最大值,分布类型定义了一个调用运算符,接受一个随机数引擎作为参数,我们传递给分布对象的是引擎本身,即u(e)

  1. 调用一个default_random_engine对象的输出类似rand的输出,引擎类型的范围可以通过调用该类型对象的min max成员获得
  2. 对于给定的随机数生成器,每次运行程序都会返回相同的数值序列,正确方法是将引擎和与之相关联的分布对象定义为static的
vector<unsigned> good_randVec()
{
	static default_random_engine e;
	static 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;
}

引擎和分布都是static的,故在两次函数调用之间会保持状态,第一次生成前100个,第二次调用生成接下来100个

  1. 如果想每次运行程序生成不同随机结构,可以提供种子,种子是数值,引擎利用它从序列中一个新位置生成随机数,
default_random_engine e1;
default_random_engine e2(2147483646);//使用给定的种子
default_random_engine e3;//默认种子值
e3.seed(32767);
default_random_engine e4(32767);

种子数相同的引擎生成相同的序列,通常调用系统函数time作为种子,time返回以秒计的时间,这种方式只适用于生成种子的间隔为秒级或者更长的应用

其他随机数分布

  1. 生成随机实数,若需要0到1之间的随机浮点数,可以通过uniform_real_distribution
default_random_engine e;
uniform_real_distribution<double> u(0,1);
for(size_t i =0;i<10;++i)
	cout<<u(e)<<" ";
  1. 分布类型都是模板,模板类型参数表示分布生成的随机数的类型,分布模板是有模板实参的,生成浮点值的分布类型默认生成double值,生成整型值的分布默认生成int值,需要在模板名后加尖括号来表示使用默认随机数类型
  2. 新标准库可以生成非均匀分布的值的序列
default_random_engine e;
normal_distribution<> n(4,1,5);

生成均值为4,标准差1.5的正态分布随机数

  1. 伯努利分布类,它是不接受模板参数的,该分布总是返回一个bool值,返回true的概率是常数,可以在初始化时进行调整bernoulli_distribution s(0.55) 0.55的概率返回true

IO库再探

格式化输入与输出

除了条件状态之外,每个iostream对象还维护一个格式状态来控制IO如何格式化的细节,标准库定义了一组操纵符,一个操纵符是一个函数或者一个对象,会影响流的状态,操纵符会返回它所处理的流对象,endl就是一个操作符,输出一个换行符并刷新缓冲区。需要注意的是,操纵符改变流的格式状态时,通常会对所有后续IO都生效
故改变格式状态的操纵符都是设置成对的,一个设置格式,一个复原。

  1. 控制bool值,boolalpha操作符使bool 值1输出为true,如果取消格式,用noboolalpha操作符
  2. 用hex oct dec指定整形值输入输出的进制,并用showbase指出进制:0x十六进制 0八进制
  3. uppercase输出大写的十六进制数字,noshowcase nouppercase可以重置流的状态
  4. 可以调用IO对象的precision成员改变精度
cout<<"Precision:"<<cout.precision()<<",Value"<<sqrt(2.0)<<endl;
cout.precision(12);//打印精度12位数字
cout<<"Precision:"<<setprecision(3)<<cout.precision()<<",Value"<<sqrt(2.0)<<endl;//精度三位
  1. showpoint强制打印小数点,默认是小数为0时不打印小数点
  2. noskipws令输入运算符读取空白字符

未格式化输入输出

如cin.get(ch); 或者cout.put(ch);

  1. 将字符放回输入流,有三种方法,peek返回流中下一个字符的副本,但不会将其从流中删除;unget使输入流向后移动,从而最后读取的值回到流中;putback退回从流中读取的最后一个值,但是它接受一个参数,此参数必须与最后读取的值相同,标准库保证在读取下一个值之前可以退回最多一个值

  2. peek和无参的get版本以int类型从输入流返回一个字符,因为int可以表示文件尾标记,标准库用负值表示文件为,cstdio定义了EOF来检查get返回的值是否是文件尾(ch=cin.get())!=EOF ,需要注意的是,ch不应当声明为char类型,如果在一个机器上char 被实现为unsigned char 那么ch就永远不会等于EOF

  3. 多字节操作

is.get(sink, size,delim)从is中读取最多size个字节,保存在由sink给出起始地址的字符数组,直到遇到delim或者读取了size 个字节,如果遇到了delim则将其保留在输入流中
is.getline(sink, size,delim)读取并丢弃delim
is.read(sink, size)读取最多size个字节,存入字符数组sink中,
is.gcount()返回上一个未格式化读取操作从is读取的字节数
is.ignore(size, delim)读取并忽略最多size个字符,包括delim
is.gcount()返回上一个未格式化读取操作从is读取的字节数

get和getline函数接受相同的参数,行为类似但是不相同,两个函数中sink都是一个char数组,两个函数都一直读取数据,直到下面条件之一发生:已读取size-1个字符,遇到了文件尾,遇到了分隔符,get将分隔符留作istream的下一个字符,而getline则读取并丢弃分隔符

  1. 通过gcount可以确定最后一个未格式化的输入操作读取了多少个字符,如果在gcount之前调用了peek unget putback 则返回值为0

流随机访问

可以重定位流使之跳过一些数据,虽然标准库为所有流类型都定义了seek和tell函数,但是一般来说绑定到cin cout cerr的流不支持随机访问,可以调用seek和tell函数,但是在运行时会出错

  1. seek和tell IO类型维护一个标记确定下一个读写操作要在哪里进行,seek将标记重定位,tell返回当前位置,这两个函数有分别用于输入和输出的版本
tellg() tellp()返回输入流(tellg)或输出流(tellp)标记的当前位置
seekg(pos) seekp(pos)将标记重定位到给定的绝对地址,pos通常为tell返回的值
seekg(off,from) seekp(off,from)将标记重定位到from之前或者之后off个字符 from可以是beg(相对于流开始位置) cur(相对于流当前位置) end(偏移量相对于流结尾位置)

只能对fstream和sstream使用以上函数

  1. seek和tell在流中只维护单一的标记,处理一个只读或者只写的流,区别是不明显的,如果是fstream或者stringstream这类读写流,有单一缓冲区保存读写数据,并将g和p版本的函数映射到同一个标记,所以如果在读写操作间切换,就必须进行seek操作来重定位标记

  2. 重定位标记,seek有两个重载的版本,一个移动到文件中的绝对地址,seekg(new_position) 一个移动到给定点之前或之后指定的偏移位置seekg(offset,from) new_position类型为pos_type from类型 为off_type这两个类型与机器有关,前者表示文件位置,后者表示距离当前位置的一个偏移量

  3. 访问标记 tell函数返回pos_type值表示流的当前位置,通常用于记住当前位置

ostringstream writeStr;
ostringstream::pos_type mark=writeStr.tellp();
if(0)
{
	writeStr.seekp(mark);
}
  1. 读写同一个文件
int main()
{
	fstream inOut("copyOut",fstream::ate||fstream::in||fstream::out);
	if(!inOut)
	{
		return EXIT_FAILURE;
	}
	auto end_mark = inOut.tellg();
	inOut.seekg(0,fstream::beg);
	int cnt=0;
	string s;
	while(inOut&&inOut.tellp()!=end_mark&&getline(inOut,s))
	{
		cnt+=s.size()+1;
		auto mark=inOut.tell();
		inOut.seekp(0,fstream::end);
		inOut<<cnt;
		if(mark!=end_mark)
			inOut<<" ";
		inOut.seekg(mark);
	}
	inOut.seekp(0,fstream::end);
	inOut<<"\n";
}

首先用ate in out模式打开fstream ate会在打开时直接定位到文件尾,先保存文件尾的位置到end_mark,再将标记重定位到文件开头,准备读入数据。while循环首先检查流是否合法,再检查是否到了文件尾,再读入一行数据,记录下当前位置,重定位到文件末尾输出cnt,再回到之前的mark位置,继续下一个循环。最后在文件尾输出一个换行符。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值