92.顺序容器概述

110.C++顺序容器

一个容器就是一些特定类型对象的集合。顺序容器(sequential container)为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。与之相对的,有序和无序关联容器, 则根据关键字的值来存储元素。
标准库还提供了三种容器适配器,分别为容器操作定义了不同的接口,来与容器类型适配。

1.顺序容器概述

下表列出了标准库中的顺序容器,所有顺序容器都提供了快速顺序访问元素的能力。但是,这些容器在以下方面都有不同的性能折中:

●向容器添加或从容器中删除元素的代价
●非顺序访问容器中元素的代价

  下表列出了标准库中的顺序容器,所有顺序容器都提供了快速顺序访问元素的能力。但是,这些容器在以下方面都有不同的性能折中:

●向容器添加或从容器中删除元素的代价
●非顺序访问容器中元素的代价

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

  除了固定大小的array外,其他容器都提供高效、灵活的内存管理。我们可以添加和删除元素,扩张和收缩容器的大小。容器保存元素的策略对容器操作的效率有着固有的,有时是重大的影响。在某些情况下,存储策略还会影响特定容器是否支持特定操作。

  例如,string和vector将元素保存在连续的内存空间中。由于元素是连续存储的,由元素的下标来计算其地址是非常快速的。但是,在这两种容器的中间位置添加或删除元素就会非常耗时:在一次插入或删除操作后,需要移动插入/删除位置之后的所有元素,来保持连续存储。而且,添加一个元素有时可能还需要分配额外的存储空间。在这种情况下,每个元素都必须移动到新的存储空间中。

  list和forward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问:为了访问 个元素,我们只能遍历整个容器。而且,与vector、deque和array相比,这两个容器的额外内存开销也很大。

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

  forward_list和array是新C++标准增加的类型。与内置数组相比,array是一种更安全、更容易使用的数组类型。与内置数组类似,array对象的大小是固定的。因此,array不支持添加和删除元素以及改变容器大小的操作。forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此,forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。

注意:

新标准库的容器比旧版本快得多。新标准库容器的性能几乎肯定与最精心优化过的同类数据结构一样好(通常会更好)。现代C++程序应该使用标准库容器,而不是更原始的数据结构,如内置数组。

确定使用哪种顺序容器

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

以下是一些选择容器的基本原则:

●除非你有很好的理由选择其他容器,否则应使用vector。
●如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list。
●如果程序要求随机访问元素,应使用vector或deque。
●如果程序要求在容器的中间插入或删除元素,应使用list或forward_list。
●如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque。
●如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则

---首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地向vector追加数据,然后再调用标准库的sort函数来重排容器中的元素,从而避免在中间位置添加元素。

---如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到个vector中。

  如果程序既需要随机访问元素,又需要在容器中间位置插入元素,那该怎么办?答案取决于在list或forward_list中访问元素与vector或deque中插入/删除元素的一相对性能。一般来说,应用中占主导地位的操作(执行的访问操作更多还是插入/删除更多)决定了容器类型的选择。在此情况下,对两种容器分别测试应用的性能可能就是必要的了。

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

2.容器库概述

容器类型上的操作形成了一种层次:

●某些操作是所有容器类型都提供的(参见表9.2)。
●另外一些操作仅针对顺序容器(参见表9.3)、关联容器(参见表11.7,第388页)或无序容器(参见表11.8,第395页)。
●还有一些操作只适用于一小部分容器。

  一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。即,deque定义在头文件deque中,list定义在头文件list中,以此类推。容器均定义为模板类(参见3.3节,第86页)。例如对vector,我们必须提供额外信息来生成特定的容器类型。对大多数,但不是所有容器,我们还需要额外提供元素类型信息:

list<Sales_data>//保存Sales_data对象的巨st 
deque<double>//保存double的deque

对容器可以保存的元素类型的限制

  顺序容器几乎可以保存任意类型的元素。特别是,我们可以定义一个容器,其元素的类型是另一个容器。这种容器的定义与任何其他容器类型完全一样:在尖括号中指定元素类型(此种情况下,是另一种容器类型):

vector<vector<string>> lines; //vector的vector

此处lines是一个vector,其元素类型是string的vector。

注意:较旧的编译器可能需要在两个尖括号之间键入空格,例如,vector<vector<string> >。

  虽然我们可以在容器中保存几乎任何类型,但某些容器操作对元素类型有其自己的特殊要求。我们可以为不支持特定操作需求的类型定义容器,但这种情况下就只能使用那些没有特殊要求的容器操作了。
  例如,顺序容器构造函数的一个版本接受容器大小参数(参见3.3.1节,第88页),它使用了元素类型的默认构造函数。但某些类没有默认构造函数。我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数:

