《C++ Primer》第9章 顺序容器
9.5节习题答案 额外的string操作
练习9.41:编写程序,从一个vector<char>初始化一个string。
【出题思路】
本题练习从字符数组初始化string。
【解答】
vector提供了data成员函数,返回其内存空间的首地址。将此返回值作为string的构造函数的第一个参数,将vector的size返回值作为第二个参数,即可获取vector<char>中的数据,将其看作一个字符数组来初始化string。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<char> 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 <iostream>
#include <vector>
#include <string>
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 <iostream>
#include <vector>
#include <string>
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 <iostream>
#include <vector>
#include <string>
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 <iostream>
#include <vector>
#include <string>
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 <iostream>
#include <vector>
#include <string>
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 <iostream>
#include <string>
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 <iostream>
#include <string>
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 <iostream>
#include <fstream>
#include <string>
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<string>,其元素都表示整型值。计算vector中所有元素之和。修改程序,使之计算表示浮点值的string之和。
【出题思路】
本题练习简单的字符串到数值的类型转换,这在开发实际应用程序时是非常常见的操作,是很有用的基本编程技巧。
【解答】
标准库提供了将字符串转换为整型函数stoi。如果希望转换为不同整型类型,如长整型、无符号整型等,标准库也都提供了相应的版本。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<string> 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 <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<string> 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 <iostream>
#include <string>
#include <stdexcept>
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 /* PROGRAM09_51_H */
主程序:
#include <iostream>
#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;
}
运行结果: