第9章 顺序容器

  • 第9章 顺序容器 (sequential container)
    • 9.1 顺序容器概述
      • 确定使用哪种顺序容器
    • 9.2 容器库概览
      • 对容器可以保存的元素类型的限制
        • 表9.2:容器操作
      • 9.2.1 迭代器
        • 迭代器范围
        • 使用左闭合范围蕴含的变成假定
      • 9.2.2 容器类型成员
      • 9.2.3 beginend成员
      • 9.2.4 容器定义和初始化
        • 将一个容器初始化为另一个容器的拷贝
        • 列表初始化
        • 与顺序容器大小相关的构造函数
        • 标准库array具有固定大小
      • 9.2.5 赋值和swap
        • 使用 assign(仅顺序容器)
        • 使用 swap
      • 9.2.6 容器大小操作
      • 9.2.7 关系运算符
        • 容器的关系运算符使用元素的关系运算符完成比较
    • 9.3 顺序容器操作
      • 9.3.1 向顺序容器添加元素
        • 使用push_back
        • 使用push_front
        • 在容器中的特定位置添加元素
        • 插入范围内元素
        • 使用insert的返回值
        • 使用emplace操作
      • 9.3.2 访问元素
        • 访问成员函数返回的是引用
        • 下标操作和安全的随机访问
      • 9.3.3 删除元素
        • pop_frontpop_back成员函数
        • 从容器内部删除一个元素
        • 删除多个元素
      • 9.3.4 特殊的forward_list操作
      • 9.3.5 改变容器大小
      • 9.3.6 容器操作可能使迭代器失效
        • 编写改变容器的循环程序
        • 不要保存end返回的迭代器
    • 9.4 vector对象是如何增长的
      • 管理容量的成员函数
      • capacitysize
    • 9.5 额外的string操作
      • 9.5.1 构造string的其他方法
        • substr操作
      • 9.5.2 改变string的其他方法
        • appendreplace函数
        • 改变string的多种重载函数
      • 9.5.3 string搜索操作
        • 指定在哪里开始搜索
        • 逆向搜素
      • 9.5.4 compare函数
      • 9.5.5 数值转换
    • 9.6 容器适配器 (adapter)
      • 定义一个适配器
      • 栈适配器
      • 队列适配器

第9章 顺序容器 (sequential container)

顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。
关联容器中元素的位置由元素相关联的关键字值决定。

9.1 顺序容器概述

表9.1: 顺序容器类型
vector 可变大小数组,支持快速随机访问,在尾部之外的位置插入或删除元素可能很慢
deque 双端队列,支持快速随机访问,在头尾位置插入 / 删除速度很快
list 双向链表,只支持双向顺序访问,在list中任何位置进行插入 / 删除操作速度都很快
forward_list 单向链表,只支持单向顺序访问,在链表任何位置进行插入 / 删除操作速度都很快
array 固定大小数组,支持快速随机访问,不能添加或删除元素
string vector相似的容器,但专门用于保存字符,随机访问快,在尾部插入 / 删除速度快

stringvector将元素保存在连续的内存空间中。由于元素是连续存储的,由元素的下标来计算其地址是非常快速的。

但是,在这两种容器的中间位置添加或删除元素就会非常耗时:在一次插入或删除操作后,需要移动插入 / 删除位置之后的所有元素,来保持连续存储。而且添加一个元素有时还需要分配额外的存储空间。在这种情况下,每个元素都必须移动到新的存储空间中。

listforward_list 两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。

作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。而且,与vectordequearray相比,这两个容器的额外内存开销也很大。

deque 是一个更为复杂的数据结构,与stringvector类似,deque支持快速的随机访问。与stringvector一样,在deque的中间位置添加或删除元素的代价(可能)很高。但是,在deque的两段添加或删除元素都是很快的,与listforward_list添加删除元素的速度相当。

array对象的大小是固定的,因此,array不支持添加和删除元素以及改变容器大小的操作。

forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能,因此forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。

确定使用哪种顺序容器

Tip: 通常,使用vector是最好的选择,除非你有很好的理由选择其他容器。

  • 除非你有很好的理由选择其他容器,否则应使用vector
  • 如果你的程序又很多小的元素,且空间的额外开销很重要,则不要使用listforward_list
  • 如果程序要求随机访问元素,应使用vectordeque
  • 如果程序要求在容器的中间插入或删除元素,应使用listforward_list
  • 如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque
  • 如果程序只有在读取输入时才需要在容器中检位置插入元素,随后需要随机访问元素,则
    • 首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地向vector追加数据,然后再调用标准库的sort函数来重排容器中的元素,从而避免在中间位置添加元素。
    • 如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中。

注意:如果不确定应该使用哪种容器,那么可以再程序中只使用vectorlist公共的操作:使用迭代器,不使用下标操作,避免随机访问。这样,在必要时选择使用vectorlist都很方便。

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

(a)读取固定数量的单词,将它们按字典序插入到容器中。我们将在下一章中看到, 关联容器更适合这个问题。

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

(b)读取未知数量的单词,总是将新单词插入到末尾。删除操作在头部进行。

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

©从一个文件读取未知数量的整数。将这些数排序,然后将它们打印到标准输出。

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

9.2 容器库概览

每个容器都定义在一个头文件中,文件名与类型名相同。
即,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);       // 错误:必须提供一个元素初始化器

表9.2:容器操作

类型别名
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,将迭代器 be 指定的范围内的元素拷贝到 carray不支持)
C c{a, b, c...}; 列表初始化 c
赋值与swap
c1 = c2 c1 中的元素替换为 c2 中元素
c1 = {a, b, c...} c1 中的元素替换为列表中元素(不适用于array
a.swap(b) 交换 ab 的元素
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对象,其元素类型是 intdeque

list a;

9.2.1 迭代器

迭代器范围

迭代器范围(iterator range):beginend(one past the last element),它们标记了容器中元素的一个范围。

这种元素范围被称为左闭合区间(left-inclusive interval),其标准数学描述为 [begin, end)end可以与begin指向相同的位置。

使用左闭合范围蕴含的变成假定

假定beginend构成一个合法的迭代器范围,则

  • 如果beginend相等,则范围为空
  • 如果beginend不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
  • 我们可以对begin递增若干次,使得begin==end
while (begin != end)
    *begin = val;   // 正确:范围非空,因此 begin 指向一个元素
    ++begin;        // 移动迭代器,获取下一个元素

练习9.3:构成迭代器范围的迭代器有何限制?

两个迭代器beginend必须指向同一个容器中的元素,或者是容器最后一个元素之后的位置;
而且,对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) /* ...*/

【出题思路】
理解不同类型容器的迭代器之间的差别,更深层次的,理解数据结构的实现如何导致迭代器的差别。

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

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

9.2.2 容器类型成员

每个容器都定义了多个类型,如 size_typeiteratorconst_iterator

除了已经使用过的迭代器类型,大多数容器还提供反向迭代器。反向迭代器就是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义也都发生了颠倒。例如,对一个反向迭代器执行++操作,会得到上一个元素。

类型别名,我们可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type。如果需要元素类型的一个引用,可以使用referenceconst_reference

// 为了使用这些类型,我们必须显式使用其类名:
// iter 是通过 list 定义的一个迭代器类型
list<string>::iterator iter;
// count 是通过 vector 定义的一个 difference_type 类型
vector<int>::difference_type count;

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

【出题思路】标准库容器定义了若干类型成员,对应容器使用中可能涉及的类型,如迭代器、元素引用等。

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

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

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

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

9.2.3 beginend成员

beginenc有多个版本:带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类型。rbeginenrend的情况类似。当我们对一个非常量对象调用这些成员时,得到的是返回 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:begincbegin两个函数有什么不同?

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

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

9.2.4 容器定义和初始化

表9.3: 容器定义和初始化
C c 默认构造函数,如果C是一个array,则c中元素按默认方式初始化,否则c为空
C c1(c2)
C c1=c2
c1初始化为c2的拷贝,c1c2必须是相同类型(即,它们必须是相同的容器类型,且保存的是相同的元素类型;对于array类型,两者还必须具有相同大小)
C c{a, b, c...}
C c={a, b, c...}
c初始化为初始化列表中元素的拷贝,列表中元素的类型必须与c的元素类型相容。
对于array类型,列表中元素数目必须等于或小于array的大小,任何遗漏的元素都进行值初始化
C c(b, e) c初始化为迭代器be指定范围中的元素的拷贝。范围中元素的类型必须与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;   // 正确:只要数组类型匹配即合法

9.2.5 赋值和swap

与内置数组不同,标准库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中元素的拷贝,c1c2必须具有相同的类型
c={a,b,c...} c1中元素替换为初始化列表中元素的拷贝(array不适用)
swap(c1,c2) 交换c1c2中的元素,c1c2必须具有相同的类型
c1.swap(c2) swap通常比从c2c1拷贝元素快得多
assign操作不适用于关联容器和array
seq.assign(b,e) seq中的元素替换为迭代器be所表示的范围中的元素,迭代器be不能指向seq中的元素
seq.assign(il) seq中的元素替换为初始化列表il中的元素
seq.assign(n, t) seq中的元素替换为n个值为t的元素

WARNING: 赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为arraystring的情况除外)。

使用 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将包含24string元素,svec2将包含10string。除array外,交换两个容器内容的操作保证会很快——元素本身并未交换 / 元素不会被移动,swap只是交换了两个容器的内部数据结构。

Note: 除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。

元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。

对一个string调用swap会导致迭代器、引用和指针失效。

swap两个array会真正交换它们的元素。因此,交换两个array所需的时间与array中元素的数目成正比。

因此,对于array,在swap操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素已经与另一个array中对应元素的值进行了交换。

9.2.6 容器大小操作

  • 成员函数size返回容器中元素的数目;
  • emptysize0时返回布尔值true,否则返回false
  • max_size返回一个大于或等于该类型容器所能容纳的最大元素数的值。
  • forward_list支持max_sizeempty,但不支持size

9.2.7 关系运算符

每个容器类型都支持相等运算符(==!=);
除了无序关联容器外的所有容器都支持关系运算符(>>=<<=)。

容器的关系运算符使用元素的关系运算符完成比较

Note: 只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。

如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。
例如,我们在第7章中定义的Sales_data类型并未定义==<运算。因此,就不能比较两个保存Sales_data元素的容器:

vector<Sales_data> storeA, storeB;
if (storeA < storeB)    // 错误:Sales_data 没有 < 运算符

练习9.17:假定c1c2是两个容器,下面的比较操作有何限制(如果有的话)?
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.3 顺序容器操作

9.3.1 向顺序容器添加元素

表9.5: 向顺序容器添加元素的操作
这些操作会改变容器的大小;array不支持这些操作。
forward_list有自己专有版本的insertemplace
forward_list不支持push_backemplace_back
vectorstring不支持push_frontemplace_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的元素。返回指向新添加的第一个元素的迭代器;若n0,则返回p
c.insert(p, b, e) 将迭代器be指定的范围内的元素插入到迭代器p指向的元素之前。be不能指向c中的元素。返回指向新添加的第一个元素的迭代器;若范围为空,则返回p
c.insert(p, il) il是一个花括号包围的元素值列表。将这些给定值插入到迭代器p指向的元素之前。返回指向新添加的第一个元素的迭代器;若列表为空,则返回p

WARNING: 向一个vectorstringdeque插入元素会使所有指向容器的迭代器、引用和指针失效。

在一个vectorstring的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。
而且,向一个vectorstring添加元素可能引起整个对象存储空间的重新分配。
重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。

使用push_back

arrayforward_list之外,每个顺序容器(包括string类型)都支持push_back

// 从标准输入读取数据,将每个单词放到容器末尾
string word;
while (cin >> word)
    container.push_back(word);

container的类型可以是listvectordeque

由于string是一个字符容器,我们也可以用push_backstring末尾添加字符:

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

listforward_listdeque还支持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`。*/

在容器中的特定位置添加元素

vectordequeliststring都支持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: 将元素插入到vectordequestring中的任何位置都是合法的。然而,这样做可能很耗时。

插入范围内元素

接受一对迭代器或一个初始化列表的 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是一个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);

【出题思路】
首先,理解容器插入操作的副作用 —— 向一个vectorstringdeque插入元素会使现有指向容器的迭代器、引用和指针失效。
其次,练习如何利用insert返回的迭代器,使得在向容器插入元素后,仍能正确在容器中进行遍历。

【解答】
循环中未对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 + 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函数的参数必须与元素类型的构造函数相匹配。

9.3.2 访问元素

// 在解引用一个迭代器或调用 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中的首元素和尾元素的引用。

直接的方法是调用frontback
而间接的方法是通过解引用begin返回的迭代器来获得首元素的引用,以及通过递减然后解引用end返回的迭代器来获得尾元素的引用。

迭代器end指向的是容器尾元素之后的(不存在)的元素。为了获取尾元素,必须首先递减此迭代器。

在调用frontback(或解引用beginend返回的迭代器)之前,要确保c非空。如果容器为空,if中操作的行为将是未定义的。

表9.6: 在顺序容器中访问元素的操作
at和下标操作只适用于stringvectordequearray
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 中的元素
}

在容器中访问元素的成员函数(即,frontback、下标和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下标运算符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) << 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.3.3 删除元素

表9.7: 顺序容器的删除操作
这些操作会改变容器的大小,所以不适用于array
forward_list有特殊版本的erase
forward_list不支持pop_backvectorstring不支持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) 删除迭代器be所指定范围内的元素。返回一个指向最后一个被删元素之后元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器
c.clear() 删除c中的所有元素。返回void

WARNING: 删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vectorstring中删除点之后位置的迭代器、引用和指针都会失效。

pop_frontpop_back成员函数

vectorstring不支持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页中删除一个范围内的元素的程序,如果elem1elem2相等会发生什么,如果elem2是尾后迭代器,或者elem1elem2皆为尾后迭代器,又会发生什么?

【出题思路】
理解范围删除操作的两个迭代器参数如何决定删除操作的结果。

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

因此elem1elem2都是尾后迭代器时,容器保持不变。
如果elem2为尾后迭代器,elem1指向之前的合法位置,则会删除从elem1开始直至容器末尾的所有元素。


为了删除一个容器中的所有元素,我们既可以调用clear,也可以用beginend获得的迭代器作为参数调用erase:

slist.clear();  // 删除容器中所有元素
slist.erase(slist.begin(), slist.end());    // 等价调用

练习9.26:使用下面代码定义的 ia,将 ia 拷贝到一个 vector 和一个 list 中。使用单迭代器版本的 eraselist 中删除奇数元素,从 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
*/

9.3.4 特殊的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) be是表示范围的一对迭代器(be不能指向lst内)
lst.insert_after(p,il) il是一个花括号列表。返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回p。若p为尾后迭代器,则函数行为未定义
emplace_after(p,args) 使用argsp指定的位置之后创建一个元素。返回一个指向这个新元素的迭代器。若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;
}

9.3.5 改变容器大小

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缩小容器,则指向被删除元素的迭代器、引用和指针都会失效:对vectorstringdeque进行resize可能导致迭代器、指针 和引用失效。

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

【出题思路】 更深入理解改变容器大小的操作。
【解答】对于元素是类类型,则单参数resize版本要求该类型必须提供一个默认构造函数

9.3.6 容器操作可能使迭代器失效

在向容器添加元素后:

  • 如果容器是vectorstring,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。

  • 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。

  • 对于listforward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。

当我们删除一个元素后:

  • 对于listforward_list,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效。

  • 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代群、引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。

  • 对于vectorstring,指向被删元素之前元素的迭代器、引用和指针仍有效。 注意:当我们删除元素时,尾后迭代器总是会失效。

建议:管理迭代器:

必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对vectorstringdeque尤为重要。

编写改变容器的循环程序

程序比须保证每个循环步中都更新迭代器、引用和指针。如果循环中调用的是inserterase,那么更新迭代器很容易。这些操作都返回迭代器,我们可以用来更新:

// 此程序删除 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页中删除偶数值元素并复制奇数值元素的程序不能用于listforward_list。为什么,修改程序,使之也能用于这些类型。

【出题思路】
本题继续练习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;

    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: 如果在一个循环体中插入/删除 dequestringvector 中的元素,不要缓存 end 返回的迭代器。

// 更安全的方法:在每个循环步添加/删除元素后都重新计算 end
while (begin != v.end()) {
     
    // 做一些处理
    ++begin;    // 向前移动 begin,因为我们想在此元素之后插入元素
    begin = v.insert(begin, 42);    // 插入新值
    ++begin;    // 向前移动 begin,跳过我们刚刚加入的元素
}

9.4 vector对象是如何增长的

假定容器中元素是连续存储的,且容器的大小是可变的,考虑向vectorstring中添加元素会发生什么:
如果没有空间容纳新元素,容器不可能简单地将它添加到内存中其他位置——因为元素必须连续存储。容器必须分配新的内存空间来保存己有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放旧存储空间。如果我们每添加一个新元素,vector就执行一次这样的内存分配和释放操作,性能会慢到不可接受。

为了避免这种代价,标准库实现者采用了可以减少容器空间重新分配次数的策略。当不得不获取新的内存空间时,vectorstring的实现通常会分配比新的空间需求更大的内存空问。容器预留这些空间作为备用,可用来保存更多的新元素。
这样,就不需要每次添加新元素都重新分配容器的内存空间了。这种分配策略比每次添加新元素时都重新分配容器内存空间的策略要高效得多。其实际性能也表现得足够好——虽然vector在每次重新分配内存空间时都要移动所有元素,但使用此策略后,其扩张操作通常比listdeque还要快。

管理容量的成员函数

表9.10: 容器大小管理操作
shrink_to_fit只适用于vectorstringdeque
capacityreserve只适用于vectorstring
c.shrink_to_fit() 请将capacity()减少为与size()相同大小
c.capacity() 不重新分配内存空间的话,c可以保存多少元素
c.reserve(n) 分配至少能容纳n个元素的内存空间

Note: reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。

在新标准库中,我们可以调用shrink_to_fit来要求dequevectorstring退回不需要的内存空间。此函数指出我们不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求。也就是说,调用shrink_to_fit也并不保证一定退回内存空间。

capacitysize

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

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

【解答】
首先,reservesvec分配了1024个元素(字符串)的空间。

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

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

*练习9.35:解释一个vectorcapacitysize有何区别。

【解答】

理解capacitysize的区别非常重要。

容器的size是指它己经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。

练习9.37:为什么listarray没有capacity成员函数?

【出题思路】
理解listarrayvector在数据结构上的差异导致内存分配方式的不同。

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

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

因此它们均不需要capacity

9.5 额外的string操作

未完待续!!!

  • string类和C风格字符数组之间的相互转换
  • 用下标代替迭代器的版本

9.5.1 构造string的其他方法

substr操作

9.5.2 改变string的其他方法

appendreplace函数

改变string的多种重载函数

9.5.3 string搜索操作

指定在哪里开始搜索

逆向搜素

9.5.4 compare函数

9.5.5 数值转换

9.6 容器适配器 (adapter)

除了顺序容器外,标准库还定义了三个顺序容器适配器:
stackqueuepriority_queue

本质上,一个适配器是种机制,能使某种事物的行为看起来像另外种事物一样。一个容器适配器接受一种己有的容器类型,使其行为看起来像一种不同的类型。

例如:stack适配器接受一个顺序容器(除arrayforward_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) 交换ab的内容,ab必须有相同类型,包括底层容器类型也必须相同。
a.swap(b)

定义一个适配器

每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构 造的数拷贝该容器来初始化适配器。

例如,假定deq是一个deque,我们可以用deq来初始化一个新的stack,如下所示:

stack<int> stk(deq);    // 从 deq 拷贝元素到 stk

默认情况下,stackqueue是基于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_backpop_backback操作,因此可以使用除arrayforward_list之外的任何容器类型来构造stack

  • queue适配器要求backpush_backfrontpush_front,因此,它可以构造于listdeque之上,但不能基于vector构造。

  • priority_queue除了front, push_backpop_back操作之外还要求随机访问能力,因此它可以构造于vectordeque之上,但不能基于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实现,也可以在listvector之上实现
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未列出的queuepriority_queue操作
queue默认基于deque实现,priority_queue默认基于vector实现
queue也可以用listvector实现,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允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。
饭店按照客人预定时间而不是到来时间的早晚来为他们安排座位,就是一个优先队列的例子。默认清况下,标准库在元素类型上使用<运算符来确定相对优先级。

你可能感兴趣的:(C++,Primer,c++)