//假定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,将迭代器b和e指定的范围内的元素拷贝到c(array不支持)
C c{a, b, c...}; 列表初始化c
const_reference 元素的const左值类型(即,const value_type&)
构造函数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

2.1迭代器

  与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。例如,标准容器类型上的所有迭代器都允许我们访问容器中的元素,而所有迭代器都是通过解引用运算符来实现这个操作的。类似的,标准库容器的所有迭代器都定义了递增运算符,从当前元素移动到下一个元素。

  73.迭代器 - CodeMagicianT - 博客园 (cnblogs.com)列出了容器迭代器支持的所有操作,其中有一个例外不符合公共接口特点---forward_list迭代器不支持递减运算符(--)。73.迭代器 - CodeMagicianT - 博客园 (cnblogs.com)列出了迭代器支持的算术运算,这些运算只能应用于string、vector、deque和array的迭代 器。我们不能将它们用于其他任何容器类型的迭代器。

(1)迭代器范围

注意:迭代器范围的概念是标准库的基础。

  一个迭代器范围(iterator range)由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置(one past the last element)。这两个迭代器通常被称为begin和end,或者是first和last(可能有些误导),它们标记了容器中元素的一个范围。

  虽然第二个迭代器常常被称为last,但这种叫法有些误导,因为第二个迭代器从来都不会指向范围中的最后一个元素,而是指向尾元素之后的位置。迭代器范围中的元素包含first所表示的元素以及从first开始直至last(但不包含last)之间的所有元素。

  这种元素范围被称为左闭合区间(left-inclusive interval),其标准数学描述为

[begin, end) 

表示范围自begin开始,于end之前结束。迭代器begin和end必须指向相同的容器。end可以与begin指向相同的位置,但不能指向begin之前的位置。

对构成范围的迭代器的要求
--------------------------------------------------------------------------------------------------------------------------------
如果满足如下条件,两个迭代器begin和end构成一个迭代器范围:
●它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置,且
●我们可以通过反复递增begin来到达end。换句话说,end不在begin之前。
警告:编译器不会强制这些要求。确保程序符合这些约定是程序员的责任。

(2)使用左闭合范围蕴含的编程假定

  标准库使用左闭合范围是因为这种范围有三种方便的性质。假定begin和end构成一个合法的迭代器范围,则

●如果begin与end相等,则范围为空

●如果begin与end不相等,则范围至少包含一个元素,且begin指向该范围中的第一个元素

●我们可以对begin递增若干次,使得begin==end

这些性质意味着我们可以像下面的代码样用一个循环来处理一个元素范围,而这是安全的:

while(begin != end)
{
    *begin = val;//正确:范围为空,因此begin指向一个元素
    ++begin;//移动迭代器,获取下一个元素
}

  给定构成一个合法范围的迭代器begin和end,若begin==end,则范围为空。在此情况下,我们应该退出循环。如果范围不为空,begin指向此非空范围的一个元素。因此,在while循环体中,可以安全地解引用begin,因为begin必然会指向一个元素。最后,由于每次循环对begin递增一次,我们确定循环最终会结束。

2.2容器类型成员

  每个容器都定义了多个类型,如表9.2所示(第295页)。已经使用过其中三种:size_type、iterator和const_iterator。

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

  剩下的就是类型别名了,通过类型别名,我们可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type。如果需要元素类型的一个引用,可以使用reference或const_reference。这些元素相关的类型别名在泛型编程中非常有用。

为了使用这些类型, 我们必须显式使用其类名:

//iter是通过list<string>定义的一个迭代器类型
list<string>::iterator iter; 
//count是通过vector <int>定义的一个 difference_type 类型
vector<int>::difference_type count;

   这些声明语句使用了作用域运算符来说明我们希望使用list<string>类的iterator成员及vector<int>类定义的difference_type。

2.3begin和end成员

  begin和end操作成指向容器中第一个元素和尾元素之后位置的迭代器 。这两个迭代器最常见的用途是形成一个包含容器中所有元素的迭代器范围。
  如表9.2所示,begin和end有多个版本:带r的版本返回反向迭代器中介绍相关内容);以c开头的版本则返回const迭代器:

