《C++ Primer》第9章 9.5节习题答案

《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;
}

运行结果:

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值