begin
和end
成员array
具有固定大小assign
(仅顺序容器)swap
push_back
push_front
insert
的返回值emplace
操作pop_front
和pop_back
成员函数forward_list
操作end
返回的迭代器capacity
和size
substr
操作append
和replace
函数string
的多种重载函数顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。
关联容器中元素的位置由元素相关联的关键字值决定。
表9.1: | 顺序容器类型 |
---|---|
vector |
可变大小数组,支持快速随机访问,在尾部之外的位置插入或删除元素可能很慢 |
deque |
双端队列,支持快速随机访问,在头尾位置插入 / 删除速度很快 |
list |
双向链表,只支持双向顺序访问,在list 中任何位置进行插入 / 删除操作速度都很快 |
forward_list |
单向链表,只支持单向顺序访问,在链表任何位置进行插入 / 删除操作速度都很快 |
array |
固定大小数组,支持快速随机访问,不能添加或删除元素 |
string |
与vector 相似的容器,但专门用于保存字符,随机访问快,在尾部插入 / 删除速度快 |
string
和 vector
将元素保存在连续的内存空间中。由于元素是连续存储的,由元素的下标来计算其地址是非常快速的。
但是,在这两种容器的中间位置添加或删除元素就会非常耗时:在一次插入或删除操作后,需要移动插入 / 删除位置之后的所有元素,来保持连续存储。而且添加一个元素有时还需要分配额外的存储空间。在这种情况下,每个元素都必须移动到新的存储空间中。
list
和 forward_list
两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。
作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。而且,与vector
、deque
和array
相比,这两个容器的额外内存开销也很大。
deque
是一个更为复杂的数据结构,与string
和vector
类似,deque
支持快速的随机访问。与string
和vector
一样,在deque
的中间位置添加或删除元素的代价(可能)很高。但是,在deque
的两段添加或删除元素都是很快的,与list
或forward_list
添加删除元素的速度相当。
array
对象的大小是固定的,因此,array
不支持添加和删除元素以及改变容器大小的操作。
forward_list
的设计目标是达到与最好的手写的单向链表数据结构相当的性能,因此forward_list
没有size
操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size
保证是一个快速的常量时间的操作。
Tip: 通常,使用vector
是最好的选择,除非你有很好的理由选择其他容器。
vector
。list
或forward_list
。vector
或deque
。list
或forward_list
。deque
。vector
追加数据,然后再调用标准库的sort
函数来重排容器中的元素,从而避免在中间位置添加元素。list
,一旦输入完成,将list
中的内容拷贝到一个vector
中。注意:如果不确定应该使用哪种容器,那么可以再程序中只使用vector
和list
公共的操作:使用迭代器,不使用下标操作,避免随机访问。这样,在必要时选择使用vector
或list
都很方便。
练习9.1:对于下面的程序任务,vector
、 deque
和list
哪种容器最为适合?解释你的选择的理由。如果没有哪一种容器优于其他容器,也请解释理由。
(a)读取固定数量的单词,将它们按字典序插入到容器中。我们将在下一章中看到, 关联容器更适合这个问题。
(a)“按字典序插入到容器中”意味着进行插入排序操作,从而需要在容器内部频繁进行插入操作,vector
在尾部之外的位置插入和删除元素很慢,deque
在头尾之外的位置插入和删除元素很慢,而list
在任何位置插入、删除速度都很决。
因此,这个任务选择list
更为适合。
当然,如果不是必须边读取单词边插入到容器中, 可以使用vector
,将读入的单词依次追加到尾部,读取完毕后,调用标准库到排序算法将单词重排为字典序。
(b)读取未知数量的单词,总是将新单词插入到末尾。删除操作在头部进行。
(b)由于需要在头、尾分别进行插入、删除操作,因此将 vector
排除在外,deque
和 list
都可以达到很好的性能。如果还需要频繁进行随机访问。则 deque
更好。
©从一个文件读取未知数量的整数。将这些数排序,然后将它们打印到标准输出。
© 由于整数占用空间很小,且快速的排序算法需频繁随机访问元素,将list
排除在外。由于无须在头部进行插入、删除操作,因此使用vector
即可,无须使用deque
。
每个容器都定义在一个头文件中,文件名与类型名相同。
即,deque
定义在头文件deque
中,list
定义在头文件list
中。
容器均定义为模板类。
例如对vector
,我们必须提供额外信息来生成特定的容器类型。
对大多数,但不是所有容器,我们还需要额外提供元素类型信息:
list<Sales_data> // 保存 Sales_data 对象的 list
deque<double> // 保存 double 的 deque
顺序容器几乎可以保存任意类型的元素。
vector<vector<string>> lines; // vector 的 vector
// 此处 lines 是一个 vector,其元素类型是 string 的 vector
顺序容器构造函数的一个版本,接受容器大小参数,它使用了元素类型的默认构造函数。
但某些类没有默认构造函数,我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数:
// 假定 noDefault 是一个没有默认构造函数的类型
vector<noDefault> v1(10, init); // 正确:提供了元素初始化器
vector<noDefault> v2(10); // 错误:必须提供一个元素初始化器
类型别名 | |
---|---|
iterator |
此容器类型的迭代器类型 |
const_iterator |
可以读取元素,但不能修改元素的迭代器类型 |
size_type |
无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
difference_type |
带符号整数类型,足够保存两个迭代器之间的距离 |
value_type |
元素类型 |
reference |
元素的左值类型:与value_type& 含义相同 |
const_reference |
元素的const 左值类型(即,const value_type& ) |
构造函数 | |
---|---|
C c |
默认构造函数,构造空容器 |
C c1(c2); |
构造 c2 的拷贝 c1 |
C c(b, e); |
构造 c ,将迭代器 b 和 e 指定的范围内的元素拷贝到 c (array 不支持) |
C c{a, b, c...}; |
列表初始化 c |
赋值与swap |
|
---|---|
c1 = c2 |
将 c1 中的元素替换为 c2 中元素 |
c1 = {a, b, c...} |
将 c1 中的元素替换为列表中元素(不适用于array ) |
a.swap(b) |
交换 a 和 b 的元素 |
swap(a, b) |
与 a.swap(b) 等价 |
大小 | |
---|---|
c.size() |
c 中元素的数目(不支持 forward_list ) |
c.max_size() |
c 可保存的最大元素数目 |
c.empty() |
若 c 中存储了元素,返回 false ,否则返回 true |
添加 / 删除元素(不适用于 array ) |
|
---|---|
c.insert(args) |
将args 中的元素拷贝进 c |
c.emplace(inits) |
使用 inits 构造 c 中的一个元素 |
c.erase(args) |
删除 args 指定的元素 |
c.clear() |
删除 c 中的所有元素,返回 void |
关系运算符 | |
== ,!= |
所有容器都支持相等(不等)运算符 |
< ,<= ,> ,>= |
关系运算符(无序关联容器不支持) |
获取迭代器 | |
---|---|
c.begin() , c.end() |
返回指向 c 的首元素和尾元素之后位置的迭代器 |
c.cbegin() , c.cend() |
返回 const_iterator |
反向容器的额外成员(不支持forward_list ) |
|
---|---|
reverse_iterator |
按逆序寻址元素的迭代器 |
const_reverse_iterator |
不能修改元素的逆序迭代器 |
c.rbegin() , c.rend() |
返回指向 c 的尾元素和首元素之前位置的迭代器 |
c.crbegin() , c.crend() |
返回 const_reverse_iterator |
练习9.2:定义一个list
对象,其元素类型是 int
的 deque
。
list
迭代器范围(iterator range):begin
到 end
(one past the last element),它们标记了容器中元素的一个范围。
这种元素范围被称为左闭合区间(left-inclusive interval),其标准数学描述为 [begin, end)
,end
可以与begin
指向相同的位置。
假定begin
和end
构成一个合法的迭代器范围,则
begin
与end
相等,则范围为空begin
与end
不等,则范围至少包含一个元素,且begin
指向该范围中的第一个元素begin
递增若干次,使得begin==end
while (begin != end)
*begin = val; // 正确:范围非空,因此 begin 指向一个元素
++begin; // 移动迭代器,获取下一个元素
练习9.3:构成迭代器范围的迭代器有何限制?
两个迭代器begin
和end
必须指向同一个容器中的元素,或者是容器最后一个元素之后的位置;
而且,对begin
反复进行递增操作,可保证到达end
,即end
不在begin
之前。
练习9.4:编写函数,接受一对指向 vector
的迭代器和一个 int
值。在两个迭代器指定的范围中查找给定的值,返回一个布尔值来指出是否找到。
/* 练习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;
return 0;
}
练习9.5:重写上一题的函数,返回一个迭代器指向找到的元素。注意,程序必须处理未找到给定值的情况。
/* 练习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;
return 0;
}
练习9.6:下面程序有何错误?你应该如何修改它?
list<int> lst1;
list<int>::iterator iter1 = lst1.begin(), iter2 = lst1.end();
while (iter1 < iter2) /* ...*/
【出题思路】
理解不同类型容器的迭代器之间的差别,更深层次的,理解数据结构的实现如何导致迭代器的差别。
【解答】
与vector
和deque
不同,list
的迭代器不支持<
运算,只支持递增、递减、 ==
以及!=
运算。
原因在于这几种数据结构实现上的不同。vector
和deque
将元素在内存中连续保存,而list
则是将元素以链表方式存储,因此前者可以方便地实现迭代器的大小比较(类似指针的大小比较)来体现元素的前后关系。而在list
中,两个指针的大小关系与它们指向的元素的前后关系并不一定是吻合的,实现<
运算将会非常困难和低效。
每个容器都定义了多个类型,如 size_type
、iterator
和 const_iterator
。
除了已经使用过的迭代器类型,大多数容器还提供反向迭代器。反向迭代器就是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义也都发生了颠倒。例如,对一个反向迭代器执行++
操作,会得到上一个元素。
类型别名,我们可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type
。如果需要元素类型的一个引用,可以使用reference
或const_reference
。
// 为了使用这些类型,我们必须显式使用其类名:
// iter 是通过 list 定义的一个迭代器类型
list<string>::iterator iter;
// count 是通过 vector 定义的一个 difference_type 类型
vector<int>::difference_type count;
练习9.7:为了索引 int
的 vector
中的元素,应该使用什么类型?
【出题思路】标准库容器定义了若干类型成员,对应容器使用中可能涉及的类型,如迭代器、元素引用等。
【解答】使用迭代器类型 vector
来索引int
的vector
中的元素。
练习9.8:为了读取string
的list
中的元素,应该使用什么类型?如果写入list
,又该使用什么类型?
【解答】
为了读取string
的list
中的元素,应使用list
,因为value_type
表示元素类型。
为了写入数据,需要(非常量)引用类型,因此应使用list
。
begin
和end
成员begin
和enc
有多个版本:带r
的版本返回反向迭代器;以c
开头的版本则返回const
迭代器:
list<string> a = {
"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin(); // list::iterator
auto it2 = a.rbegin(); // list::reverse_iterator
auto it3 = a.cbegin(); // list::const_iterator
auto it4 = a.crbegin(); // list::const_reverse_iterator
不以 c
开头的函数都是被重载过的。也就是说,实际上有两个名为begin
的成员。
一个是const
成员,返回容器的const_iterator
类型。另一个是非常量成员,返回容器的iterator
类型。rbegin
、en
和rend
的情况类似。当我们对一个非常量对象调用这些成员时,得到的是返回 iterator
的版本。只有在对一个const
对象调用这些函数时,才会得到一个const
版本。
// 显式指定类型
list<string>::iterator it5 = a.begin();
list<string>::const_iterator it6 = a.begin();
// 是 iterator 还是 const_iterator 依赖于 a 的类型
auto it7 = a.begin(); // 仅当 a 是 const 时,it7 是 const_iterator
auto it8 = a.cbegin(); // it8 是 const_iterator
练习9.9:begin
和cbegin
两个函数有什么不同?
cbegin
是C++新标准引入的,用来与 auto
结合使用。
它返回指向容器第一个元素的 const
代器,可以用来只读地访问容器元素,但不能对容器元素进行修改。因此,当不需要写访问时,应该使用 cbegin
。
begin
则是被重载过的,有两个版本:其中一个是 const
成员函数,也返回 const
迭代器:另一个则返回普通迭代器,可以对容器元索进行修改。
表9.3: | 容器定义和初始化 |
---|---|
C c |
默认构造函数,如果C 是一个array ,则c 中元素按默认方式初始化,否则c 为空 |
C c1(c2) C c1=c2 |
c1 初始化为c2 的拷贝,c1 和c2 必须是相同类型(即,它们必须是相同的容器类型,且保存的是相同的元素类型;对于array 类型,两者还必须具有相同大小) |
C c{a, b, c...} C c={a, b, c...} |
c 初始化为初始化列表中元素的拷贝,列表中元素的类型必须与c 的元素类型相容。对于 array 类型,列表中元素数目必须等于或小于array 的大小,任何遗漏的元素都进行值初始化 |
C c(b, e) |
c 初始化为迭代器b 和e 指定范围中的元素的拷贝。范围中元素的类型必须与c 的元素类型相容。 |
只有顺序容器(不包括array )的构造函数才能接受大小参数 |
|
C seq(n) |
seq 包含n 个元素,这些元素进行了值初始化;此构造函数是explicit 的 |
C seq(n, t) |
seq 包含n 个初始化为值t 的元素 |
将一个新容器创建为另一个容器的拷贝的方法有两种:
为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。
不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。
而且,新容器和元容器中的元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。
练习9.12:对于接受一个容器创建其拷贝的构造函数,和接受两个迭代器创建拷贝的构造函数,解释它们的不同。
接受一个已有容器的构造函数会拷贝此容器中的所有元素,这样,初始化完成后,我们得到此容器的一个一模一样的拷贝。当我们确实需要一个容器的完整拷贝时,这种初始化方式非常方便。
但当我们不需要已有容器中的全部元素,而只是想拷贝其中一部分元素时,可使用接受两个迭代器的构造函数。传递给它要拷贝的范围的起始和尾后位置的迭代器,即可令新容器对象包含所需范围中元素的拷贝。
// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {
"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {
"a", "an", "the"};
list<string> list2(authors); // 正确:类型匹配
deque<string> authList(authors); // 错误:容器类型不匹配
vector<string> words(articles); // 错误:元素类型必须匹配
// 正确:可以将 const char* 元素转换为 string
forward_list<string> words(articles.begin(), articles.end());
Note: 当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。
由于两个迭代器表示一个范围,因此可以使用这种构造函数来拷贝一个容器中的子序列。
例如,假定迭代器it
表示authors
中的一个元素,我们可以编写如下代码:
// 拷贝元素,直到(但不包括)it 指向的元素
deque<string> authList(authors.begin(), it);
练习9.11:对 6 种创建和初始化 vector 对象的方法,每一种都给出一个实例。解释每个 vector 包含什么值。
/* 练习9.11:对 6 种创建和初始化 vector 对象的方法,每一种都给出一个实例。解释每个 vector 包含什么值。*/
(1) vector<int> ilist1; // 默认初始化
(2) vector<int> ilist2(ilist); // ilist2 初始化为 ilist 的拷贝
vector<int> ilist2_1 = ilist; // 等价方式
(3) vector<int> ilist = {
1, 2, 3.0, 4, 5, 6, 7}; // ilist 初始化为列表中元素的拷贝
vector<int> ilist{
1, 2, 3.0, 4, 5, 6, 7}; // 等价方式
(4) vector<int> ilist(ilist.begin() + 2, ilist.end() - 1);
/* ilist3 初始化为两个迭代器指定范围中的元素的拷贝,范围中的元素类型必须与 ilist3 的元素类型相容,在本例中 ilist3 被初始化为 {3, 4, 5, 6}。
注意,由于只要求范围中元素类型鱼待初始化的容器的元素类型相容,因此,迭代器来自于不同类型的容器是可能的,例如,用一个 double 的 list 的范围来初始化 ilist3 是可行的。
另外,由于构造函数只是读取范围中的元素并进行拷贝,因此使用普通迭代器还是 const 迭代器来指出范围并无区别。
这种初始化方法特别适合于获取一个序列的子序列。*/
(5) vector<int> ilist4(7); // 默认值初始化
(6) vector<int> ilist5(7, 3); // 指定值初始化
练习9.13:如何从一个 list
初始化一个 vector
?从一个 vector
又该如何创建?编写代码验证你的答案。
/* 练习9.13:如何从一个 `list` 初始化一个 `vector`?
从一个 `vector` 又该如何创建?编写代码验证你的答案。
【出题思路】更深入地理解容器拷贝初始化和范围初始化两种方式的差异。
【解答】
由于 `list` 与 `vector` 是不同的容器类型,因此无法采用容器拷贝初始化方式。
但前者的元素类型是 `int`,与后者的元素类型 `double` 是相容的,因此可以采用范围初始化方式来构造一个 `vector`,令它的元素值与 `list`完全相同。*/
#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 dvec1(ivec);
/* 元素类型相容,因此可采用范围初始化 */
vector<double> dvec1(ivec.begin(), ivec.end());
cout << dvec.capacity() << " " << dvec.size() << " " << dvec[0] << " " << dvec[dvec.size() - 1] << endl;
cout << dvec1.capacity() << " " << dvec1.size() << " " << dvec1[0] << " " << dvec1[dvec1.size() - 1] << endl;
return 0;
}
/*Output:
7 7 1 7
7 7 7 1
*/
我们可以对一个容器进行列表初始化:
// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {
"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {
"a", "an", "the"};
顺序容器接受一个容器大小和一个(可选的)元素初始值。
如果我们不提供元素初始值,则标准库会创建一个值初始化器:
vector<int> ivec(10, -1); // 10 个 int 元素,每个都初始化为 -1
list<string> svec(10, "hi!"); // 10 个 strings; 每个都初始化为 "hi!"
forward_list<int> ivec(10); // 10 个元素,每个都初始化为 0
deque<string> svec(10); // 10 个元素,每个都是空 string
Note: 只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
array
具有固定大小当定义一个array
时,除了制定元素类型,还要指定容器大小:
array<int, 42> // 类型为:保存 42 个 int 的数组
array<string, 10> // 类型为:保存 10 个 string 的数组
为了使用array
类型,我们必须同时指定元素类型和大小,大小是array
类型的一部分:
array<int, 10>::size_type i; // 数组类型包括元素类型和大小
array<int>::size_type j; // 错误:array 不是一个类型
如果元素类型是一个类类型,那么该类必须有一个默认构造函数,以使值初始化能够进行:
array<int, 10> ia1; // 10 个默认初始化的 int
array<int, 10> ia2 = {
0,1,2,3,4,5,6,7,8,9}; // 列表初始化
array<int, 10> ia3 = {
42}; // ia3[0] 为 42,剩余元素为 0
值得注意的是,虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array
并无此限制:
int digs[10] = {
0,1,2,3,4,5,6,7,8,9};
int cpy[10] = digs; // 错误:内置数组不支持拷贝或赋值
array<int, 10> digits = {
0,1,2,3,4,5,6,7,8,9};
array<int, 10> copy = digits; // 正确:只要数组类型匹配即合法
与内置数组不同,标准库array
类型允许赋值。赋值号左右两边的运算对象必须具有相同的类型:
array<int, 10> a1 = {
0,1,2,3,4,5,6,7,8,9};
array<int, 10> a2 = {
0}; // 所有元素值均为 0
a1 = a2; // 替换 a1 中的元素
a2 = {
0}; // 错误:不能讲一个花括号列表赋予数组
由于右边运算对象的大小可能与左边运算对象的大小不同,因此array
类型不支持assign
,也不允许用花括号包围的值列表进行复制。
表9.4: | 容器赋值运算 |
---|---|
c1=c2 |
将c1 中的元素替换为c2 中元素的拷贝,c1 和c2 必须具有相同的类型 |
c={a,b,c...} |
将c1 中元素替换为初始化列表中元素的拷贝(array 不适用) |
swap(c1,c2) |
交换c1 和c2 中的元素,c1 和c2 必须具有相同的类型 |
c1.swap(c2) |
swap 通常比从c2 向c1 拷贝元素快得多 |
assign 操作不适用于关联容器和array |
|
seq.assign(b,e) |
将seq 中的元素替换为迭代器b 和e 所表示的范围中的元素,迭代器b 和e 不能指向seq 中的元素 |
seq.assign(il) |
将seq 中的元素替换为初始化列表il 中的元素 |
seq.assign(n, t) |
将seq 中的元素替换为n 个值为t 的元素 |
WARNING: 赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap
操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为array
和string
的情况除外)。
assign
(仅顺序容器)我们可以用assign
实现将一个vector
中的一段char*
值赋予一个list
中的string
:
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // 错误:容器类型不匹配
// 正确:可以将 const char* 转换为 string
names.assign(oldstyle.cbegin(), oldstyle.cend());
这段代码中对assign
的调用将names
中的元素替换为迭代器指定的范围中的元素的拷贝。assign
的参数决定了容器中将有多少个元素以及它们的值都是什么。
assign
的第二个版本,用指定书目且具有相同给定值的元素替换容器中原有的元素:
// 等价于 slist1.clear();
// 后跟 slist1.insert(slist1.begin(), 10, "Hiya!");
list<string> slist1(1); // 1 个元素,为空 string
slist1.assign(10, "Hiya!"); // 10 个元素,每个都是 "Hiya!"
练习9.14:编写程序,将一个list
中的char*
指针(指向C风格字符串)元素赋值给一个vector
中的string
。
/* 练习9.14:编写程序,将一个`list`中的`char*`指针(指向C风格字符串)元素赋值给一个`vector`中的`string`。
【出题思路】
容器有多种赋值操作,本题帮助读者理解不同赋值方式的差异。
【解答】
由于 list 与 vector 是不同类型的容器,因此无法采用赋值运算符 `=` 来进行元素赋值。
但 char* 可以转换为 string,因此可以采用范围赋值方式来实现本题要求。*/
#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;
return 0;
}
// Output: 3 3 hello !
swap
vector<string> svec1(10); // 10 个元素的 vector
vector<string> svec2(24); // 24 个元素的 vector
swap(svec1, svec2);
调用swap
后,svec1
将包含24
个string
元素,svec2
将包含10
个string
。除array
外,交换两个容器内容的操作保证会很快——元素本身并未交换 / 元素不会被移动,swap
只是交换了两个容器的内部数据结构。
Note: 除array
外,swap
不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。
元素不会被移动的事实意味着,除string
外,指向容器的迭代器、引用和指针在swap
操作之后都不会失效。
对一个string
调用swap
会导致迭代器、引用和指针失效。
swap
两个array
会真正交换它们的元素。因此,交换两个array
所需的时间与array
中元素的数目成正比。
因此,对于array
,在swap
操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素已经与另一个array
中对应元素的值进行了交换。
size
返回容器中元素的数目;empty
当size
为0
时返回布尔值true
,否则返回false
;max_size
返回一个大于或等于该类型容器所能容纳的最大元素数的值。forward_list
支持max_size
和empty
,但不支持size
。每个容器类型都支持相等运算符(==
和!=
);
除了无序关联容器外的所有容器都支持关系运算符(>
、>=
、<
、<=
)。
Note: 只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。
如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。
例如,我们在第7章中定义的Sales_data
类型并未定义==
和<
运算。因此,就不能比较两个保存Sales_data
元素的容器:
vector<Sales_data> storeA, storeB;
if (storeA < storeB) // 错误:Sales_data 没有 < 运算符
练习9.17:假定c1
和c2
是两个容器,下面的比较操作有何限制(如果有的话)?
if (c1 < c2)
【解答】
首先,容器类型必须相同,元素类型也必须相同。
其次,元素类型必须支持 <
运算符。
练习9.16:重写上一题的程序,比较一个 list
中的元素和一个vector
中的元素。
/* 练习9.16:重写上一题的程序,比较一个 list 中的元素和一个 vector 中的元素。*/
/* 练习9.15:编写程序,判定两个 vector 是否相等。
【解答】标准库容器支持关系运算符,比较两个 vector 是否相等使用 == 运算符即可。
当两个vector包含相同个数的元素,且对位元素都相等时,判定两个 vector 相等,否则不等。
两个 vector 的 capacity 不会影响相等性判定,因此,当下面程序中 ivec1 在添加、删除元素导致扩容后,仍然与 ivec 相等。*/
#include
#include
#include
using namespace std;
bool l_v_equal(vector<int> &ivec, list<int> &ilist) {
// 比较 list 和 vector 元素个数
if (ilist.size() != ivec.size())
return false;
auto lb = ilist.cbegin(); // list 首元素
auto le = ilist.cend(); // list 尾后位置
auto vb = ivec.cbegin(); // vector 首元素
for ( ; lb != le; lb++, vb++)
if (*lb != *vb) // 元素不等,容器不等
return false;
return true; // 容器相等
}
int main()
{
vector<int> ivec = {
1, 2, 3, 4, 5, 6, 7};
list<int> ilist = {
1, 2, 3, 4, 5, 6, 7};
list<int> ilist1 = {
1, 2, 3, 4, 5};
list<int> ilist2 = {
1, 2, 3, 4, 5, 6, 8};
list<int> ilist3 = {
1, 2, 3, 4, 5, 7, 6};
cout << l_v_equal(ivec, ilist) << endl;
cout << l_v_equal(ivec, ilist1) << endl;
cout << l_v_equal(ivec, ilist2) << endl;
cout << l_v_equal(ivec, ilist3) << endl;
return 0;
}
表9.5: | 向顺序容器添加元素的操作 |
---|---|
这些操作会改变容器的大小;array 不支持这些操作。 |
|
forward_list 有自己专有版本的insert 和emplace ; |
|
forward_list 不支持push_back 和emplace_back 。 |
|
vector 和string 不支持push_front 和emplace_front 。 |
|
c.push_back(t) c.emplace_back(args) |
在c 的尾部创建一个值为t 或由args 创建的元素,返回void |
c.push_front(t) c.emplace_front(args) |
在c 的头部创建一个值为t 或由args 创建的元素,返回void |
c.insert(p, t) c.emplace(p, args) |
在迭代器p 指向的元素之前创建一个值为t 或由args 创建的元素。返回指向新添加的元素的迭代器 |
c.insert(p, n, t) |
在迭代器p 指向的元素之前插入n 个值为t 的元素。返回指向新添加的第一个元素的迭代器;若n 为0 ,则返回p |
c.insert(p, b, e) |
将迭代器b 和e 指定的范围内的元素插入到迭代器p 指向的元素之前。b 和e 不能指向c 中的元素。返回指向新添加的第一个元素的迭代器;若范围为空,则返回p |
c.insert(p, il) |
il 是一个花括号包围的元素值列表。将这些给定值插入到迭代器p 指向的元素之前。返回指向新添加的第一个元素的迭代器;若列表为空,则返回p |
WARNING: 向一个vector
、string
或deque
插入元素会使所有指向容器的迭代器、引用和指针失效。
在一个vector
或string
的尾部之外的任何位置,或是一个deque
的首尾之外的任何位置添加元素,都需要移动元素。
而且,向一个vector
或string
添加元素可能引起整个对象存储空间的重新分配。
重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。
push_back
除array
和forward_list
之外,每个顺序容器(包括string
类型)都支持push_back
。
// 从标准输入读取数据,将每个单词放到容器末尾
string word;
while (cin >> word)
container.push_back(word);
container
的类型可以是list
、vector
或deque
。
由于string
是一个字符容器,我们也可以用push_back
在string
末尾添加字符:
void pluralize (size_t cnt, string &word)
{
if (cnt > 1)
word.push_back('s'); // 等价于 word += 's'
}
练习9.20:编写程序,从一个 list
拷贝元素到两个 deque
中。值为偶数的所有元素都拷贝到一个 deque
中,而奇数值元素都拷贝到另一个 deque
中。
/* 练习9.20:编写程序,从一个 `list` 拷贝元素到两个 `deque` 中。
值为偶数的所有元素都拷贝到一个 `deque` 中,而奇数值元素都拷贝到另一个 `deque` 中。
【出题思路】练习多个容器间数据的处理、拷贝。*/
#include
#include
#include
using namespace std;
int main()
{
list<int> ilist = {
1, 2, 3, 4, 5, 6, 7, 8}; // 初始化整数 list
deque<int> odd_d, even_d;
// 遍历整数 list
for (auto iter = ilist.cbegin(); iter != ilist.cend(); iter++)
if (*iter & 1) // 查看最低位,1:奇数, 0:偶数
odd_d.push_back(*iter);
else even_d.push_back(*iter);
cout << "The odd numbers are: ";
for (auto iter = odd_d.cbegin(); iter != odd_d.cend(); iter++)
cout << *iter << " ";
cout << endl;
cout << "The even numbers are: ";
for (auto iter = even_d.cbegin(); iter != even_d.cend(); iter++)
cout << *iter << " ";
cout << endl;
return 0;
}
/*Output:
The odd numbers are: 1 3 5 7
The even numbers are: 2 4 6 8
【其他解题思路】对于奇偶性判定,可用模2运算“%2”代替位与运算,两者是等价的。*/
关键概念:容器元素是拷贝:
当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。
就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。
随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。
push_front
list
、forward_list
和deque
还支持push_front
的类似操作,将元素插入到容器头部:
list<int> ilist;
// 将元素添加到 ilist 开头
for (size_t ix = 0; ix != 4; ++ix)
ilist.push_front(ix);
此循环将元素0、1、2、3添加到ilist
头部。在循环执行完毕后,ilist
保存序列3、2、1、0。
练习9.18:编写程序,从标准输入读取 string
序列,存入一个 deque
中。编写一个循环,用迭代器打印 deque
中的元素。
/* 练习9.18:编写程序,从标准输入读取 `string` 序列,存入一个 `deque` 中。编写一个循环,用迭代器打印 `deque` 中的元素。
【解答】对 `deque` 来说,在首尾位置添加新元素性能最佳,在中间位置插入新元素性能会很差。对遍历操作,可高效完成。*/
#include
#include
using namespace std;
int main()
{
deque<string> sd; // string 的 deque
string word;
while (cin >> word) // 读取字符串,直至遇到文件结束符
sd.push_back(word);
// 用 cbegin() 获取 deque 首元素迭代器,遍历 deque 中所有元素
for (auto si = sd.cbegin(); si != sd.cend(); si++)
cout << *si << endl;
return 0;
}
/*【其他解题思路】
由于在 `deque` 的首尾位置添加新元素性能很好,因此可以用 `push_front` 替换 `push_back`,性能不变,但元素在 `deque` 中的顺序将与输入顺序相反。
若需保持相同顺序,应使用 `push_back`。*/
vector
、deque
、list
和string
都支持insert
成员。
insert
函数将元素插入到迭代器所指定的位置之前:
slist.insert(iter, "Hello!"); // 将 "Hello!" 添加到 iter 之前的位置
// 将一个值为"Hello"的string插入到iter指向的元素之前的位置
虽然某些容器不支持push_front
,但它们对于insert
操作并无类似的限制(插入开始位置)。因此,我们可以将元素插入到容器的开始位置,而不必担心容器是否支持push_front
:
vector<string> svec;
list<string> slist;
// 等价于调用 slist.push_front("Hello!");
slist.insert(slist.begin(), "Hello!");
// vector 不支持 push_front,但我们可以插入到 begin() 之前
// 警告:插入到 vector 末尾之外的任何位置都可能很慢
svec.insert(svec.begin(), "Hello!");
WARNING: 将元素插入到vector
、deque
和string
中的任何位置都是合法的。然而,这样做可能很耗时。
接受一对迭代器或一个初始化列表的 insert 版本将给定范围中的元素插入到指定位置之前:
// 接受一对迭代器或一个初始化列表的 insert 版本将给定范围中的元素插入到指定位置之前:
vector<string> v = {
"quasi", "simba", "frollo", "scar"};
// 将 v 的最后两个元素添加到 slist 的开始位置
slist.insert(slist.begin(), v.end() - 2, v.end());
slist.insert(slist.end(), {
"these", "words", "will", "go", "at", "the", "end"});
如果我们传递给insert
一对迭代器,它们不能指向添加元素的目标容器:
// 运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end());
insert
的返回值通过使用insert
的返回值,可以再容器中一个特定位置反复插入元素:
list<string> lst;
auto iter = lst.begin();
while (cin >> word)
iter = lst.insert(iter, word); // 等价于调用 push_front
练习:假定iv
是一个int
的vector
,下面的程序存在什么错误?你将如何修改?
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);
【出题思路】
首先,理解容器插入操作的副作用 —— 向一个vector
、 string
或deque
插入元素会使现有指向容器的迭代器、引用和指针失效。
其次,练习如何利用insert
返回的迭代器,使得在向容器插入元素后,仍能正确在容器中进行遍历。
【解答】
循环中未对iter
进行递增操作,iter
无法向中点推进。其次,即使加入了iter++
语句,由于向iv
插入元素后,iter
己经失效,iter++
也不能起到将迭代器向前推进一个元素的作用。修改方法如下:
首先,将insert
返回的迭代器赋予iter
,这样,iter
将指向新插入的元素y
。我们知道,insert
将y
插入到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 + new_ele
都能正确指向 iv
原来的中央元素。
#include
#include
using namespace std;
int main()
{
vector<int> iv = {
1, 1, 2, 1}; // int 的 vector
int some_val = 1;
vector<int>::iterator iter = iv.begin();
int org_size = iv.size(), new_ele = 0; // 原大小和新元素个数
// 每个循环步都重新计算 "mid",保证正确指向 iv 原中央元素
while (iter != (iv.begin() + org_size / 2 + new_ele))
if (*iter == some_val) {
iter = iv.insert(iter, 2 * some_val);
new_ele++;
iter++; iter++; // 将 iter 推进到旧元素的下一个位置
} else iter++; // 简单推进 iter
// 用 begin() 获取 vector 首元素迭代器,遍历 vector 中的所有元素
for (iter iv.begin(); iter != iv.end(); iter++)
cout << *iter << endl;
return 0;
}
emplace
操作当我们调用emplace
成员函数时,是将参数传递给元素类型的构造函数。
emplace
成员使用这些参数在容器管理的内存空间中直接构造元素。
例如:假定c
保存Sales_data
元素:
// 在 c 的末尾构造一个 Sales_data 对象
// 使用三个参数的 Sales_data 构造函数
c.emplace_back("978-0590353403", 25, 15.99);
// 错误:没有接受三个参数的 push_back 版本
c.push_back("978-0590353403", 25, 15.99);
// 正确:创建一个临时的 Sales_data 对象传递给 push_back
c.push_back(Sales_data("978-0590353403", 25, 15.99));
在调用emplace_back
时,会在容器管理的内存空间中直接创建对象。而调用push_back
则会创建一个局部临时对象,并将其压入容器中。
emplace
函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配:
// iter 指向 c 中一个元素,其中保存了 Sales_data 元素
c.emplace_back(); // 使用 Sales_data 的默认构造函数
c.emplace(iter, "999-999999999"); // 使用 Sales_data(string)
// 使用 Sales_data 的接受一个 ISBN、一个 count 和一个 price 的构造函数
c.emplace_front("978-0590353403", 25, 15.99");
Note: emplace
函数在容器中直接构造元素。传递给emplace
函数的参数必须与元素类型的构造函数相匹配。
// 在解引用一个迭代器或调用 front 或 back 之前检查是否有元素
if (!c.empty()) {
// val 和 val2 是 c 中第一个元素值的拷贝
auto val = *c.begin(), val2 = c.front();
// val3 和 val4 是 c 中最后一个元素值的拷贝
auto last = c.end();
auto val3 = *(--last); // 不能递减 forward_list 迭代器
auto val4 = c.back(); // forward_list 不支持
}
此程序用两种方式来获取c
中的首元素和尾元素的引用。
直接的方法是调用front
和back
。
而间接的方法是通过解引用begin
返回的迭代器来获得首元素的引用,以及通过递减然后解引用end
返回的迭代器来获得尾元素的引用。
迭代器end
指向的是容器尾元素之后的(不存在)的元素。为了获取尾元素,必须首先递减此迭代器。
在调用front
和back
(或解引用begin
和end
返回的迭代器)之前,要确保c
非空。如果容器为空,if
中操作的行为将是未定义的。
表9.6: | 在顺序容器中访问元素的操作 |
---|---|
at 和下标操作只适用于string 、vector 、deque 和array 。 |
|
back 不适用于 forward_list 。 |
|
c.back() |
返回c 中尾元素的引用。若c 为空,函数行为未定义。 |
c.front() |
返回c 中首元素的引用。若c 为空,函数行为未定义。 |
c[n] |
返回c 中下标为n 的元素的引用,n 是一个无符号整数。若n >= s.size() ,则函数行为未定义 |
c.at(n) |
返回下标为n 的元素的引用。如果下标越界,则抛出 out_of_range 异常 |
if (!c.empty()) {
c.front() = 42; // 将 42 赋予 c 中的第一个元素
auto &v = c.back(); // 获得指向最后一个元素的引用
v = 1024; // 改变 c 中的元素
auto v2 = c.back(); // v2 不是一个引用,它是 c.back() 的一个拷贝
v2 = 0; // 未改变 c 中的元素
}
在容器中访问元素的成员函数(即,front
、back
、下标和at
)返回的都是引用。
如果我们使用auto
变量来保存这些函数的返回值,并且希望使用此变量来改变元素的值,必须记得将变量定义为引用类型。
如果希望确保下标是合法的,可以使用at
成员函数。at
成员函数类似下标运算符,但如果下标越界,at
会抛出一个out_of_range
异常:
vector<string> svec; // 空 vector
cout << svec[0]; // 运行时错误:svec 中没有元素!
cout << svec.at(0); // 抛出一个 out_of_range 异常
练习9.24:编写程序,分别使用at
、下标运算符
、front
和begin
提取一个 vector
中的第一个元素。在一个空vector
上测试你的程序。
【出题思路】
练习获取容器首元素的不同方法,以及如何安全访问容器元素。
【解答】
下面的程序会异常终止。因为vector
为空,此时用at
访问容器的第一个元素会抛出一个out_of_range
异常,而此程序未捕获异常,因此程序会因异常退出。正确的编程方式是,捕获可能的out of range
异常,进行相应的处理。
但对于后三种获取容器首元素的方法,当容器为空时,不会抛出out_of_range
异常,而是导致程序直接退出(注释掉前几条语句即可看到后面语句的执行效果)。
因此,正确的编程方式是,在采用这几种获取容器的方法时,检查下标的合法性(对front
和begin
只需检查容器是否为空),确定没有问题后再获取元素。当然这种方法对at
也适用。
#include
#include
using namespace std;
int main()
{
vector<int> iv;
cout << iv.at(0) << endl;
cout << iv[0] << endl;
cout << iv.front() << endl;
cout << *(iv.begin()) << endl;
return 0;
}
/* 报错:
terminate called after throwing an instance of 'std::out_of_range'
what(): vector::_M_range_check: __n (which is 0) >= this->size() (which is 0)
*/
表9.7: | 顺序容器的删除操作 |
---|---|
这些操作会改变容器的大小,所以不适用于array |
|
forward_list 有特殊版本的erase |
|
forward_list 不支持pop_back ;vector 和string 不支持pop_front 。 |
|
c.pop_back() |
删除c 中尾元素。若c 为空,则函数行为未定义。函数返回void |
c.pop_front() |
删除c 中首元素。若c 为空,则函数行为未定义。函数返回void |
c.erase(p) |
删除迭代器p 所指定的元素,返回一个指向被删元素之后元素的迭代器,若p 指向尾元素,则返回尾后(off-the-end )迭代器。若p 是尾后迭代器,则函数行为未定义。 |
c.erase(b, e) |
删除迭代器b 和e 所指定范围内的元素。返回一个指向最后一个被删元素之后元素的迭代器,若e 本身就是尾后迭代器,则函数也返回尾后迭代器 |
c.clear() |
删除c 中的所有元素。返回void |
WARNING: 删除deque
中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector
或string
中删除点之后位置的迭代器、引用和指针都会失效。
pop_front
和pop_back
成员函数与vector
和string
不支持push_front
一样,这些类型也不支持pop_front
。
类似的,forward_list
不支持pop_back
。
成员函数erase
从容器中指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase
都返回指向删除的(最后一个)元素之后位置的迭代器。
// 下面的循环删除一个 list 中的所有奇数元素:
list<int> lst = {
0,1,2,3,4,5,6,7,8,9};
auto it = lst.begin();
while (it != lst.end())
if (*it % 2) // 若元素为奇数
it = lst.erase(it); // 删除此元素
else
++it;
每个循环步中,首先检查当前元素是否是奇数。
如果是,就删除该元素,并将it
设置为我们所删除的元素之后的元素。
如果*it
为偶数,我们将it
递增,从而在下一步循环检查下一个元素。
接受一对迭代器的erase
版本允许我们删除一个范围内的元素:
// 删除两个迭代器表示的范国内的元素
// 返回指向最后一个被删元素之后位置的迭代器
elem1 = slist.erase(elem1, elem2); // 调用后,elem1 == elem2
迭代器elem1
指向我们要删除的第一个元素,elem2
指向我们要删除的最后一个元素之后的位置。
练习25:对于第312页中删除一个范围内的元素的程序,如果elem1
与elem2
相等会发生什么,如果elem2
是尾后迭代器,或者elem1
和elem2
皆为尾后迭代器,又会发生什么?
【出题思路】
理解范围删除操作的两个迭代器参数如何决定删除操作的结果。
【解答】如果两个迭代器eleml
和elem2
相等,则什么也不会发生,容器保持不变.哪怕两个迭代器是指向尾后位置(例如end()+1
),也是如此,程序也不会出错。
因此elem1
和elem2
都是尾后迭代器时,容器保持不变。
如果elem2
为尾后迭代器,elem1
指向之前的合法位置,则会删除从elem1
开始直至容器末尾的所有元素。
为了删除一个容器中的所有元素,我们既可以调用clear
,也可以用begin
和end
获得的迭代器作为参数调用erase
:
slist.clear(); // 删除容器中所有元素
slist.erase(slist.begin(), slist.end()); // 等价调用
练习9.26:使用下面代码定义的 ia
,将 ia
拷贝到一个 vector
和一个 list
中。使用单迭代器版本的 erase
从 list
中删除奇数元素,从 vector
中删除偶数元素。
/* 练习9.26:使用下面代码定义的 ia,将 ia 拷贝到一个 vector 和一个 list 中。使用单迭代器版本的 erase 从 list 中删除奇数元素,从 vector 中删除偶数元素。
int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 }; */
/*【解答】
当从 vector 中删除元素时,会导致删除点之后位置的迭代器、引用和指针失效。
而 erase 返回的迭代器指向删除元素之后的位置。因此,将 erase 返回的迭器赋予 iiv,使其正确向前推进。且尾后位置每个循环步中都用 end 重新获得,保证其有效。
对于 list 删除操作并不会令迭代器失效,但上述方法仍然是适用的. */
#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); // 将数据拷贝到 vector
il.assign(ia, ia + 11); // 将数据拷贝到 list
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;
return 0;
}
/*Output:
1 1 3 5 13 21 55 89
0 2 8
*/
forward_list
操作表9.8: | 在forward_list 中插入或删除元素的操作 |
---|---|
lst.before_begin() |
返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用。 |
lst.cbefore_begin() |
cbefore_begin() 返回一个const_iterator |
lst.insert_after(p,t) |
在迭代器p 之后的位置插入元素。t 是一个对象。 |
lst.insert_after(p,n,t) |
t 是一个对象,n 是数量 |
lst.insert_after(p,b,e) |
b 和e 是表示范围的一对迭代器(b 和e 不能指向lst 内) |
lst.insert_after(p,il) |
il 是一个花括号列表。返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回p 。若p 为尾后迭代器,则函数行为未定义 |
emplace_after(p,args) |
使用args 在p 指定的位置之后创建一个元素。返回一个指向这个新元素的迭代器。若p 为尾后迭代器,则函数行为未定义 |
lst.erase_after(p) |
删除p 指向的位置之后的元素 |
lst.erase_after(b,e) |
或删除从b 之后直到(但不包含)e 之间的元素。返回一个指向被删元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代器。如果p 指向lst 的尾元素或者是一个尾后迭代器,则函数行为未定义 |
改写 p312 中从list
中删除奇数元素的循环程序,将其改为从forward_list
中删除元素:
forward_list<int> flst = {
0,1,2,3,4,5,6,7,8,9};
auto prev = flst.before_begin(); // 表示 flst 的“前首元素”
auto curr = flst.begin(); // 表示 flst 中的第一个元素
while (curr != flst.end()) {
// 仍有元素要处理
if (*curr % 2) // 若元素为奇数
curr = flst.erase_after(prev); // 删除它并移动 curr
else {
prev = curr; // 移动迭代器 curr,指向下一个元素,prev
++curr;
}
}
分析:当找到奇数元素后,我们将prev
传递给erase_after
。此调用将prev
之后的元素删除,即,删除curr
指向的元素。然后我们将curr
重置为erase_after
的返回值,使得curr
指向序列中下一个元素,prev
保持不变,仍指向(新)curr
之前的元素。如果curr
指向的元素不是奇数,在else
中我们将两个迭代器都向前移动。
练习9.27:编写程序,查找并删除 forward_list
中的奇数元素。
/* 练习9.27:编写程序,查找并删除 forward_list 中的奇数元素。*/
#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;
return 0;
}
// Output: 2 4 6 8
练习9.28:编写函数,接受一个 forward list
和两个 string
共三个参数。函数应在链表中查找第一个 string
,并将第二个string
插入到紧接着第一个 string
之后的位置。若第一个 string
未在链表中,则将第二个 string
插入到链表末尾。
/* 练习9.28:编写函数,接受一个 `forward list` 和两个 `string` 共三个参数。
函数应在链表中查找第一个 `string`,并将第二个`string` 插入到紧接着第一个 `string` 之后的位置。
若第一个 `string` 未在链表中,则将第二个 `string` 插入到链表末尾。 */
#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); // 插入新字符串,curr 指向它
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", "nihao");
for (auto curr = sflst.cbegin(); curr != sflst.cend(); curr++)
cout << *curr << " ";
cout << endl; // Hello nihao ! world !
test_and_insert(sflst, "!", "?");
for (auto curr = sflst.cbegin(); curr != sflst.cend(); curr++)
cout << *curr << " ";
cout << endl; // Hello ! ? world ! ?
test_and_insert(sflst, "Bye", "Zaijian");
for (auto curr = sflst.cbegin(); curr != sflst.cend(); curr++)
cout << *curr << " ";
cout << endl; // Hello ! world ! Zaijian
return 0;
}
用resize
来增大或缩小容器
list<int> ilist(10, 42); // 10 个 int:每个的值都是42
ilist.resize(15); // 将 5 个值为 0 的元素添加到 ilist 的末尾
ilist.resize(25, -1); // 将 10个 值为 -1 的元素添加列 ilist 的末尾
ilist.resize(5); // 从 ilist 末尾删除 20 个元素
表9.9: | 顺序容器大小操作 |
---|---|
resize 不适用于array |
|
c.resize(n) |
调整c 的大小为n 个元素。若 n |
c.resize(n,t) |
调整c 的大小为n 个元素。任何新添加的元素都初始化为值t |
WARNNG: 如果resize
缩小容器,则指向被删除元素的迭代器、引用和指针都会失效:对vector
、string
或deque
进行resize
可能导致迭代器、指针 和引用失效。
练习9.30:接受单个参数的resize
版本对元素类型有什么限制(如果有的话)?
【出题思路】 更深入理解改变容器大小的操作。
【解答】对于元素是类类型,则单参数resize
版本要求该类型必须提供一个默认构造函数。
在向容器添加元素后:
如果容器是vector
或string
,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。
对于deque
,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
对于list
和forward_list
,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。
当我们删除一个元素后:
对于list
和forward_list
,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效。
对于deque
,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代群、引用或指针也会失效。如果是删除deque
的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。
对于vector
和string
,指向被删元素之前元素的迭代器、引用和指针仍有效。 注意:当我们删除元素时,尾后迭代器总是会失效。
建议:管理迭代器:
必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对vector
、string
和deque
尤为重要。
程序比须保证每个循环步中都更新迭代器、引用和指针。如果循环中调用的是insert
或erase
,那么更新迭代器很容易。这些操作都返回迭代器,我们可以用来更新:
// 此程序删除 vector 中的偶数值元素,并复制每个奇数值元素
// 我们在调用 insert 和 erase 后都更新迭代器,因为两者都会使迭代器失效
vector<int> vi = {
0,1,2,3,4,5,6,7,8,9};
auto iter = vi.begin();
while (iter != vi.end()) {
if (*iter % 2) {
iter = vi.insert(iter, *iter); // 复制当前元素
iter += 2; // 当前移动迭代器,跳过当前元素以及插入到它之前的元素
} else
iter = vi.erase(iter); // 删除偶数元素
// 不应向前移动迭代器,iter 指向我们删除的元素之后的元素
}
在调用erase
后,不必递增迭代器,因为erase
返回的迭代器已经指向序列中下一个元素。
调用insert
后,需要递增迭代器两次。记住,insert
在给定位置之前插入新元素,然后返回指向新插入元素的迭代器。因此,在调用insert
后,iter
指向新插入元素,位于我们正在处理的元素之前。我们将迭代器递增两次,恰好越过了新添加的元素和正在处理的元素,指向下一个未处理的元素。
练习9.32:在第316页的程序中,像下面语句这样调用insert
是否合法?如果不合法,为什么?
iter = vi.insert(iter, *iter++);
【出题思路】
本题复习实参与形参的关系,进一步熟悉迭代器的处理对容器操作的关键作用。
在一条语句中混用解引用和递增运算符
【解答】 很多编译器对实参求值、向形参传递的处理顺序是由右至左的。
这意味着,编译器在编译上述代码时,首先对*iter++*
求值,传递给insert
第二个形参,此时iter
已指向当前奇数的下一个元素,因此传递给insert
的第一个参数的迭代器指向的是错误位置,程序执行会发生混乱,最终崩溃。
练习9.31:第316页中删除偶数值元素并复制奇数值元素的程序不能用于list
或forward_list
。为什么,修改程序,使之也能用于这些类型。
【出题思路】
本题继续练习list
和forward_list
的插入、删除操作,理解与其他容器的 不同,理解对迭代器的影响。
【解答】list
和forward_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;
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; // prev 移动到新插入元素
curr++; // curr 移动到下一元素
} else // 偶数
curr = iflst.erase_after(prev); // 删除,curr 指向下一元素
}
for (curr = iflst.begin(); curr != iflst.end(); curr++)
cout << *curr << " ";
cout << endl;
return 0;
}
end
返回的迭代器保存尾迭代器的值是一个坏主意,不能在循环之前保存end
返回的迭代器一直当做容器末尾使用。
Tip: 如果在一个循环体中插入/删除 deque
、string
或 vector
中的元素,不要缓存 end
返回的迭代器。
// 更安全的方法:在每个循环步添加/删除元素后都重新计算 end
while (begin != v.end()) {
// 做一些处理
++begin; // 向前移动 begin,因为我们想在此元素之后插入元素
begin = v.insert(begin, 42); // 插入新值
++begin; // 向前移动 begin,跳过我们刚刚加入的元素
}
假定容器中元素是连续存储的,且容器的大小是可变的,考虑向vector
或string
中添加元素会发生什么:
如果没有空间容纳新元素,容器不可能简单地将它添加到内存中其他位置——因为元素必须连续存储。容器必须分配新的内存空间来保存己有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放旧存储空间。如果我们每添加一个新元素,vector
就执行一次这样的内存分配和释放操作,性能会慢到不可接受。
为了避免这种代价,标准库实现者采用了可以减少容器空间重新分配次数的策略。当不得不获取新的内存空间时,vector
和string
的实现通常会分配比新的空间需求更大的内存空问。容器预留这些空间作为备用,可用来保存更多的新元素。
这样,就不需要每次添加新元素都重新分配容器的内存空间了。这种分配策略比每次添加新元素时都重新分配容器内存空间的策略要高效得多。其实际性能也表现得足够好——虽然vector
在每次重新分配内存空间时都要移动所有元素,但使用此策略后,其扩张操作通常比list
和deque
还要快。
表9.10: | 容器大小管理操作 |
---|---|
shrink_to_fit 只适用于vector 、string 和deque 。 |
|
capacity 和reserve 只适用于vector 和string 。 |
|
c.shrink_to_fit() |
请将capacity() 减少为与size() 相同大小 |
c.capacity() |
不重新分配内存空间的话,c 可以保存多少元素 |
c.reserve(n) |
分配至少能容纳n 个元素的内存空间 |
Note: reserve
并不改变容器中元素的数量,它仅影响vector
预先分配多大的内存空间。
在新标准库中,我们可以调用shrink_to_fit
来要求deque
、vector
或string
退回不需要的内存空间。此函数指出我们不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求。也就是说,调用shrink_to_fit
也并不保证一定退回内存空间。
capacity
和size
练习9.39:解释下面程序片段做了什么:
vector<string> svec;
svec.reserve(1024);
string word;
while (cin >> word)
svec.push_back(word);
svec.resize(svec.size() + svec.size()/2);
【解答】
首先,reserve
为svec
分配了1024
个元素(字符串)的空间。
随后,循环会不断读入字符串,添加到svec
末尾,直至遇到文件结束符。这个过程中,如果读入的字符串数量不多于1024
,则svec
的容量(capacity
)保持不变,不会分配新的内存空间。
否则,会按一定规则分配更大的内存空间,并进行字符串的移动。
接下来,resize
将向svec
末尾添加当前字符串数量一半那么多的新字符串, 它们的值都是空串。若空间不够,会分配足够容纳这些新字符串的内存空间。
*练习9.35:解释一个vector
的capacity
和size
有何区别。
【解答】
理解capacity
和size
的区别非常重要。
容器的size
是指它己经保存的元素的数目;而capacity
则是在不分配新的内存空间的前提下它最多可以保存多少元素。
练习9.37:为什么list
或array
没有capacity
成员函数?
【出题思路】
理解list
和array
与vector
在数据结构上的差异导致内存分配方式的不同。
【解答】
list
是链表,当有新元素加入时,会从内存空间中分配一个新节点保存它;当从链表中删除元素时,该节点占用的内存空间会被立刻释放。
因此,一个链表占用的内存空间总是与它当前保存的元素所需空间相等(换句话说,capacity
总是等于size
)。
而array
是固定大小数组,内存一次性分配,大小不变,不会变化。
因此它们均不需要capacity
。
未完待续!!!
string
类和C风格字符数组之间的相互转换substr
操作append
和replace
函数string
的多种重载函数除了顺序容器外,标准库还定义了三个顺序容器适配器:
stack
、queue
和priority_queue
本质上,一个适配器是种机制,能使某种事物的行为看起来像另外种事物一样。一个容器适配器接受一种己有的容器类型,使其行为看起来像一种不同的类型。
例如:stack
适配器接受一个顺序容器(除array
或forward_list
外),并使其操作起来像一个stack
一样。
表9.17: | 所有容器适配器都支持的操作和类型 |
---|---|
size_type |
一种类型,足以保存当前类型的最大对象的大小 |
value_type |
元素类型 |
container_type |
实现适配器的底层容器类型 |
A a; |
创建一个名为a 的空适配器 |
A a(c); |
创建一个名为a 的适配器,带有容器c 的一个拷贝 |
关系运算符 | 每个适配器都支持所有关系运算符:== 、!= 、< 、<= 、> 和>= ,这些运算符返回底层容器的比较结果 |
a.empty() |
若a 包含任何元素,返回false ,否则返回true |
a.size() |
返回a 中的元素数目 |
swap(a,b) |
交换a 和b 的内容,a 和b 必须有相同类型,包括底层容器类型也必须相同。 |
a.swap(b) |
每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构 造的数拷贝该容器来初始化适配器。
例如,假定deq
是一个deque
,我们可以用deq
来初始化一个新的stack
,如下所示:
stack<int> stk(deq); // 从 deq 拷贝元素到 stk
默认情况下,stack
和queue
是基于deque
实现的,
priority_queue
是在vector
之上实现的。
我们可以再创建一个适配器时,讲一个命名的顺序容器作为第二类型参数,来重载默认容器类型。
// 在 vector 上实现的空栈
stack<string, vector<string>> str_stk;
// str_stk2 在 vector 上实现,初始化时保存 svec 的拷贝
stack<string, vector<string>> str_stk2(svec);
所有适配器都要求容器其有添加和删除元素的能力。因此,适配器不能构造在array
之上。类似的,我们也不能用forward_list
来构造适配器,因为所有适配器都要求容器具有添加、删除以及访问尾元素的能力。
stack
只要求push_back
、pop_back
和back
操作,因此可以使用除array
和forward_list
之外的任何容器类型来构造stack
queue
适配器要求back
、push_back
、front
和push_front
,因此,它可以构造于list
或deque
之上,但不能基于vector
构造。
priority_queue
除了front
, push_back
和pop_back
操作之外还要求随机访问能力,因此它可以构造于vector
或deque
之上,但不能基于list
构造。
stack<int> intStack; // 空栈
// 填满栈
for (size_t ix = 0; ix != 10; ++ix)
intStack.push(ix); // intStack 保存 0 到 9 十个数
while (!intStack.empty()) // intStack 中有值就继续循环
int value = intStack.top() // 使用栈顶值的代码
intStack.pop(); // 弹出栈顶元素,继续循环
表9.18: | 表9.17 未列出的栈操作 |
---|---|
栈默认基于deque 实现,也可以在list 或vector 之上实现 |
|
s.pop() |
删除栈顶元素,但不返回该元素值 |
s.push(item) |
创建一个新元素压入栈顶,该元素通过拷贝或移动item 而来 |
s.emplace(args) |
或者由args 构造 |
s.top() |
返回栈顶元素,但不能将元素弹出栈 |
每个容器适配器都基于底层容器类型的操作定义了自己的特殊操作。
我们只可以使用适配器操作,而不能使用底层容器类型的操作。例如,
intStack.push(ix); // intStack 保存 0 到 9 十个数
此语句试图在intStack
的底层deque
对象上调用push_back
。
虽然stack
是基于deque
实现的.但我们不能直接使用deque
操作。
不能在一个stack
上调用push_back
, 而必须使用stack
自己的操作——push
。
表9.19: | 表9.17未列出的queue 和priority_queue 操作 |
---|---|
queue 默认基于deque 实现,priority_queue 默认基于vector 实现 |
|
queue 也可以用list 或vector 实现,priority_queue 也可以用deque 实现 |
|
q.pop() |
返回queue 的首元素或priority_queue 的最高优先级的元素,但不删除此元素 |
q.front() |
返回首元素或尾元素,但不删除此元素 |
q.back() |
只适用于queue |
q.top() |
返回最高优先级元素,但不删除该元素(只适用于priority_queue ) |
q.push(item) |
在queue 末尾或priority_queue 中恰当的位置创建一个元素,其值为item , |
q.emplace(args) |
或者由args 构造 |
(注:在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出(first in, largest out)的行为特征。)
标准库queue
使用一种先进先出(first-in, first-out, FIFO)的存储和访问策略。
priority_queue
允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。
饭店按照客人预定时间而不是到来时间的早晚来为他们安排座位,就是一个优先队列的例子。默认清况下,标准库在元素类型上使用<
运算符来确定相对优先级。