list<string> a = {"Milton", "Shakespeare", "Austen"}; 
auto it1 = a.begin();//list<string>::iterator 
auto it2 = a.rbegin(); //list<string>::reverse_iterator 
auto it3 = a.cbegin(); //list<string>::const_iterator
auto it4 = a.crbegin();//list<string>::const_reverse_iterator 

  不以c开头的函数都是被重载过的。也就是说,实际上有两个名为 begin的成员。一个是const成员(参见7.1.2节, 第231页),返回容器的const_iterator类型。 另一个是非常量成员,返回容器的iterator类型。rbegin、end和rend的情况类似。当一个const对象调用这些函数时,才会得到一个const版本。与const指针和引用类似,可以将一个普通的iterator转换为对应的 const_iterator,但反之不行。

  以c开头的版本是C++新标准引入的,用以支持auto与begin和end函数结合使用。过去,没有其他选择,只能显式声明希望使用哪种类型的迭代器:

//显式指定类型
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.cegin();//it8是const_iterator

  当auto与begin或end结合使用时,获得的迭代器类型依赖于容器类型,与我们想要如何使用迭代器毫不相干。但以c开头的版本还是可以获得const_iterator的,而不管容器的类型是什么。

建议:当不需要写访问时, 应使用cbegin和cend。

2.4容器定义和初始化

  每个容器类型都定义了一个默认构造函数(参见7.1.4节, 第236页)。除array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。

表9.3: 容器定义和初始化
C c 默认构造函数。如果C是一个array,则c中元素按默认方式初始化;否则c为空
C c1(c2) c1初始化为c2的拷贝。c1和c2必须是相同类型(即,它们必须是相同的容器类型,且保存的是相同的元素类型;对于array类型,两者还必须具有相同大小)
C c1 = c2
C c{a,b,c...} c初始化为初始化列表中元素的拷贝。列表中元素的类型必须与C的元素类型相容。对于array类型,列表中元素数目必须等于或小于array的大小,任何遗漏的元素都进行值初始化(参见3.3.1节,第88页)
C c = {a,b,c...}
C c(b,e) c初始化为迭代器b和e指定范围中的元素的拷贝。范围中元素的类型必须与C的元素类型相容(array不适用)
只有顺序容器(不包括array)的构造函数才能接受大小参数
C seq(n) seq包含n个元素,这些元素进行了值初始化;此构造函数是explicit的(参见7.5.4节,第265页)。(string不适用)
C seq(n, t) seq包含n个初始化为值t的元素

(1)将一个容器初始化为另一个容器的拷贝

  将一个新容器创建为另一个容器的拷贝的方法有两种:可以直接拷贝整个容器,或者(array除外)拷贝由一个迭代器对指定的元素范围。
  为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且,新容器和原容器中的元素类型也可以不同,只要能将要拷贝的[元素转换](78.类型转换 - CodeMagicianT - 博客园 (cnblogs.com))为要初始化的容器的元素类型即可。

//每个容器有三个元素,用给定的初始化器进行初始化
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 ()); 
注意:当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。

  接受两个迭代器参数的构造函数用这两个迭代器表示我们想要拷贝的一个元素范围。与以往一样,两个迭代器分别标记想要拷贝的第一个元素和尾元素之后的位置。新容器的大小与范围中元素的数目相同。新容器中的每个元素都用范围中对应元素的值进行初始化。
  由于两个迭代器表示一个范围,因此可以使用这种构造函数来拷贝一个容器中的子序列。例如,假定迭代器it表示authors中的一个元素,我们可以编写如下代码

//拷贝元素,直到(但不包括)it指向的元素
deque<string> authList(authors.begin(), it);

(2)列表初始化

在新标准中,我们可以对一个容器进行列表初始化(参见3.3.1节,第88页)

