《C++ Primer》第9章 顺序容器
9.5节习题答案 额外的string操作
练习9.41:编写程序,从一个vector
【出题思路】
本题练习从字符数组初始化string。
【解答】
vector提供了data成员函数,返回其内存空间的首地址。将此返回值作为string的构造函数的第一个参数,将vector的size返回值作为第二个参数,即可获取vector
#include#include #include using namespace std; int main() { vector vec = {'H', 'e', 'l','l','o'}; string str1(vec.begin(), vec.end()); string str2(vec.data(), vec.size()); cout << "str1===" << str1 << endl; cout << "str2===" << str2 << endl; }
运行结果:
练习9.42:假定你希望每次读取一个字符存入一个string中,而且知道最少需要读取100个字符,你应该如何提高程序的性能?
【出题思路】
本题练习高效地处理动态增长的string。
【解答】
由于知道至少读取100个字符,因此可以用reserve先为string分配100个字符的空间,然后逐个读取字符,用push_back添加到string末尾。
#include#include #include using namespace std; void input_string(string &str) { str.reserve(100); char c; while (cin >> c) { str.push_back(c); } } int main() { string s; input_string(s); cout << "s===" << s << endl; return 0; }
运行结果:
练习9.43:编写函数,接受三个string参数s、oldVal和newVal。使用迭代器及insert和erase函数将s中所有oldVal替换为newVal。测试你的程序,用它替换通用的简写形式,如,将“tho”替换为“though”,将“thru”替换为“through”。
【出题思路】
本题练习较为复杂的string操作。
【解答】
由于要求使用迭代器,因此使用如下算法:
1.用迭代器iter遍历字符串s。注意,对于s末尾少于oldVal长度的部分,已不可能与oldVal相等,因此无须检查。
2.对每个位置,用一个循环检查s中字符是否与oldVal中的字符都相等。
3.若循环是因为iter2 == oldVal.end而退出,表明s中iter开始的子串与oldVal相等。则调用erase将此子串删除,接着用一个循环将newVal复制到当前位置(tdm-gcc 4.8.1中,返回迭代器的insert只支持单个字符插入)。由于insert将新字符插入到当前位置之前,并返回指向新字符的迭代器,因此,逆序插入newVal字符即可。最后将iter移动到新插入内容之后,继续遍历s。
4.否则,iter开始的子串与oldVal不等,递增iter,继续遍历s。
#include#include #include using namespace std; void replace_string(string &s, const string &oldVal, const string &newVal) { auto l = oldVal.size(); if(!l) //要查找的字符串为空 return; auto iter = s.begin(); while(iter <= s.end() - 1) //末尾少于oldVal长度的部分无须检查 { auto iter1 = iter; auto iter2 = oldVal.begin(); //s中iter开始的子串必须每个字符都与oldVal牙相同 while(iter2 != oldVal.end() && *iter1 == *iter2) { iter1++; iter2++; } if(iter2 == oldVal.end())//oldVal耗尽————字符串相等 { iter = s.erase(iter, iter1);//删除s中与oldVal相等部分 if(newVal.size())//替换子串是否为空 { iter2 = newVal.end();//由后至前逐个插入newVal中的字符 do{ iter2--; iter = s.insert(iter, *iter2); }while(iter2 > newVal.begin()); } iter += newVal.size();//迭代器移动到新插入内容之后 } else { iter++; } } } int main() { string s = "tho thru tho!"; replace_string(s, "thru", "through"); cout << s << endl; replace_string(s, "tho", "though"); cout << s << endl; replace_string(s, "through", ""); cout << s << endl; return 0; }
运行结果:
练习9.44:重写上一题的函数,这次使用一个下标和replace。
【出题思路】
本题练习使用标准库提供的特性更简单地实现string操作。
【解答】
由于可以使用下标和replace,因此可以更为简单地实现上一题的目标。通过find成员函数(只支持下标参数)即可找到s中与oldVal相同的子串,接着用replace即可将找到的子串替换为新内容。可以看到,使用下标而不是迭代器,通常可以更简单地实现字符串操作。
#include#include #include using namespace std; void replace_string(string &s, const string &oldVal, const string &newVal) { unsigned long p = 0; while((p = s.find(oldVal, p)) != string::npos) //在s中查找oldVal { s.replace(p, oldVal.size(), newVal); //将找到的子串替换为newVal的内容 p += newVal.size(); //下标调整到新插入的内容之后 } } int main() { string s = "tho thru tho!"; replace_string(s, "thru", "through"); cout << s << endl; replace_string(s, "tho", "though"); cout << s << endl; replace_string(s, "through", ""); cout << s << endl; return 0; }
运行结果:
练习9.45:编写函数,接受一个表示名字的string参数和两个分别表示前缀(如“Mr.”或“Ms.”)和后缀(如“Jr.”或“III”)的字符串。使用迭代器及insert和append函数将前缀和后缀添加到给定的名字中,将生成的新string返回。
【出题思路】
本题练习string的追加操作。
【解答】
通过insert插入到首位置之前,即可实现前缀插入。通过append即可实现将后缀追加到字符串末尾。
#include#include #include using namespace std; void name_string(string &name, const string &prefix, const string &suffix) { name.insert(name.begin(), 1, ' '); name.insert(name.begin(), prefix.begin(), prefix.end());//输入前缀 name.append(" "); name.append(suffix.begin(), suffix.end()); } int main() { string s = "James Bond"; name_string(s, "Mr.", "II"); cout << s << endl; s = "M"; name_string(s, "Mrs.", "III"); cout << s << endl; return 0; }
运行结果:
练习9.46:重写上一题的函数,这次使用位置和长度来管理string,并只使用insert。
【出题思路】
本题继续练习基于位置的string操作。
【解答】
使用insert,0等价于begin(),都是在当前首字符之前插入新字符串;size()等价于end(),都是在末尾追加新字符串。
#include#include #include using namespace std; void name_string(string &name, const string &prefix, const string &suffix) { name.insert(0, " "); name.insert(0, prefix);//输入前缀 name.insert(name.size(), " "); name.insert(name.size(), suffix); //插入后缀 } int main() { string s = "James Bond"; name_string(s, "Mr.", "II"); cout << s << endl; s = "M"; name_string(s, "Mrs.", "III"); cout << s << endl; return 0; }
运行结果:
练习9.47:编写程序,首先查找string "ab2c3d7R4E6"中的每个数字字符,然后查找其中每个字母字符。编写两个版本的程序,第一个要使用find_first_of,第二个要使用find_first_not_of。【出题思路】
本题练习string的搜索操作的基本用法。
【解答】
find_first_of在字符串中查找给定字符集合中任一字符首次出现的位置。若查找数字字符,则“给定字符集合”应包含所有10个数字;若查找字母,则要包含所有大小写字母——abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOQRSTUVWXYZ。
#include#include using namespace std; void find_char(string &s, const string &chars) { cout << "在" << s << "中查找" << chars << "中字符" << endl; string::size_type pos = 0; while((pos = s.find_first_of(chars, pos)) != string::npos)//找到字符 { cout << "pos: " << pos << ", char:" << s[pos] << endl; pos++;//移动到下一个字符 } } int main() { string s = "ab2c3d7R4E6"; cout << "查找所有数字" << endl; find_char(s, "0123456789"); cout << endl << "查找所有字母" << endl; find_char(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); return 0; }
运行结果:
find_first_not_of查找第一个不在给定字符集合中出现的字符,若用它查找某类字符首次出现的位置,则应使用补集。若查找数字字符,则“给定字符集合”应包含10个数字之外的所有字符——abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ;若查找字母,则要包含所有非字母字符。注意,这一设定仅对此问题要查找的字符串有效——它只包含字母和数字。因此,字母和数字互为补集。若字符串包含任意ASCII字符,可以想见,正确的“补集”可能非常冗长。
#include#include using namespace std; void find_char(string &s, const string &chars) { cout << "在" << s << "中查找不在" << chars << "中字符" << endl; string::size_type pos = 0; while((pos = s.find_first_not_of(chars, pos)) != string::npos)//找到字符 { cout << "pos: " << pos << ", char:" << s[pos] << endl; pos++;//移动到下一个字符 } } int main() { string s = "ab2c3d7R4E6"; cout << "查找所有数字" << endl; find_char(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); cout << endl << "查找所有字母" << endl; find_char(s, "0123456789"); return 0; }
运行结果:
练习9.48:假定name和numbers的定义如325页所示,numbers.find(name)返回什么?
【出题思路】
理解find与find_first_of、find_first_not_of的区别。
【解答】
s.find(args)查找s中args第一次出现的位置,即第一个与args匹配的字符串的位置。args是作为一个字符串整体在s中查找,而非一个字符集合在s中查找其中字符。因此,对325页给定的name和numbers值,在numbers中不存在与name匹配的字符串,find会返回npos。
练习9.49:如果一个字母延伸到中线之上,如d或f,则称其有上出头部分(ascender)。如果一个字母延伸到中线之下,如p或g,则称其有下出头部分(descender)。编写程序,读入一个单词文件,输出最长的既不包含上出头部分,也不包含下出头部分的单词。
【出题思路】
本题练习用搜索操作做一些更复杂的事情。
【解答】
查找既不包含上出头字母,也不包含下出头字母的单词,等价于“排除包含上出头字母或下出头字母的单词”。因此,用find_first_of在单词中查找上出头字母或下出头字母是否出现。若出现(返回一个合法位置,而非npos),则丢弃此单词,继续检查下一个单词。否则,表明单词符合要求,检查它是否比之前的最长合法单词更长,若是,记录其长度和内容。文件读取完毕后,输出最长的合乎要求的单词。
#include#include #include using namespace std; void find_longest_word(ifstream &in) { string s; string longest_word; unsigned long max_length = 0; while(in >> s) { if(s.find_first_of("bdfghjklpqty") != string::npos) continue; //包含上出头或下出头字母 cout << s << " "; if(max_length < s.size()) //新单词更长 { max_length = s.size();//记录长度和单词 longest_word = s; } } cout << endl << "最长字符串:" << longest_word << endl; } int main(int argc, char *argv[]) { ifstream in(argv[1]); if(!in) { cerr << "无法打开输入文件" << endl; return -1; } find_longest_word(in); return 0; }
命令行参数设置
运行结果:
练习9.50:编写程序处理一个vector
【出题思路】
本题练习简单的字符串到数值的类型转换,这在开发实际应用程序时是非常常见的操作,是很有用的基本编程技巧。
【解答】
标准库提供了将字符串转换为整型函数stoi。如果希望转换为不同整型类型,如长整型、无符号整型等,标准库也都提供了相应的版本。
#include#include #include using namespace std; int main() { vector vs = {"123", "+456", "-789"}; int sum = 0; for(auto iter = vs.begin(); iter != vs.end(); ++iter) sum += stoi(*iter); cout << "和:" << sum << endl; return 0; }
运行结果:
标准库也提供了将字符串转换为浮点数的函数,其中stof是转换为单精度浮点数。简单修改上面的程序即可实现本题的第二问。注意,当给定的字符串不能转换为数值时(不是所需类型数值的合法表示),这些转换函数会抛出invalid_argument异常;如果表示的值超出类型所能表达的范围,则抛出一个out_of_range异常。这两个程序均未捕获、处理这两个异常,读者可尝试编写捕获并处理异常的版本,并用不合要求的字符串进行测试。
#include#include #include using namespace std; int main() { vector vs = {"12.3", "-4.56", "+7.8e-2"}; int sum = 0; for(auto iter = vs.begin(); iter != vs.end(); ++iter) sum += stof(*iter); cout << "和:" << sum << endl; return 0; }
运行结果:
练习9.51:设计一个类,它有三个unsigned成员,分别表示年、月和日。为其编写构造函数,接受一个表示日期的string参数。你的构造函数应该能处理不同数据格式,如January 1,1900、1/1/1900、Jan 1 1900等。
【出题思路】
本题看似简单,但实际上较为复杂。在实际应用程序开发中,编写从文本中提取格式数据的程序片段,是非常烦琐、很容易出错的工作。因为这部分程序不能只会解析格式正确的数据,还应检查格式错误,给出错误信息。
【解答】
在头文件中定义了date类。构造函数date(string &ds)从字符串中解析出年、月、日的值,大致步骤如下:
1.若首字符是数字,则为格式2,用stoi提取月份值,若月份值不合法,抛出异常,否则转到步骤6。
2.若首字符不是数字,表明是格式1或3,首先提取月份值。
3.将ds开始的子串与月份简称进行比较,若均不等,抛出异常(若与简称不等,则不可能与全称相等)。
4.若与第i个月简称相等,且下一个字符是合法间隔符,返回月份值。
5.否则,检查接下来的子串是否与全称剩余部分相等,若不等,抛出异常;否则,返回月份值。
6.用stoi提取日期值和年份值,如需要,检查间隔符合法性。
读者需要特别注意的是,在解析过程中,如何调整偏移量p。
此程序已经较为复杂,但显然离“完美”还差很远,只能解析3种格式,且进行了很多简化。程序中已经给出了几种格式错误,读者可尝试构造其他可能的格式错误。并尝试补充程序,支持其他格式,如“2006年7月12日”。此外,程序中也涉及类、异常等相关的编程知识,读者可自行分析。头文件date.h如下所示:
#ifndef PROGRAM09_51_H #define PROGRAM09_51_H #include#include #include using namespace std; class date { public: friend ostream& operator << (ostream&, const date&); date() = default; date(string &ds); unsigned y() const { return year; } unsigned m() const { return month; } unsigned d() const { return day; } private: unsigned year, month, day; }; //月份全称 const string month_name[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; //月份简写 const string month_abbr[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"}; //每月天数 const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; inline int get_month(string &ds, int &end_of_month) { int i, j; for(i = 0; i < 12; ++i) { //检查每个字符是否与月份简写相等 for(j = 0; j < month_abbr[i].size(); ++j) { if(ds[j] != month_abbr[i][j])//不是此月简写 break; } if(j == month_abbr[i].size())//与简写匹配 break; } if(i == 12) //与所有月份名都不同相同 throw invalid_argument("不是合法月份名"); if(ds[j] == ' ')//空白符,仅是月份简写 { end_of_month = j + 1; return i + 1; } for(; j < month_name[i].size(); ++j) { if(ds[j] != month_name[i][j]) break; } if(j == month_name[i].size() && ds[j] == ' ')//月份全称 { end_of_month = j + 1; return i + 1; } throw invalid_argument("不是合法月份名"); } inline int get_day(string &ds, int month, int &p) { size_t q; int day = stoi(ds.substr(p), &q);//从p开始的部分转换为日期值 if(day < 1 || day > days[month]) throw invalid_argument("不是合法日期值"); p += q;//移动到日期值之后 return day; } inline int get_year(string &ds, int &p) { size_t q; int year = stoi(ds.substr(p), &q);//从p开始的部分转换为年 if(p + q < ds.size()) throw invalid_argument("非法结尾内容"); return year; } date::date(string &ds) { int p; size_t q; if((p = ds.find_first_of("0123456789")) == string::npos) throw invalid_argument("没有数字,非法日期"); if(p > 0)//月份名格式 { month = get_month(ds, p); day = get_day(ds, month, p); if(ds[p] != ' ' && ds[p] != ',') throw invalid_argument("非法间隔符"); p++; year = get_year(ds, p); } else//月份值格式 { month = stoi(ds, &q); p = q; if(month < 1 || month > 12) throw invalid_argument("不是合法月份值"); if(ds[p++] != '/') throw invalid_argument("非法间隔符"); day = get_day(ds, month, p); if(ds[p++] != '/') throw invalid_argument("非法间隔符"); year = get_year(ds, p); } } ostream & operator << (ostream& out, const date& d) { out << d.y() << "年" << d.m() << "月 " << d.d() << "日" << endl; return out; } #endif
主程序:
#include#include "program09_51.h" using namespace std; int main() { string dates[] = {"Jan 1,2014", "February 1 2014", "3/1/2014", //"Jcn 1,2014", //"Janvary 1,2014", //"Jan 32,2014", //"Jan 1/2014", "3 1 2014" }; try{ for(auto ds: dates) { date dl(ds); cout << dl; } } catch(invalid_argument e) { cout << e.what() << endl; } return 0; }
运行结果:



