《C++ Primer》习题参考答案:第9章 - 顺序容器

专栏C++学习笔记

《C++ Primer》学习笔记/习题答案 总目录

  • https://blog.csdn.net/TeFuirnever/article/details/100700212

——————————————————————————————————————————————————————

  • 《C++ Primer》学习笔记(九):顺序容器
  • 《C++ Primer》学习笔记(三):字符串、向量和数组
  • 《C++ Primer》习题参考答案:第3章 - 字符串、向量和数组

Cpp-Prime5 + Cpp-Primer-Plus6 源代码和课后题

第9章 - 顺序容器

练习9.1

对于下面的程序任务,vectordequelist 哪种容器最为适合?解释你的选择的理由。如果没有哪一种容器优于其他容器,也请解释理由。

  • (a) 读取固定数量的单词,将它们按字典序插入到容器中。我们将在下一章中看到,关联容器更适合这个问题。
  • (b) 读取未知数量的单词,总是将单词插入到末尾。删除操作在头部进行。
  • (c) 从一个文件读取未知数量的整数。将这些数排序,然后将它们打印到标准输出。

解:

(a)“按字典序插入到容器中” 意味着进行插入排序操作,从而需要在容器内部频繁进行插入操作,vector 在尾部之外的位置插入和删除元素很慢,deque 在头尾之外的位置插入和删除元素很慢,而 list 在任何位置插入、删除速度都很快。因此,这个任务选择 list 更为适合。当然,如果不是必须边读取单词边插入到容器中,可以使用 vector,将读入的单词依次追加到尾部,读取完毕后,调用标准库到排序算法将单词重排为字典序。

(b)由于需要在头、尾分别进行插入、删除操作,因此将 vector 排除在外,dequelist 都可以达到很好的性能。如果还需要频繁进行随机访问,则 deque 更好。

(c)由于整数占用空间很小,且快速的排序算法需频繁随机访问元素,将 list 排除在外。由于无须在头部进行插入、删除操作,因此使用 vector 即可,无须使用 deque

练习9.2

定义一个 list 对象,其元素类型是 intdeque

解:

list<deque<int>> a;

练习9.3

构成迭代器范围的迭代器有何限制?

解:

  • 两个迭代器 beginend 必须指向同一个容器中的元素,或者是容器最后一个元素之后的位置;
  • 而且,对 begin 反复进行递增操作,可保证到达 end,即 end 不在 begin 之前。

练习9.4

编写函数,接受一对指向 vector 的迭代器和一个 int 值。在两个迭代器指定的范围中查找给定的值,返回一个布尔值来指出是否找到。

解:

#include
#include
using namespace std;
bool search_vec(vector<int>::iterator beg, 
	vector<int>::iterator end, int val){
	for (; beg != end; beg++){
		if (*beg == val)
			return true;
	}
	return false;
}
int main(){
	vector<int> ilist = { 1, 2, 3, 4, 5, 6, 7 };
	cout << search_vec(ilist.begin(), ilist.end(), 3) << endl;
	cout << search_vec(ilist.begin(), ilist.end(), 8) << endl;
	system("pause");
	return 0;
}

在这里插入图片描述

练习9.5

重写上一题的函数,返回一个迭代器指向找到的元素。注意,程序必须处理未找到给定值的情况。

解:

#include
#include
using namespace std;
vector<int>::iterator search_vec(vector<int>::iterator beg,
	vector<int>::iterator end, int val){
	for (; beg != end; beg++){
		if (*beg == val)
			return beg;
	}
	return end;
}
int main(){
	vector<int> ilist = { 1, 2, 3, 4, 5, 6, 7 };
	cout << search_vec(ilist.begin(), ilist.end(), 3) - ilist.begin() << endl;
	cout << search_vec(ilist.begin(), ilist.end(), 8) - ilist.begin() << endl;
	system("pause");
	return 0;
}

在这里插入图片描述

练习9.6

下面的程序有何错误?你应该如何修改它?

list<int> lst1;
list<int>::iterator iter1 = lst1.begin(),
					iter2 = lst1.end();
while (iter1 < iter2) /* ... */

解:

vectordeque 不同,list 的迭代器不支持 < 运算,只支持递增、递减、= 以及 != 运算。

原因在于这几种数据结构实现上的不同:vectordeque 将元素在内存中连续保存,而 list 则是将元素以链表方式存储,因此前者可以方便地实现迭代器的大小比较(类似指针的大小比较)来体现元素的前后关系。而在 list 中,两个指针的大小关系与它们指向的元素的前后关系并不一定是吻合的,实现 < 运算将会非常困难和低效。

练习9.7

为了索引 intvector 中的元素,应该使用什么类型?

解:

使用迭代器类型 vector::iterator 来索引 intvector 中的元素。

练习9.8

为了读取 stringlist 中的元素,应该使用什么类型?如果写入 list,又应该使用什么类型?

解:

为了读取 stringlist 中的元素,应使用 list::value_type,因为 value_type 表示元素类型。

为了写入数据,需要(非常量)引用类型,因此应使用 list::reference

练习9.9

begincbegin 两个函数有什么不同?

解:

cbegin 是C++新标准引入的,用来与 auto 结合使用。它返回指向容器第一个元素的 const 迭代器,可以用来只读地访问容器元素,但不能对容器元素进行修改。因此,当不需要写访问时,应该使用 cbegin

begin 则是被重载过的,有两个版本:其中一个是 const 成员函数,也返回 const 迭代器;另一个则返回普通迭代器,可以对容器元素进行修改。

练习9.10

下面4个对象分别是什么类型?

vector<int> v1;
const vector<int> v2;
auto it1 = v1.begin(), it2 = v2.begin();
auto it3 = v1.cbegin(), it4 = v2.cbegin();

解:

v1intvector 类型,我们可以修改 v1 的内容,包括添加、删除元素及修改元素值等操作。

v2int 的常量 vector 类型,其内容不能修改,添加、删除元素及修改元素值等均不允许。

beginauto 结合使用时,会根据调用对象的类型来决定迭代器的类型,因此 it1 是普通迭代器,可对容器元素进行读写访问,而 it2const 迭代器,不能对容器元素进行写访问。

cbegin 则不管调用对象是什么类型,始终返回 const 迭代器,因此 it3it4 都是 const 迭代器。

练习9.11

对6种创建和初始化 vector 对象的方法,每一种都给出一个实例。解释每个 vector 包含什么值。

解:

vector<int> vec;    // 0
vector<int> vec(10);    // 10个0
vector<int> vec(10, 1);  // 10个1
vector<int> vec{ 1, 2, 3, 4, 5 }; // 1, 2, 3, 4, 5
vector<int> vec(other_vec); // 拷贝 other_vec 的元素
vector<int> vec(other_vec.begin(), other_vec.end()); // 拷贝 other_vec 的元素

(1)vectorilist1; //默认初始化,vector为空

size 返回0,表明容器中尚未有元素;capacity返回0,意味着尚未分配存储空间。这种初始化方式适合于元素个数和值未知,需要在程序运行中动态添加的情况。

(2)vectorilist2(ilist); //ilist2初始化为ilist的拷贝

ilist 必须与 ilist2 类型相同,即也是 intvector 类型,ilist2 将具有与 ilist 相同的容量和元素。

vectorilist21=ilist; //等价方式

(3)vectorilist={1, 2, 3.0, 4, 5, 6, 7}; //ilist初始化为列表中元素的拷贝

列表中的元素类型必须与 ilist 的元素类型相容,在本例中必须是与整型相容的数值类型。对于整型,会直接拷贝其值,对于其他类型则需进行类型转换(如3.0转换为3)。这种初始化方式适合元素数量和值预先可知的情况。

vectorilist_1{1, 2, 3.0, 4, 5, 6, 7}; //等价方式

(4)vectorilist3(ilist.begin()+2, ilist.end()-1); //ilist3初始化为两个迭代器指定范围中的元素的拷贝

范围中的元素类型必须与 ilist3 的元素类型相容,在本例中 ilist3 被初始化为 {3, 4, 5, 6}。注意,由于只要求范围中元素类型与待初始化的容器的元素类型相容,因此,迭代器来自于不同类型的容器是可能的,例如,用一个 doublelist 的范围来初始化 ilist3 是可行的。另外,由于构造函数只是读取范围中的元素并进行拷贝,因此使用普通迭代器还是 const 迭代器来指出范围并无区别。这种初始化方法特别适合于获取一个序列的子序列。

(5)vectorilist4(7); //默认值初始化,ilist4中将包含7个元素

每个元素进行缺省的值初始化,对于 int,也就是被赋值为0,因此 ilist4 被初始化为包含7个0。当程序运行初期元素大致数量可预知,而元素的值需动态获取时,可采用这种初始化方式。

(6)vectorilist5(7, 3); //指定值初始化,ilist5被初始化为包含7个值为3的int

练习9.12

对于接受一个容器创建其拷贝的构造函数,和接受两个迭代器创建拷贝的构造函数,解释它们的不同。

解:

接受一个已有容器的构造函数会拷贝此容器中的所有元素,这样,初始化完成后,我们得到此容器的一个一模一样的拷贝。当我们确实需要一个容器的完整拷贝时,这种初始化方式非常方便。

但当我们不需要已有容器中的全部元素,而只是想拷贝其中一部分元素时,可使用接受两个迭代器的构造函数。传递给它要拷贝的范围的起始和尾后位置的迭代器,即可令新容器对象包含所需范围中元素的拷贝。

练习9.13

如何从一个 list 初始化一个 vector ?从一个 vector 又该如何创建?编写代码验证你的答案。

解:

由于 listvector 是不同的容器类型,因此无法采用容器拷贝初始化方式。但前者的元素类型是 int,与后者的元素类型 double 是相容的,因此可以采用范围初始化方式来构造一个 vector,令它的元素值与 list 完全相同。对 vector 也是这样的思路。

#include
#include
#include
using namespace std;
int main()
{
	list<int> ilist = { 1, 2, 3, 4, 5, 6, 7 };
	vector<int> ivec = { 7, 6, 5, 4, 3, 2, 1 };
	// 容器类型不同,不能使用拷贝初始化
	 //vector ivec(ilist);
	// 元素类型相容,因此可采用范围初始化
	vector<double> dvec(ilist.begin(), ilist.end());
	// 容器类型不同,不能使用拷贝初始化
	// vector dvecl(ivec);
	// 元素类型相容,因此可采用范围初始化
	vector<double> dvecl(ivec.begin(), ivec.end());

	cout << dvec.capacity() << " " << dvec.size() << " "
		<< dvec[0] << " " << dvec[dvec.size() - 1] << endl;
	cout << dvecl.capacity() << " " << dvecl.size() << " "
		<< dvecl[0] << " " << dvecl[dvecl.size() - 1] << endl;

	system("pause");
	return 0;
}

在这里插入图片描述

练习9.14

编写程序,将一个 list 中的 char * 指针元素赋值给一个 vector 中的 string

解:

由于 listvector 是不同类型的容器,因此无法采用赋值运算符 = 来进行元素赋值。但 char* 可以转换为 string,因此可以采用范围赋值方式来实现本题要求。

#include
#include
#include
#include
using namespace std;
int main()
{
	list<char*> slist = { "hello", "world", "!" };
	vector<string> svec;

	// 容器类型不同,不能直接赋值
	//	svec = slist;
	// 元素类型相容,可采用范围初始化
	svec.assign(slist.begin(), slist.end());

	cout << svec.capacity() << " " << svec.size() << " " <<
		svec[0] << " " << svec[svec.size() - 1] << endl;

	system("pause");
	return 0;
}

在这里插入图片描述

练习9.15

编写程序,判定两个 vector 是否相等。

解:

标准库容器支持关系运算符,比较两个 vector 是否相等使用 == 运算符即可。当两个 vector 包含相同个数的元素,且对位元素都相等时,判定两个 vector 相等,否则不等。

两个 vectorcapacity 不会影响相等性判定,因此,当下面程序中 ivec1 在添加、删除元素导致扩容后,仍然与 ivec 相等。

#include
#include
#include
using namespace std;
int main()
{
	vector<int> ivec = { 1, 2, 3, 4, 5, 6, 7 };
	vector<int> ivec1 = { 1, 2, 3, 4, 5, 6, 7 };
	vector<int> ivec2 = { 1, 2, 3, 4, 5 };
	vector<int> ivec3 = { 1, 2, 3, 4, 5, 6, 8 };
	vector<int> ivec4 = { 1, 2, 3, 4, 5, 7, 6 };

	cout << (ivec == ivec1) << endl;
	cout << (ivec == ivec2) << endl;
	cout << (ivec == ivec3) << endl;
	cout << (ivec == ivec4) << endl;

	ivec.push_back(8);
	ivec1.pop_back();
	cout << ivec.capacity() << " " << ivec.size() << endl;
	cout << (ivec == ivec1) << endl;

	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第1张图片

练习9.16

重写上一题的程序,比较一个 list 中的元素和一个 vector 中的元素。

解:

两个容器相等的充分条件是包含相同个数的元素,且对位元素的值都相等。因此,首先判断两个容器是否包含相同个数的元素,若不等,则两个容器也不等。否则,遍历两个容器中的元素,两两比较对位元素的值,若有元素不相等,则容器不等。否则,两个容器相等。

    std::list<int>      li{ 1, 2, 3, 4, 5 };
    std::vector<int>    vec2{ 1, 2, 3, 4, 5 };
    std::vector<int>    vec3{ 1, 2, 3, 4 };

    std::cout << (std::vector<int>(li.begin(), li.end()) == vec2 ? "true" : "false") << std::endl;
    std::cout << (std::vector<int>(li.begin(), li.end()) == vec3 ? "true" : "false") << std::endl;

《C++ Primer》习题参考答案:第9章 - 顺序容器_第2张图片

练习9.17

假定 c1c2 是两个容器,下面的比较操作有何限制?

解:

	if (c1 < c2)

首先,容器类型必须相同,元素类型也必须相同。

其次,元素类型必须支持 < 运算符。

练习9.18

编写程序,从标准输入读取 string 序列,存入一个 deque 中。编写一个循环,用迭代器打印 deque 中的元素。

解:

#include
#include
#include
using namespace std;
int main()
{
	deque<string> sd;
	string word;
	while (cin >> word)
	{
		sd.push_back(word);
	}
	for (auto si = sd.cbegin(); si != sd.cend(); si++){
		cout << *si << endl;
	}

	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第3张图片

练习9.19

重写上一题的程序,用 list 替代 deque。列出程序要做出哪些改变。

解:

list 来说,在任何位置添加新元素都有很好的性能,遍历操作也能高效完成,因此程序与上一题并无太大差异。

#include
#include
#include
using namespace std;
int main()
{
	list<string> sl;
	string word;
	while (cin >> word)
	{
		sl.push_back(word);
	}
	for (auto si = sl.cbegin(); si != sl.cend(); si++){
		cout << *si << endl;
	}

	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第4张图片

练习9.20

编写程序,从一个 list 拷贝元素到两个 deque 中。值为偶数的所有元素都拷贝到一个 deque 中,而奇数值元素都拷贝到另一个 deque 中。

解:

#include
#include
#include
#include
using namespace std;
int main()
{
	list<int> ilist = { 1, 2, 3, 4, 5, 6, 7, 8 };
	deque<int> odd_d, even_d;

	for (auto iter = ilist.cbegin(); iter != ilist.cend();iter++){
		if (*iter & 1){
			odd_d.push_back(*iter);
		}
		else{
			even_d.push_back(*iter);
		}
	}
	for (auto iter = odd_d.cbegin(); iter != odd_d.cend(); iter++){
		cout << *iter << " ";
	}
	cout << endl;
	for (auto iter = even_d.cbegin(); iter != even_d.cend(); iter++){
		cout << *iter << " ";
	}
	cout << endl;

	system("pause");
	return 0;
}

在这里插入图片描述

练习9.21

如果我们将第308页中使用 insert 返回值将元素添加到 list 中的循环程序改写为将元素插入到 vector 中,分析循环将如何工作。

《C++ Primer》习题参考答案:第9章 - 顺序容器_第5张图片

解:

在循环之前,vector 为空,此时将 iter 初始化为 vector 首位置,与初始化为尾后位置效果是一样的。循环中第一次调用 insert 会将读取的第一个 string 插入到 iter 指向位置之前的位置,即,令新元素成为 vector 的首元素,而 insert 的返回指向此元素的迭代器,我们将它赋予 iter,从而使得 iter 始终指向 vector 的首元素。接下来的每个循环步均是如此,将新 string 插入到 vector 首元素之前的位置,成为新的首元素,并使 iter 始终指向 vector 首。这样,stringvector 排列的顺序将与它们的输入顺序恰好相反。整个循环执行的过程和最后的结果都与 list 版本没有什么区别。

但要注意,在 list 首元素之前插入新元素性能很好,但对于 vector,这样的操作需要移动所有现有元素,导致性能很差。

#include
#include
#include
using namespace std;
int main()
{
	vector<string> svec;
	string word;
	auto iter = svec.begin();
	while (cin >> word)
	{
		svec.insert(iter, word);
	}
	for (auto iter = svec.cbegin(); iter != svec.cend(); iter++){
		cout << *iter << endl;
	}

	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第6张图片

练习9.22

假定 iv 是一个 intvector,下面的程序存在什么错误?你将如何修改?

解:

vector<int>::iterator iter = iv.begin(),
					  mid = iv.begin() + iv.size() / 2;
while (iter != mid)
	if (*iter == some_val)
		iv.insert(iter, 2 * some_val);

解:

循环中未对 iter 进行递增操作,iter 无法向中点推进。其次,即使加入了 iter++ 语句,由于向 iv 插入元素后,iter 已经失效,iter++ 也不能起到将迭代器向前推进一个元素的作用。修改方法如下:

首先,将 insert 返回的迭代器赋予 iter,这样,iter 将指向新插入的元素 y。我们知道,inserty 插入到 iter 原来指向的元素 x 之前的位置,因此,接下来我们需要进行两次 iter++ 才能将 iter 推进到 x 之后的位置。

其次,insert() 也会使 mid 失效,因此,只正确设置 iter 仍不能令循环在正确的时候结束,我们还需设置 mid 使之指向 iv 原来的中央元素。在未插入任何新元素之前,此位置是 iv.begin()+iv.size()/2,我们将此时的 iv.size() 的值记录在变量 org_size 中,然后在循环过程中统计新插入的元素的个数 new_ele,则在任何时候,iv.begin()+org_size/2+newele 都能正确指向 iv 原来的中央元素。

#include
#include
#include
using namespace std;
int main()
{
	vector<int> iv = { 1, 1, 2, 1 };
	int some_val = 1;
	vector<int>::iterator iter = iv.begin();
	int org_size = iv.size(), new_ele = 0;
	while (iter != (iv.begin() + org_size / 2 + new_ele))
	{
		if (*iter == some_val){
			iter = iv.insert(iter, 2 * some_val);
			new_ele++;
			iter++; iter++;
		}
		else{
			iter++;
		}
	}
	for (iter = iv.begin(); iter != iv.end(); iter++){
		cout << *iter << endl;
	}

	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第7张图片

练习9.23

在本节第一个程序中,若 c.size() 为1,则 valval2val3val4 的值会是什么?

解:

4个变量的值会一样,都等于容器中唯一一个元素的值。

练习9.24

编写程序,分别使用 at、下标运算符、frontbegin 提取一个 vector 中的第一个元素。在一个空 vector 上测试你的程序。

解:

下面的程序会异常终止。因为 vector 为空,此时用 at 访问容器的第一个元素会抛出一个 out_of_range 异常,而此程序未捕获异常,因此程序会因异常退出。正确的编程方式是,捕获可能的 out of range 异常,进行相应的处理。

但对于后三种获取容器首元素的方法,当容器为空时,不会抛出 out_of_range 异常,而是导致程序直接退出(注释掉前几条语句即可看到后面语句的执行效果)。因此,正确的编程方式是,在采用这几种获取容器的方法时,检查下标的合法性(对 frontbegin 只需检查容器是否为空),确定没有问题后再获取元素。当然这种方法对 at 也适用。

#include 
#include 
using namespace std;
int main()
{
	vector<int> iv;
	cout << iv.at(0);
	cout << iv[0];
	cout << iv.front();
	cout << *(iv.begin());

	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第8张图片

练习9.25

对于第312页中删除一个范围内的元素的程序,如果 elem1elem2 相等会发生什么?如果 elem2 是尾后迭代器,或者 elem1elem2 皆为尾后迭代器,又会发生什么?

解:

如果两个迭代器 elem1elem2 相等,则什么也不会发生,容器保持不变。哪怕两个迭代器是指向尾后位置(例如 end()+1),也是如此,程序也不会出错。因此 elem1elem2 都是尾后迭代器时,容器保持不变。

如果 elem2 为尾后迭代器,elem1 指向之前的合法位置,则会删除从 elem1 开始直至容器末尾的所有元素。

练习9.26

使用下面代码定义的 ia,将 ia 拷贝到一个 vector 和一个 list 中。是用单迭代器版本的 eraselist 中删除奇数元素,从 vector 中删除偶数元素。

int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 };

解:

当从 vector 中删除元素时,会导致删除点之后位置的迭代器、引用和指针失效。而 erase 返回的迭代器指向删除元素之后的位置。因此,将 erase 返回的迭代器赋予 iiv,使其正确向前推进。且尾后位置每个循环步中都用 end 重新获得,保证其有效。

对于 list,删除操作并不会令迭代器失效,但上述方法仍然是适用的。

#include
#include
#include
#include
using namespace std;
int main()
{
	int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 };
	vector<int> iv;
	list<int> il;

	iv.assign(ia, ia + 11);
	il.assign(ia, ia + 11);
	vector<int>::iterator iiv = iv.begin();
	while (iiv != iv.end())
	{
		if (!(*iiv & 1)){
			iiv = iv.erase(iiv);
		}
		else
			iiv++;
	}
	list<int>::iterator iil = il.begin();
	while (iil != il.end())
	{
		if (*iil & 1){
			iil = il.erase(iil);
		}
		else
			iil++;
	}
	for (iiv = iv.begin(); iiv != iv.end(); iiv++){
		cout << *iiv << " ";
	}
	cout << endl;
	for (iil = il.begin(); iil != il.end(); iil++){
		cout << *iil << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

在这里插入图片描述

练习9.27

编写程序,查找并删除 forward_list 中的奇数元素。

解:

关键点是理解 forward_list 其实是单向链表数据结构,只有前驱节点指向后继节点的指针,而没有反向的指针。因此,在 forward_list 中可以高效地从前驱转到后继,但无法从后继转到前驱。而当我们删除一个元素后,应该调整被删元素的前驱指针指向被删元素的后继,起到将该元素从链表中删除的效果。

因此,在 forward_list 中插入、删除元素既需要该元素的迭代器,也需要前驱迭代器。为此,forward_list 提供了 before_begin 来获取首元素之前位置的迭代器,且插入、删除都是 after 形式,即删除(插入)给定迭代器的后继。

#include 
#include 
using namespace std;
int main()
{
	forward_list<int> iflst = { 1, 2, 3, 4, 5, 6, 7, 8 };
	auto prev = iflst.before_begin();
	auto curr = iflst.begin();

	while (curr != iflst.end())
	{
		if (*curr & 1){
			curr = iflst.erase_after(prev);
		}
		else{
			prev = curr;
			++curr;
		}
	}
	for (curr = iflst.begin(); curr != iflst.end(); curr++){
		cout << *curr << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

在这里插入图片描述

练习9.28

编写函数,接受一个 forward_list 和两个 string 共三个参数。函数应在链表中查找第一个 string,并将第二个 string 插入到紧接着第一个 string 之后的位置。若第一个 string 未在链表中,则将第二个 string 插入到链表末尾。

解:

与删除相同的是,forwardlist 的插入操作也是在给定元素之后。不同的是,插入一个新元素后,只需将其后继修改为给定元素的后继,然后修改给定元素的后继为新元素即可,不需要前驱迭代器参与。但对于本题,当第一个 string 不在链表中时,要将第二个 string 插入到链表末尾。因此仍然需要维护前驱迭代器,当遍历完链表时,prev 指向尾元素,curr 指向尾后位置。若第一个 string 不在链表中,此时只需将第二个 string 插入到 prev 之后即可。

总体来说,单向链表由于其数据结构上的局限,为实现正确插入、删除操作带来了困难。标准库的 forward_list 容器为我们提供了一些特性,虽然(与其他容器相比)我们仍需维护一些额外的迭代器,但已经比直接用指针来实现链表的插入、删除方便了许多。

#include 
#include 
#include 
using namespace std;
void test_and_insert(forward_list<string> &sflst, const string &s1, const string &s2)
{
	auto prev = sflst.before_begin();
	auto curr = sflst.begin();
	bool inserted = false;
	while (curr != sflst.end())
	{
		if (*curr == s1)
		{
			curr = sflst.insert_after(curr, s2);
			inserted = true;
		}
		prev = curr;
		++curr;
	}
	if (!inserted){
		sflst.insert_after(prev, s2);
	}
}
int main()
{
	forward_list<string> sflst = { "Hello", "!", "world", "!" };
	test_and_insert(sflst, "Hello", "你好");
	for (auto curr = sflst.begin(); curr != sflst.end(); curr++){
		cout << *curr << " ";
	}
	cout << endl;
	test_and_insert(sflst, "!", "?");
	for (auto curr = sflst.begin(); curr != sflst.end(); curr++){
		cout << *curr << " ";
	}
	cout << endl;
	test_and_insert(sflst, "Bye", "再见");
	for (auto curr = sflst.begin(); curr != sflst.end(); curr++){
		cout << *curr << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第9张图片

练习9.29

假定 vec 包含25个元素,那么 vec.resize(100) 会做什么?如果接下来调用 vec.resize(10) 会做什么?

解:

调用 vec.resize(100) 会向 vec 末尾添加75个元素,这些元素将进行值初始化。

接下来调用 vec.resize(10) 会将 vec 末尾的90个元素删除。

练习9.30

接受单个参数的 resize 版本对元素类型有什么限制(如果有的话)?

解:

对于元素是类类型,则单参数 resize 版本要求该类型必须提供一个默认构造函数。

练习9.31

第316页中删除偶数值元素并复制奇数值元素的程序不能用于 listforward_list。为什么?修改程序,使之也能用于这些类型。

解:

listforward_list 与其他容器的一个不同是,迭代器不支持加减运算,究其原因,链表中元素并非在内存中连续存储,因此无法通过地址的加减在元素间远距离移动。因此,应多次调用++来实现与迭代器加法相同的效果。

#include 
#include 
using namespace std;
int main()
{
	list<int> ilst = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	auto curr = ilst.begin();

	while (curr != ilst.end())
	{
		if (*curr & 1){
			curr = ilst.insert(curr, *curr);
			curr++; curr++;
		}
		else{
			curr = ilst.erase(curr);
		}
	}
	for (curr = ilst.begin(); curr != ilst.end(); curr++){
		cout << *curr << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

在这里插入图片描述
对于 forward_list,由于是单向链表结构,删除元素时,需将前驱指针调整为指向下一个节点,因此需维护 前驱后继 两个迭代器。

#include 
#include 
using namespace std;
int main()
{
	forward_list<int> iflst = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	auto prev = iflst.before_begin();
	auto curr = iflst.begin();

	while (curr != iflst.end())
	{
		if (*curr & 1){
			curr = iflst.insert_after(curr, *curr);
			prev = curr;
			curr++;
		}
		else{
			curr = iflst.erase_after(prev);
		}
	}
	for (curr = iflst.begin(); curr != iflst.end(); curr++){
		cout << *curr << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

在这里插入图片描述

练习9.32

在第316页的程序中,向下面语句这样调用 insert 是否合法?如果不合法,为什么?

iter = vi.insert(iter, *iter++);

解:

很多编译器对实参求值、向形参传递的处理顺序是由右至左的。这意味着,编译器在编译上述代码时,首先对 *iter++ 求值,传递给 insert 第二个形参,此时 iter 已指向当前奇数的下一个元素,因此传递给 insert 的第一个参数的迭代器指向的是错误位置,程序执行会发生混乱,最终崩溃。

因此,若将代码改为 iter=vi.insert(iter++, *iter);,或是使用由左至右求值、传递参数的编译器,代码的运行结果是正确的。当然,这样的代码在逻辑上是毫无道理的。

练习9.33

在本节最后一个例子中,如果不将 insert 的结果赋予 begin,将会发生什么?编写程序,去掉此赋值语句,验证你的答案。

解:

vector 中插入新元素后,原有迭代器都会失效。因此,不将 insert() 返回的迭代器赋予 begin,会使 begin 失效。继续使用 begin 会导致程序崩溃。对此程序,保存尾后迭代器和不向 begin 赋值两个错误存在其一,程序都会崩溃。

练习9.34

假定 vi 是一个保存 int 的容器,其中有偶数值也有奇数值,分析下面循环的行为,然后编写程序验证你的分析是否正确。

iter = vi.begin();
while (iter != vi.end())
	if (*iter % 2)
		iter = vi.insert(iter, *iter);
	++iter;

解:

此段代码的第一个错误是忘记使用花括号,使得 ++iter 变成循环后的第一条语句,而非所期望的循环体的最后一条语句。因此,除非容器为空,否则程序会陷入死循环:

  1. 若容器的第一个元素是偶数,布尔表达式为假,if 语句真分支不会被执行,iter 保持不变。循环继续执行,真分支仍然不会执行,iter 继续保持不变,如此陷入死循环。
  2. 若容器的第一个元素是奇数,insert 语句被调用,将该值插入到首元素之前,并将返回的迭代器(指向新插入元素)赋予 iter,因此 iter 指向新首元素。继续执行循环,会继续将首元素复制到容器首位置,并令 iter 指向它,如此陷入死循环。

示例(粗体代表迭代器位置):

  • 初始:{1,2,3,4,5,6,7,8,9}
  • 第一步:{1,1,2,3,4,5,6,7,8,9}
  • 第二步:{1,1,1,2,3,4,5,6,7,8,9}

练习9.35

解释一个 vectorcapacitysize 有何区别。

解:

capacity 返回已经为 vector 分配了多大内存空间(单位是元素大小),也就是在不分配新空间的情况下,容器可以保存多少个元素。

size 则返回容器当前已经保存了多少个元素。

练习9.36

一个容器的 capacity 可能小于它的 size 吗?

解:

由上一题解答可知,这是不可能的。

练习9.37

为什么 listarray 没有 capacity 成员函数?

解:

list 是链表,当有新元素加入时,会从内存空间中分配一个新节点保存它;当从链表中删除元素时,该节点占用的内存空间会被立刻释放。因此,一个链表占用的内存空间总是与它当前保存的元素所需空间相等(换句话说,capacity 总是等于 size)。

array 是固定大小数组,内存一次性分配,大小不变,不会变化。

因此它们均不需要 capacity

练习9.38

编写程序,探究在你的标准实现中,vector 是如何增长的。

解:

#include 
#include 
#include 
using namespace std;
int main()
{
	vector<int> vi;
	int s, c;

	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	for (s = vi.size(), c = vi.size(); s <= c; s++){
		vi.push_back(1);
	}
	cout << "capacity: " << vi.capacity() << "  size: " << vi.size() << endl;
	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第10张图片

练习9.39

解释下面程序片段做了什么:

vector<string> svec;
svec.reserve(1024);
string word;
while (cin >> word)
	svec.push_back(word);
svec.resize(svec.size() + svec.size() / 2);

解:

首先,resersesvec 分配了1024个元素(字符串)的空间。

随后,循环会不断读入字符串,添加到 svec 末尾,直至遇到文件结束符。这个过程中,如果读入的字符串数量不多于1024,则 svec 的容量(capacity)保持不变,不会分配新的内存空间。否则,会按一定规则分配更大的内存空间,并进行字符串的移动。

接下来,resize 将向 svec 末尾添加当前字符串数量一半那么多的新字符串,它们的值都是空串。若空间不够,会分配足够容纳这些新字符串的内存空间。

练习9.40

如果上一题的程序读入了256个词,在 resize 之后容器的 capacity 可能是多少?如果读入了512个、1000个、或1048个呢?

解:

  • 若读入了256个词,则 resize 之后容器的 capacity 将是384。
  • 若读入了512个词,则 resize 之后容器的 capacity 将是768。
  • 若读入了1000个词,则 resize 之后容器的 capacity 将是2048。
  • 若读入了1048个词,则 resize 之后容器的 capacity 将是2048。

练习9.41

编写程序,从一个 vector 初始化一个 string

解:

vector 提供了 data 成员函数,返回其内存空间的首地址。将此返回值作为 string 的构造函数的第一个参数,将 vectorsize 返回值作为第二个参数,即可获取 vector 中的数据,将其看作一个字符数组来初始化 string

#include 
#include 
#include 
using namespace std;
int main()
{
	vector<char> vc = { 'H', 'E', 'L', 'L', 'O' };
	string s(vc.data(), vc.size());
	cout << s << endl;
	
	system("pause");
	return 0;
}

在这里插入图片描述

练习9.42

假定你希望每次读取一个字符存入一个string中,而且知道最少需要读取100个字符,应该如何提高程序的性能?

解:

由于知道至少读取100个字符,因此可以用 reserve 先为 string 分配100个字符的空间,然后逐个读取字符,用 push_back 添加到 string 末尾。

#include 
#include 
#include 
using namespace std;
void input_string(string &s){
	s.reserve(100);
	char c;
	while (cin >> c)
	{
		s.push_back(c);
	}
}
int main()
{
	string s;
	input_string(s);
	cout << s << endl;
	
	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第11张图片

练习9.43

编写一个函数,接受三个 string 参数是 soldValnewVal。使用迭代器及 inserterase 函数将 s 中所有 oldVal 替换为 newVal。测试你的程序,用它替换通用的简写形式,如,将"tho"替换为"though",将"thru"替换为"through"。

解:

由于要求使用迭代器,因此使用如下算法:

  1. 用迭代器 iter 遍历字符串 s。注意,对于 s 末尾少于 oldVal 长度的部分,己不可能与 oldVal 相等,因此无须检查。
  2. 对每个位置,用一个循环检查 s 中字符是否与 oldVal 中的字符都相等。
  3. 若循环是因为 iter2==oldVal.end 而退出,表明 siter 开始的子串与 oldVal 相等。则调用 erase 将此子串删除,接着用一个循环将 newVal 复制到当前位置。由于 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)
	{
		auto iter1 = iter;
		auto iter2 = oldVal.begin();
		while (iter2 != oldVal.end() && *iter1 == *iter2)
		{
			iter1++;
			iter2++;
		}
		if (iter2 == oldVal.end()){
			iter = s.erase(iter, iter1);
			if (newVal.size()){
				iter2 = newVal.end();
				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;
	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第12张图片

练习9.44

重写上一题的函数,这次使用一个下标和 replace

解:

由于可以使用下标和 replace,因此可以更为简单地实现上一题的目标。通过 find 成员函数(只支持下标参数)即可找到 s 中与 oldVal 相同的子串,接着用 replace 即可将找到的子串替换为新内容。可以看到,使用下标而不是迭代器,通常可以更简单地实现字符串操作。

#include 
#include 
#include 
using namespace std;
void replace_string(string &s, const string &oldVal, const string &newVal)
{
	int p = 0;
	while ((p = s.find(oldVal, p)) != string::npos)
	{
		s.replace(p, oldVal.size(), 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;
	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第13张图片

练习9.45

编写一个函数,接受一个表示名字的 string 参数和两个分别表示前缀(如"Mr.“或"Mrs.”)和后缀(如"Jr.“或"III”)的字符串。使用迭代器及 insertappend 函数将前缀和后缀添加到给定的名字中,将生成的新 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;
	system("pause");
	return 0;
}

在这里插入图片描述

练习9.46

重写上一题的函数,这次使用位置和长度来管理 string,并只使用 insert

解:

#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;
	system("pause");
	return 0;
}

在这里插入图片描述

练习9.47

编写程序,首先查找 string "ab2c3d7R4E6"中每个数字字符,然后查找其中每个字母字符。编写两个版本的程序,第一个要使用 find_first_of,第二个要使用 find_first_not_of

解:

find_first_of 在字符串中查找给定字符集合中任一字符首次出现的位置。若查找数字字符,则“给定字符集合”应包含所有10个数字;若查找字母,则要包含所有大小写字母—abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOQRSTUVWXYZ

#include 
#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, "abcdefghijklmnopqrstuvwxyz" \
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ");
	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第14张图片

find_first_not_of 查找第一个不在给定字符集合中出现的字符,若用它查找某类字符首次出现的位置,则应使用补集。若查找数字字符,则“给定字符集合”应包含10个数字之外的所有字符—abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL MNOPQRSTUVWXYZ;若查找字母,则要包含所有非字母字符。

注意,这一设定仅对此问题要查找的字符串有效—它只包含字母和数字。因此,字母和数字互为补集。若字符串包含任意 ASCII 字符,可以想见,正确的“补集”可能非常冗长。

#include 
#include 
#include 
using namespace std;
void find_not_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_not_char(s, "abcdefghijklmnopqrstuvwxyz" \
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ");
	cout << endl << "查找所有字母" << endl;
	find_not_char(s, "0123456789");
	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第15张图片

练习9.48

假定 namenumbers 的定义如325页所示,numbers.find(name) 返回什么?

解:
在这里插入图片描述
s.find(args) 查找 sargs 第一次出现的位置,即第一个与 args 匹配的字符串的位置。args 是作为一个字符串整体在 s 中查找,而非一个字符集合在 s 中查找其中字符。因此,对325页给定的 namenumbers 值,在 numbers 中不存在与 name 匹配的字符串,find 会返回 npos

练习9.49

如果一个字母延伸到中线之上,如 df,则称其有上出头部分(ascender)。如果一个字母延伸到中线之下,如 pg,则称其有下出头部分(descender)。编写程序,读入一个单词文件,输出最长的既不包含上出头部分,也不包含下出头部分的单词。

解:

查找既不包含上出头字母,也不包含下出头字母的单词,等价于“排除包含上出头字母或下出头字母的单词”。因此,用 find_first_of 在单词中查找上出头字母或下出头字母是否出现。若出现(返回一个合法位置,而非 npos)则丢弃此单词,继续检查下一个单词。否则,表明单词符合要求,检查它是否比之前的最长合法单词更长,若是,记录其长度和内容。文件读取完毕后,输出最长的合乎要求的单词。

#include 
#include 
#include 
#include           // file I/O support
#include           // support for exit()
using namespace std;
void find_longest_word(ifstream &in){
	string s, longest_word;
	int 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()
{
	char filename[20];
	cout << "Enter name of data file: ";
	cin.getline(filename, 20);
	ifstream inFile;        // object for handling file input
	inFile.open(filename);  // associate inFile with a file
	if (!inFile.is_open())  // failed to open file
	{
		cout << "Could not open the file " << filename << endl;
		cout << "Program terminating.\n";
		// cin.get();    	// keep window open
		exit(EXIT_FAILURE);
	}
	find_longest_word(inFile);
	inFile.close();         // finished with the file
	
	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第16张图片
《C++ Primer》习题参考答案:第9章 - 顺序容器_第17张图片

练习9.50

编写程序处理一个 vector,其元素都表示整型值。计算 vector 中所有元素之和。修改程序,使之计算表示浮点值的 string 之和。

解:

标准库提供了将字符串转换为整型函数 stoi。如果希望转换为不同整型类型,如长整型、无符号整型等,标准库也都提供了相应的版本。

#include 
#include 
#include 
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;
	
	system("pause");
	return 0;
}

在这里插入图片描述

标准库也提供了将字符串转换为浮点数的函数,其中 stof 是转换为单精度浮点数。简单修改上面的程序即可实现本题的第二问。

#include 
#include 
#include 
using namespace std;
int main()
{
	vector<string> vs = { "12.3", "-4.56", "+7.8e-2" };
	float sum = 0;
	for (auto iter = vs.begin(); iter != vs.end(); iter++){
		sum += stof(*iter);
	}
	cout << "和:" << sum << endl;
	
	system("pause");
	return 0;
}

在这里插入图片描述
注意,当给定的字符串不能转换为数值时(不是所需类型数值的合法表示),这些转换函数会抛出 invalid_argument 异常;如果表示的值超出类型所能表达的范围,则抛出一个 out_of_range 异常。

练习9.51

设计一个类,它有三个 unsigned 成员,分别表示年、月和日。为其编写构造函数,接受一个表示日期的 string 参数。你的构造函数应该能处理不同的数据格式,如January 1,1900、1/1/1990、Jan 1 1900 等。

解:

在头文件中定义了 date 类。构造函数 date(string &ds) 从字符串中解析出年、月、日的值,大致步骤如下:

  1. 若首字符是数字,则为格式2,用 stoi 提取月份值,若月份值不合法,抛出异常,否则转到步骤6。
  2. 若首字符不是数字,表明是格式1或3,首先提取月份值。
  3. ds 开始的子串与月份简称进行比较,若均不等,抛出异常(若与简称不等,则不可能与全称相等)。
  4. 若与第 i 个月简称相等,且下一个字符是合法间隔符,返回月份值。
  5. 否则,检查接下来的子串是否与全称剩余部分相等,若不等,抛出异常;否则,返回月份值。
  6. stoi 提取日期值和年份值,如需要,检查间隔符合法性。

此程序已经较为复杂,但显然离“完美”还差很远,只能解析3种格式,且进行了很多简化。程序中已经给出了几种格式错误,读者可尝试构造其他可能的格式错误。并尝试补充程序,支持其他格式,如“2006年7月12日”。

data.h

#ifndef DATE_H_INCLUDED
#define DATE_H_INCLUDED
#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	// DATE_H_INCLUDED

main.cpp

#include 
#include 
#include "data.h"
using namespace std;
int main()
{
	string dates[] = { "Jan 1, 2014", "February 1 2014", "3/1/2014", "3 1 2014"
		//"Jcn 1,2014",
		//"Janvary 1,2014",
		//"Jan 32,2014",
		//"Jan 1/2014",
	};
	try{
		for (auto ds : dates){
			date dl(ds);
			cout << dl;
		}
	}
	catch (invalid_argument e){
		cout << e.what() << endl;
	}
	system("pause");
	return 0;
}

《C++ Primer》习题参考答案:第9章 - 顺序容器_第18张图片

练习9.52

使用 stack 处理括号化的表达式。当你看到一个左括号,将其记录下来。当你在一个左括号之后看到一个右括号,从 stackpop 对象,直至遇到左括号,将左括号也一起弹出栈。然后将一个值(括号内的运算结果)push 到栈中,表示一个括号化的(子)表达式已经处理完毕,被其运算结果所替代。

解:

如下所示,本题的解答已经较为复杂,但这已是将问题大幅简化之后的解答了。我们假定表达式中只有加、减两种运算,由于所有运算优先级相同,无须进行复杂的优先级判断。由于加、减运算都是左结合,因此,遇到一个数值,直接与之前的运算数进行它们中间的运算即可。唯一的例外是括号,它将改变计算次序——括号里的运算将先于括号前的运算执行。不过,我们可以将它看作“阻断”了括号前的表达式,将括号内的部分看作一个独立的表达式优先处理,计算结果作为一个运算数参与之前的运算即可。

表达式解析的逻辑大致如下:

  1. 读入了一个运算数 v
    a)若栈空或栈顶是左括号,则 v 是第一个运算数,直接压栈即可。
    b)否则,v 前必须是一个运算符,再之前是另一个运算数 v',从栈顶弹出这两项,将计算结果压栈即可;否则,就抛出一个“缺少运算符”异常。
  2. 读入了一个左括号,直接压栈。
  3. 读入了一个运算符,
    a)若栈空或栈顶不是一个运算数,则抛出一个“缺少运算数”异常。注意,若运算符之前是一个右括号,之前也已处理完毕,栈顶是其计算结果,仍应该是运算数,不影响此逻辑。
    b)否则,运算符压栈。
  4. 读入了一个右括号,
    a)若栈空,表明之前没有与之配对的左括号,抛出“未匹配右括号”异常。
    b)若栈顶是左括号,表明括号对之间无表达式,抛出“空括号”异常。
    c)若栈顶不是运算数,表明括号内缺少一个运算数,抛出一个异常。
    d)弹出此运算数 v,若栈空或栈顶不是左括号,仍抛出“未匹配右括号”异常;否则弹出左括号,把 v作为新运算数,执行1中的逻辑。
  5. 以上均不是,则出现了非法输入,会在转换为数值时产生异常。
  6. 当字符串处理完毕后,判断栈中是否有且仅有一个运算数,若是,此值即为表达式运算结果,输出它;否则,表达式非法。

值得注意的是,为了在栈中保存括号、运算符和运算数三类对象,程序中定义了枚举类型 obj_type。栈中每个对象都保存了类型 t 和数值 v(如果 tVAL 的话)。

#include
#include
#include
#include
#include
using namespace std;
//表示栈中对象的不同类型
enum obj_type{ LP, RP, ADD, SUB, VAL };
struct obj{
	obj(obj_type type, double val = 0){ t = type; v = val; }
	obj_type t;
	double v;
};
inline void skipws(string &exp, size_t &p){
	p = exp.find_first_not_of(" ", p);
}
inline void new_val(stack<obj> &so, double v){
	if (so.empty() || so.top().t == LP){	//空栈或左括号
		so.push(obj(VAL, v));
		//cout << "push" << v << endl;
	}
	else if(so.top().t == ADD || so.top().t == SUB){
		//之前是运算符
		obj_type type = so.top().t;
		so.pop();
		/*
		if(type == ADD)
			cout << "pop+" << endl;
		else 
			cout << "pop-" << endl;
		*/
		// cout << "pop" << so.top().v << endl;
		// 执行加减法
		if (type == ADD)
			v += so.top().v;
		else
			v = so.top().v - v;
		so.pop();
		so.push(obj(VAL, v));			//运算结果压栈
		//cout <<"push"<< v<< endl;
	}
	else 
		throw invalid_argument("缺少运算符");
}

int main()
{
	stack<obj> so;
	string exp;
	size_t p = 0, q;
	double v;
	cout << "请输入表达式:";
	getline(cin, exp);
	while (p < exp.size()){
		skipws(exp, p);					//跳过空格
		if (exp[p] == '('){				//左括号直接压栈
			so.push(obj(LP));
			p++;
			//cout <<"push LP"<< endl;
		}
		else if (exp[p] == '+' || exp[p] == '-'){
			//新运算符
			if (so.empty() || so.top().t != VAL)
				//空栈或之前不是运算数
				throw invalid_argument("缺少运算数");
			if (exp[p] == '+')			//运算符压栈
				so.push(obj(ADD));
			else
				so.push(obj(SUB));
			p++;
			//cout <<"push"<< exp[p-1]<< endl;
		}
		else if (exp[p] == ')'){		//右括号
			p++;
			if (so.empty())				//之前无配对的左括号
				throw invalid_argument("未匹配右括号");
			if (so.top().t == LP)		//一对括号之间无内容
				throw invalid_argument("空括号");
			if (so.top().t == VAL){		//正确:括号内运算结果
				v = so.top().v;
				so.pop();
				//cout <<"pop"<
				if (so.empty() || so.top().t != LP)
					throw invalid_argument("未匹配右括号");
				so.pop();
				//cout <<"pop LP"<
				new_val(so, v);			//与新运算数逻辑一致
			}
			else
				//栈顶必定是运算符
				throw invalid_argument("缺少运算数");
		}
		else{			//应该是运算数
			v = stod(exp.substr(p), &q);
			p += q;
			new_val(so, v);
		}
	}
	if (so.size() != 1 || so.top().t != VAL)
		throw invalid_argument("非法表达式");
	cout << so.top().v << endl;

	system("pause");
	return 0;
}

在这里插入图片描述

最后两道题看的大佬书籍上的写法,还在研究中。

如果想要更多的资源,欢迎关注 @我是管小亮,文字强迫症MAX~

回复【福利】即可获取我为你准备的大礼,包括C++,编程四大件,NLP,深度学习等等的资料。

想看更多文(段)章(子),欢迎关注微信公众号「程序员管小亮」~

在这里插入图片描述

你可能感兴趣的:(C++学习笔记❤️)