//每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"}; 
vector<const char*> articles = {"a", "an", "the" J; 

  当这样做时,我们就显式地指定了容器中每个元素的值。对于除array之外的容器类型,初始化列表还隐含地指定了容器的大小:容器将包含与初始值一样多的元素。

(3)与顺序容器大小相关的构造函数

  除了与关联容器相同的构造函数外,顺序容器(array除外)还提供另一个构造函数,它接受一个容器大小和一个(可选的)元素初始值。如果我们不提供元素初始值,则标准库会创建一个值初始化器(参见3.3.1节,第88页)

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 

  如果元素类型是内置类型或者是具有默认构造函数(参见9.2节,第294页)的类类型,可以只为构造函数提供一个容器大小参数。如果元素类型没有默认构造函数,除了大小参数外,还必须指定一个显式的元素初始值。

注意:只有顺序容器的构造函数才接受大小参数,关联容器并不支持。

(4)标准库array具有固定大小

  与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:

array<int, 42>//类型为:保存42个int的数组 
array<string, 10>//类型为:保存10个string的数组 

 为了使用array类型,我们必须同时指定元素类型和大小:

array<int, 10>::size_type i;//数组类型包括元素类型和大小 
array<int>::size_type j;//错误:array<int>不是一个类型

  由于大小是array类型的一部分,array不支持普通的容器构造函数。这些构造函数都会确定容器的大小,要么隐式地,要么显式地。而允许用户向一个array构造函数传递大小参数,最好情况下也是多余的,而且容易出错。

  array大小固定的特性也影响了它所定义的构造函数的行为。与其他容器不同,默认构造的array是非空的:它包含了与其大小一样多的元素。这些元素都被默认初始化(参见2.2.1节,第40页),就像一个内置数组(参见3.5.1节,第102页)中的元素那样。如果我们对array进行列表初始化,初始值的数目必须等于或小于array的大小。如果初始值数目小于array的大小,则它们被用来初始化array中靠前的元素,所有剩余元素都会进行值初始化(参见3.3.1节,第88页)。在这两种情况下,如果元素类型是一个类类型,那么该类必须有一个默认构造函数,以使值初始化能够进行:

array<int, 10> ial;//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

  值得注意的是,虽然我们不能对内置数组类型进行拷贝或对象赋值操作(参见3.5. 节,第102页),但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还要求元素类型和大小也都一样,因为大小是array类型的部分。

(5)赋值和swap

  表9.4中列出的与赋值相关的运算符可用于所有容器。赋值运算符将其左边容器中的全部元素替换为右边容器中元素的拷贝:

c1 = c2;//将c1的内容替换为c2中元素的拷贝 
c1 = {a,b,c};//赋值后,c1大小为3

一个赋值运算后,左边容器将与右边容器相等。如果两个容器原来大小不同,赋值运算后两者的大小都与右边容器的原大小相同。第二个赋值运算后,c·的size变为3,即花括号列表中值的数目。
  与内置数组不同,标准库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... } 将c中元素替换为初始化列表中元素的拷贝(array不适用)
swap(c1,c2) 交换c1和c2中的元素。c1和c2必须具有相同的类型。swap通常比从c2向c1拷贝元素快得多
c1.swap(c2)
assign操作不适用于关联容器和array
seq.assign(b, e) 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素
seq.assign(il) 将seq中的元素替换为初始化列表il中的元素
seq.assign(n, t) 将seq中的元素替换为n个值为t的元素
警告:赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为array和string的情况除外)。

(6)使用assign(仅顺序容器)

  赋值运算符要求左边和右边的运算对象具有相同的类型。它将右边运算对象中所有元素拷贝到左边运算对象中。顺序容器(array除外)还定义了一个名为assign的成员,允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。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的迭代器不能指向调用assign的容器。

  assign的第二个版本接受一个整型值和一个元素值。它用指定数目且具有相同给定值的元素替换容器中原有的元素:

//等价于 slist1.clear();
//后跟 slist1.insert(slist1.begin(), 10, "Hiya!"); 
list<string> slist1(1);//1个元素,为空string 
slist1.assign(10, "Hiya!"); // 10 个元素, 每个都是

(7)使用 swap

  swap操作交换两个相同类型容器的内容。调用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只是交换了两个容器的内部数据结构(交换名字)。

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

  元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍指向swap操作之前所指向的那些元素。但是,在swap 之后,这些元素已经属于不同的容器了。例如,假定iter 在swap之前指向svec1[3] 的string,那么在swap之后它指向svec2[3]的元素。与其他容器不同,对一个string 调用swap会导致迭代器、引用和指针失效。

  与其他容器不同,swap 两个array会真正交换它们的元素。因此,交换两个array 所需的时间与array中元素的数目成正比。
  因此,对于array,在swap操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array中对应元素的值进行了交换。
  在新标准库中,容器既提供成员函数版本的swap,也提供非成员版本的swap。而早期标准库版本只提供成员函数版本的swap。非成员版本的swap在泛型编程中是非常重要的。统—使用非成员版本的swap是一个好习惯。

(8)容器大小操作

  除了一个例外,每个容器类型都有三个与大小相关的操作。成员函数size返回容器中元素的数目;empty当size为0时返回布尔值true,否则返回false;max_size返回一个大于或等于该类型容器所能容纳的最大元素数的值。forward_list支持max_size和empty,但不支持size。

(9)关系运算符

  每个容器类型都支持相等运算符(==和!=):除了无序关联容器外的所有容器都支持关系运算符(>、>= 、<、<=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。即,我们只能将一个vector<int>与另一个vector<int>进行 比较,而不能将一个vector<int>与一个list<int>或一个vector<double>进行比较。
  比较两个容器实际上是进行元素的逐对比较。这些运算符的工作方式与string的关系运算类似:

●如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等。
●如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
●如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。

下面的例子展示了这些关系运算符是如何工作的:

vector<int> v1 = { 1, 3, 5, 7, 9, 12}; 
vector<int> v2 = { 1, 3, 9}; 
vector<int> v3 = { 1, 3, 5, 7} ; 
vector<int> v4 = { 1, 3, 5, 7, 9, 12 }; 
v1 < v2 //true;v1和v2在元[2]处不同:v1[2]小于等于v2[2] 
vl < v3 //false;所有元素都相等,但v3中元素数目更少
vl == v4 //true;每个元素都相等,且v1和v4大小相同
vl == v2 //false:v2元素数目比v1少

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

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

  容器的相等运算符实际上是使用元素的==运算符实现比较的,而其他关系运算符是使 用元素的<运算符。如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。

  顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系到了元素如何存储、访问、添加以及删除。本部分将介绍顺序容器所特有的操作。

3.顺序容器操作

3.1向顺序容器添加元素

  除array外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小。表9.5列出了向顺序容器(非array)添加元素的操作。

表9.5:向顺序容器添加元素的操作
这些操作会改变容器的大小;array不支持这些操作。
forward_list有自己专有版本的insert和emplace;参见9.3.4节(第312页)。
forward_list不支持push_back和emplace_back。
vector和string不支持push_front和emplace_front。
c.push_back(t) 在c的尾部创建一个值为t或由args创建的元素。返回void
c.emplace_back(args)
c.push_front(t) 在c的头部创建一个值为t或由args创建的元素。返回void
c.emplace_front(args)
c.insert(p, t) 在迭代器p指向的元素之前创建一个值为t或由args创建的元素。返回指向新添加的元素的迭代器
c.emplace(p, 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
警告:向一个vector、String或deque插入元素会使所有指向容器的迭代器、引用和指针失效。

  当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。在一个vector或string的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。

(1)使用 push_back

  除array和forward_list之外,每个顺序容器(包括string类型)都支持push_back。

例如,下面的循环每次读取一个string到 word中,然后追加到容器尾部:

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

  对push_back的调用在container尾部创建了一个新的元素,将container的size增大了1。该元素的值为 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'
}
关键概念:容器元素是拷贝
--------------------------------------------------------------------------------------------------------------------------------
    当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。

(2)使用 push_front

  除了push_back、lis、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添加到llist头部。每个元素都插入到list的新的开始位置(new beginning)。即,当我们插入1时,它会被放置在0之前,2被放置在1之前,依此类推。因此,在循环中以这种方式将元素添加到容器中,最终会形成逆序。在循环执行完毕后,ilist保存序列3、2、1、0。

  注意,deque像vector一样提供了随机访问元素的能力,但它提供了vector所不支持的push_front。deque保证在容器首尾进行插入和删除元素的操作都只花费常数时间。与vector一样,在deque首尾之外的位置插入元素会很耗时。

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

  push_back和push_front操作提供了一种方便地在顺序容器尾部或头部插入单个元素的方法。insert成员提供了更一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素。vector、deque、list和string都支持insert成员。forward_list提供了特殊版本的insert成员,我们将在9.3.4节(第312页)中介绍。
  每个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!"); 
警告:将元素插入到vector、deque和string中的任何位置都是合法的。然而,这样做可能很耗时。

(4)插入范围内元素

  除了第一个迭代器参数之外,insert函数还可以接受更多的参数,这与容器构造函数类似。其中一个版本接受一个元素数目和一个值,它将指定数量的元素添加到指定位置之前,这些元素都按给定值初始化:

  这行代码将10个元素插入到svec的末尾,并将所有元素都初始化为string"Anna”。
接受一对迭代器或一个初始化列表的insert版本将给定范围中的元素插入到指定位置之前:

svec.insert(svec.end(), 10, "Anna"); 

这行代码将10个元素插入到svec的末尾,并将所有元素都初始化为string"Anna”。
  接受一对迭代器或一个初始化列表的insert版本将给定范围中的元素插入到指定位置之前:

vector<string> v = {"quas正", "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"}); 
//运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end()); 

如果我们传递给 insert一对迭代器,它们不能指向添加元素的目标容器。

  在新标准下,接受元素个数或范围的insert版本返回指向第一个新加入元素的迭代器(在旧版本的标准库中,这些操作返回 void)如果范围为空,不插入任何元素,insert操作会将第一个参数返回。

(5)使用 insert 的返回值

通过使用 insert的返回值,可以在容器中一个特定位置反复插入元素:

list<string> lst; 
auto iter = lst.begin(); 
while (cin >> word) 
    iter = lst.insert(iter, word);//等价于调用push_front
注意:理解这个循环是如何工作的非常重要,特别是理解这个循环为什么等价于调用push_front尤为重要。

在循环之前,我们将iter初始化为lst.begin()。第一次调用insert会将我们刚刚读入的 string 插入到 iter 所指向的元素之前的位置。insert 返回的迭代器恰好指向这一个新元素。我们将此迭代器赋予 iter并重复循环,读取下一个单词。只要继续有单词读入,每步 while 循环就会将一个新元素插入到 iter 之前,并将 iter 改变为新加入元素的位置。此元素为(新的 )首元素。因此,每步循环将一个新元素插入到list 首元素之前的位置。

(6)使用emplace操作

  新标准引入了三个新成员 emplace_front、emplace 和 emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应 push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。

  当调用 push 或 insert 成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个 emplace 成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。例如,假定 c 保存 Sales_data(参见 7.1.4 节,第 237 页)元素:

//在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 调用都会创建新的 Sales_data 对象。在 调用 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);
注意:emplace素类型的构造函数相匹配。函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。

3.2访问元素

  表9.6列出了我们可以用来在顺序容器中访问元素的操作。如果容器中没有元素,访问操作的结果是未定义的。

包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有个back成员函数。这两个操作分别返回首元素和尾元素的引用:

//在解引用一个迭代器或调用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>=c.size(), 则函数行为未定义
c.at(n) 返回下标为n的元素的引用。如果下标越界,则抛出一out_of_range异常
对一个空容器调用front和back,就像使用一个越界的下标一样,是一种严重的程序设计错误。

(1)访问成员函数返回的是引用

  在容器中访问元素的成员函数(即, front、back、 下标和at)返回的都是引用。如果容器是一个const对象,则返回值是const的引用。如果容器不是const的,则返回值是普通引用,我们可以用来改变元素的值:

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中的元素
}

与往常一 样,如果我们使用auto变量来保存这些函数的返回值,并且希望使用此变量来 改变元素的值,必须记得将变量定义为引用类型。

(2)下标操作和安全的随机访问

  提供快速随机访问的容器(string、vector、deque和array)也都提供下标运算符(参见3.3.3节, 第91页)。就像我们已经看到的那样,下标运算符接受一个下标参数,返回容器中该位置的元素的引用。给定下标必须 “在范围内 “ (即,大于等于0,且小于容器的大小)。保证下标有效是程序员的责任,下标运算符并不检查下标是否在合法范围内。使用越界的下标是一种严重的程序设计错误,而且编译器并不检查这种错误。
  如果我们希望确保下标是合法的,可以使用at成员函数。at成员函数类似下标运算符,但如果下标越界,at会抛出一个out_of_range异常(参见5.6节, 第173页):标参

vector<string> svec;//空vector
cout << svec[0];//运行时错误:svec中没有元素! 
cout << svec.at(0);//抛出 一个out_of_range异常 

3.3删除元素

表9.7: 顺序容器的删除操作
这些操作会改变容器的大小,所以不适用于array。
forward_list有特殊版本的erase,参见9.3.4节(第312页)。
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
警告:删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。
警告:删除元素的成员函数并不检查其参数。在删除元素之前,程序员必须确保它们是存在的。

(1)pop_front 和 pop_back 成员函数

  pop_front和pop_back成员函数分别删除首元素和尾元素。与vector和string不支持push_front一样,这些类型也不支待pop_front。类似的,forward_list不支持pop_back。与元素访问成员函数类似,不能对一个空容器执行弹出操作。

  这些操作返回void。如果你需要弹出的元素的值,就必须在执行弹出操作之前保存它:

while (!ilist.empty()) 
{
    process (ilist.front());//对ilist的首元素进行一些处理 
    ilist.pop_front();//完成处理后删除首元素
}

(2)从容器内部删除一个元素

  成员函数erase从容器中指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase都返回指向删除的(最后一个)元素之后位置的迭代器。即,若j是i之后的元素,那么erase(i) 将返回指向j的迭代器。

例如,下面的循环删除一个list中的所有奇数元素:

list<int> lst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
auto it = lst.begin(); 
while(it != 1st.end()) 
{
    if (*it%2)//若元素为奇数
        it = lst.erase(it);//删除此元素
    else 
        ++it;
}

每个循环步中,首先检查当前元素是否是奇数。如果是,就删除该元素,并将it设置为我们所删除的元素之后的元素。如果*it为偶数,我们将it递增,从而在下一步循环检查下一个元素。

(3)删除多个元素

  接受一对迭代器的erase版本允许我们删除一个范围内的元素:

//删除两个迭代器表示的范围内的元素
//返回指向最后一个被删元素之后位置的迭代器
elem1 = slist.erase(elem1, elem2);//调用后,eleml == elem2 

迭代器elem1指向我们要删除的第一个元素,elem2指向我们要删除的最后一个元素之后的位置。
  为了删除一个容器中的所有元素,我们既可以调用clear,也可以用begin和end获得的迭代器作为参数调用erase:

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

3.4特殊的forward_list操作

  为了理解 forward_list 为什么有特殊版本的添加和删除操作,考虑当我们从一个单向链表中删除一个元素时会发生什么。如图9.1所示,删除一个元素会改变序列中的链接。在此情况下,删除elem3会改变elem2,elem2原来指向elem3,但删除elem3后,elem2 指向了elem4

  当添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生改变。为了添加或删除一个元素,我们需要访问其前驱,以便改变前驱的链接。但是,forward_list是单向链表。在 一个单向链表中,没有简单的方法来获取一个元素的前驱。出于这个原因,在 一个forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。这样,我们总是可以访问到被添加或删除操作所影响的元素。

  由于这些操作与其他容器上的操作的实现方式不同,forward_list并未定义insert、emplace和erase,而是定义了名为insert_after、emplace_after和erase_after的操作(参见表9.8)。例如,在我们的例子中,为了删除elem3,应该用指向elem2 的迭代器调用erase_after 。为了支持这些操作,forward_list也定义了before_begin,它返回一个首前(off-the-beginning)迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素”之后 “添加或删除元素(亦即在链表首元素之前添加删除元素)。

表9.5:向顺序容器添加元素的操作
lst.before_begin() 返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用。cbefore_begin()返回一个const_iterator
lst.cbefore_begin()
lst.before_begin() 在迭代器p之后的位置插入元素。t是一个对象,n是数量,b和e是表示范围的一对迭代器(b和e不能指向lst内),il是一个花括号列表。返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回p。若p为尾后迭代器,则函数行为未定义
lst.insert_after(p, n, t)
lst.insert_after(p, b, e)
lst.insert_after(p,il)
emplace_after(p, args) 使用args在p指定的位宜之后创建一个元素。返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数行为未定义
lst.erase_after(p) 删除p指向的位置之后的元素,或删除从b之后直到(但不包含)e之间的元素。返回一个指向被删元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代器。如果p指向lst的尾元素或者是一个尾后迭代器,则函数行为未定义
lst.erase_after(b,e)

  当在 forward_list中添加或删除元素时,我们必须关注两个迭代器---一个指向我们要处理的元素,另一个指向其前驱。例如,可以改写第312页中从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;//curr之前的元素
    }
}

此例中,curr表示我们要处理的元素,prev表示curr的前驱。调用begin来初始化curr,这样第一步循环就会检查第一个元素是否是奇数。我们用before_begin来初始化prev,它返回指向curr之前不存在的元素的迭代器。

  当找到奇数元素后,我们将prev传递给erase_after。此调用将prev之后的元素删除,即,删除curr指向的元素。然后我们将curr重置为erase_after的返回值,使得curr指向序列中下一个元素,prev保持不变,仍指向(新)curr之前的元素。如果curr指向的元素不是奇数,在else中我们将两个迭代器都向前移动。

3.5改变容器大小

  如表9.9所描述,我们可以用resize来增大或缩小容器,与往常一样,array不支 持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个元素 

  resize操作接受一个可选的元素值参数,用来初始化添加到容器中的元素。如果调用者未提供此参数,新元素进行值初始化(参见3.3.1节, 第88页)。如果容器保存的是类类型元素,且resize向容器添加新元素,则我们必须提供初始值,或者元素类型必须提供一个默认构造函数 。

表9.9:顺序容器大小操作
resize不适用于array
c.resize(n) 调整c的大小为n个元素。 若n < c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化
c.resize(n,t) 调整c的大小为n个元素。任何新添加的元素都初始化为值t
如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能导致迭代器、指针和引用失效。

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

  向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针样的问题(参见2.3.2节,第49页)

在向容器添加元素后:

●如果容器是vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。
●对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
●对于list和forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。

当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效,这应该不会令人惊讶。毕竟,这些元素都已经被销毁了。当我们删除一个元素后:

●对于list和forward_list,指向容器其他位置的迭代器(包括尾后迭代器和 首前迭代器)、引用和指针仍有效。
●对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代 器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。
●对于vector和string,指向被删元素之前元素的迭代器、引用和指针仍有效。注意:当我们删除元素时,尾后迭代器总是会失效。

注意:使用失效的迭代器、指针或引用是严重的运行时错误。
建议:管理迭代器
当你使用迭代器(或指向容器元素的引用或指针)时,最小化要求迭代器必须保持有效的程序片段是一个好的方法。
   由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对vector、string和deque尤为重要。

(1)编写改变容器的循环程序

  添加/删除vector、string或deque元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是insert 或 erase,那么更新迭代器很容易。这些操作都返回迭代器,我们可 以用来更新:

//傻瓜循环,删除偶数元素,复制每个奇数元素
vector<int> vi= {0,1,2,3,4,5,6,7,8,9}; 
auto iter = vi.begin();//调用begin而不是cbegin,因为我们要改变vi
while (iter != vi.end()) 
{
    if (*iter % 2) 
    {
        iter = vi.insert(iter, *iter);//复制当前元素
        iter += 2;//向前移动迭代器,跳过当前元素以及插入到它之前的元素
    }
    else
        iter = vi.erase(iter);//删除偶数元素
    //不应向前移动迭代器,iter指向我们删除的元素之后的元素
}

此程序删除vector中的偶数值元素,并复制每个奇数值元素。我们在调用insert和 erase后都更新迭代器,因为两者都会使迭代器失效。
  在调用erase后,不必递增迭代器,因为erase返回的迭代器已经指向序列中下一个元素。调用insert后,衙要递增迭代器两次。记住,insert在给定位置之前插入新元素,然后返回指向新插入元素的迭代器。因此,在调用insert后,iter指向新插入元素,位于我们正在处理的元素之前。我们将迭代器递增两次,恰好越过了新添加的元素和正在处理的元素,指向下一个未处理的元素。

(2)不要保存end返回的迭代器

  当我们添加/删除vector或string的元素后,或在deque中首元素之外任何位置添加/删除元素后,原来end返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器,一直当作容器末尾使 用。通常C++标准库的实现中end()操作都很快,部分就是因为这个原因。

  例如,考虑这样一个循环,它处理容器中的每个元素,在其后添加一个新元素。我们希望循环能跳过新添加的元素,只处理原有元素。在每步循环之后,我们将定位迭代器,使其指向下“优化”这个循环,在循环之前保存end()返回的迭代器,一直用作容器末尾,就会导致一场灾难:

//灾难:此循环的行为是未定义的
auto begin = v. begin(); 
end = v.end();//保存尾迭代器的值是一个坏主意
while (begin != end)
{
    //做一些处理
    //插入新值,对begin重新赋值,否则的话它就会失效
    ++begin;//向前移动begin,因力我们想在此元素之后插入元素
    begin = v.insert(begin, 42);//插入新值
    ++begin;//向前移动begin跳过我们刚刚加入的元素
}

此代码的行为是未定义的。在很多标准库实现上,此代码会导致无限循环。问题在于我们一将end操作返回的迭代器保存在一个名为end的局部变量中。在循环体中,我们向容器中添加了一个元素,这个操作使保存在end中的迭代器失效了。这个迭代器不再指向v中任何元素,或是v中尾元素之后的位置。

建议:如果在一个循环中插入/删除deque、string或vector中的元素,不要缓存end返回的迭代器。

必须在每次插入操作后重新调用end(),而不能在循环开始前保存它返回的迭代器:

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

参考资料:

C++ Primer

posted @ 2023-05-11 13:37  CodeMagicianT  阅读(53)  评论(0编辑  收藏  举报