- 一个容器就是一些特定类型对象的集合。顺序容器(sequentialcontainer)为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器
- 时的位置相对应。与之相对的,我们将在第11章介绍的有序和无序关联容器,则根据关键字的值来存储元素。
- 标准库还提供了三种容器适配器,分别为容器操作定义了不同的接口,来与容器类型适配。我们将在本章末尾介绍适配器。
9.1顺序容器概述
- 表9.1列出了标准库中的顺序容器,所有顺序容器都提供了快速顺序访问元素的能力。但是,这些容器在以下方面都有不同的性能折中:
- 向容器添加或从容器中删除元素的代价
- 非顺序访问容器中元素的代价
- 除了固定大小的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保证是一个快速的常量时间的操作
- 通常,使用vector是最好的选择,除非你有很好的理由选择其他容器
- 以下是一些选择容器的基本原则:
- 除非你有很好的理由选择其他容器,否则应使用vector0
- 如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list
- 如果程序要求随机访问元素,应使用vector或deque。
- 如果程序要求在容器的中间插入或删除元素,应使用list或forward_list
- 如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用dequeo
- 如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地向vector追加数据,然后再调用标准库的sort函数(我们将在10.2.3节介绍sort(第343页))来重排容器中的元素,从而避免在中间位置添加元素。如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中。
- 如果程序既需要随机访问元素,又需要在容器中间位置插入元素,那该怎么办?答案取决于在list或forward_list中访问元素与vector或deque中插入/删除元素的相对性能。一般来说,应用中占主导地位的操作(执行的访问操作更多还是插入/删除更多)决定了容器类型的选择。在此情况下,对两种容器分别测试应用的性能可能就是必要的了。
- 如果你不确定应该使用哪种容器,那么可以在程序中只使用vector和list公共的操作:使用迭代器,不使用下标操作,避免随机访问。这样,在必要时选择使用vector或list都很方便
9.2容器库概览
- 容器类型上的操作形成了一种层次:
- 某些操作是所有容器类型都提供的(参见表9.2,第295页)。
- 另外一些操作仅针对顺序容器(参见表9.3,第299页)、关联容器(参见表11.7,第388页)或无序容器(参见表11.8,第395页)。
- 还有一些操作只适用于一小部分容器。
- 在本节中,我们将介绍对所有容器都适用的操作。本章剩余部分将聚焦于仅适用于顺序容器的操作。关联容器特有的操作将在第11章介绍。
- 一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。即,deque定义在头文件deque中,list定义在头文件list中,以此类推。容器均定义为模板类(参见3.3节,第86页)。例如对vector,我们必须提供额外信息来生成特定的容器类型。对大多数,但不是所有容器,我们还需要额外提供元素类型信息:
- list<Sales_data>//保存Sales_data对象的list
- deque<double>//保存double的deque
对容器可以保存的元素类型的限制
- 顺序容器几乎可以保存任意类型的元素。特别是,我们可以定义一个容器,其元素的类型是另一个容器。这种容器的定义与任何其他容器类型完全一样:在尖括号中指定元素
- 类型(此种情况下,是另一种容器类型):
- vector<vector<string>>Lines;//vector的vector 此处lines是一个vector,其元素类型是string的vector。
- 虽然我们可以在容器中保存几乎任何类型,但某些容器操作对元素类型有其自己的特殊要求。我们可以为不支持特定操作需求的类型定义容器,但这种情况下就只能使用那些没有特殊要求的容器操作了。
- 例如,顺序容器构造函数的一个版本接受容器大小参数(参见3.3.1节,第88页),它使用了元素类型的默认构造函数。但某些类没有默认构造函数。我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数:
- 假定noDefault是一个没有默认构造函数的类型
- vector<noDefault> vl (10, init) ; // 正确:提供了 元素初始化器
- vector<noDefault> v2 (10) ; / / 错误:必须提供一个元素初始化器
- 当后面介绍容器操作时,我们还会注意到每个容器操作对元素类型的其他限制。
9.2.1迭代器
- 与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。例如,标准容器类型上的所有迭代器都允许我们访问容器中的元素,而所有迭代器都是通过解引用运算符来实现这个操作的。类似的,标准库容器的所有迭代器都定义了递增运算符,从当前元素移动到下一个元素。
- 表3.6(第96页)列出了容器迭代器支持的所有操作,其中有一个例外不符合公共接口特点一forward_list迭代器不支持递减运算符(--)。表3.7(第99页)列出了迭代器支持的算术运算,这些运算只能应用于string、vector、deque和array的迭代器。我们不能将它们用于其他任何容器类型的迭代器。
- 一个迭代器范围(iteratorrange)由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置(onepastthelastelement)这两个迭代器通常被称为begin和end,或者是first和last(可能有些误导),它们标记了容器中元素的一个范围。虽然第二个迭代器常常被称为last,但这种叫法有些误导,因为第二个迭代器从来都不会指向范围中的最后一个元素,而是指向尾元素之后的位置。迭代器范围中的元素包含first所表示的元素以及从first开始直至last(但不包含last)之间的所有元素。这种元素范围被称为左闭合区间。其标准数学描述为[begin,end)。表示范围自begin开始,于end之前结束。迭代器begin和end必须指向相同的容器。end可以与begin指向相同的位置,但不能指向begin之前的位置。
对构成范围的迭代器的要求
- 如果满足如下条件,两个迭代器begin和end构成一个迭代器范围。它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置,且我们可以通过反复递增begin来到达end。换句话说,end不在begin之前。
使用左闭合范围蕴含的编程假定
- 标准库使用左闭合范围是因为这种范围有三种方便的性质。假定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递增一次,我们确定循环最终会结束。
9.2.2容器类型成员
- 每个容器都定义了多个类型,如表9.2所示(第295页)。我们已经使用过其中三种:sizetype(参见3.2.2节,第79页)、iterator和const_iterator(参见3.4.1节,第97页)。
- 除了已经使用过的迭代器类型,大多数容器还提供反向迭代器。简单地说,反向迭代器就是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义也都发生了颠倒。
- 例如,对一个反向迭代器执行++操作,会得到上一个元素。
- 剩下的就是类型别名了,通过类型别名,可以在不了解容器中元素类型的情况下使用它。(类型别名类似typedef,将一个使用多个容器嵌套而成的自定义的类型起一个统一的名字,都叫value_type)如果需要元素类型,可以使用容器的value_type。如果需要元素类型的一个引用,可以使用reference或const_:refe:rence。这些元素相关的类型别名在泛型编程中非常有用
- 为了使用这些类型,我们必须显式使用其类名:
- list<string>::iterator iter; //iter是通过list<string>定义的一个迭代器类型 operator 如< <= > >=
- vector<int>::difference_type count; //count是通过vector<int>定义的一个difference_type类型 容器内,两个元素之间的距离差值;it2 - it1返回值为difference_type
- 参考链接 https://blog.csdn.net/pzhw520hchy/article/details/80368869
- 这些声明语句使用了作用域运算符(参见1.2节,第7页)来说明我们希望使用list<string>类的iterator成员及vector<int>类定义的difference_type
9.2.3begin和end成员
-
begin和end操作(参见341节,第95页)生成指向容器中第一个元素和尾元素之后位置的迭代器。这两个迭代器最常见的用途是形成一个包含容器中所有元素的迭代器范围。
- 如表9.2(第295页)所示,begin和end有多个版本:带r的版本返回反向迭代器(我们将在10.4.3节(第363页)中介绍相关内容);以c开头的版本则返回const迭代器:
- list<string>a=("Milton","Shakespeare","Austen"};
- auto itl=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的情况类似。当我们对一个非常量对象调用这些成员时,得到的是返回iterator的版本。只有在对一个const对象调用这些函数时,才会得到-个const版本。与const指针和引用类似,可以将一个普通的iterator转换为对应的const_iterator,但反之不行。
- 以c开头的版本是C++新标准引入的,用以支持auto(参见2.5.2节,第61页)与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时,是const_iterator
- auto it8=a.cbegin();//it8是const_iterator
- 当auto与begin或end结合使用时,获得的迭代器类型依赖于容器类型,与想要如何使用迭代器毫不相干。但以c开头的版本还是可以获得const_iterator的,而不管容器的类型是什么
- 当不需要写访问时,应使用cbegin 和 cend 这个不对元素进行更改,仅仅是访问
9.2.4容器定义和初始化咆
- 每个容器类型都定义了一个默认构造函数(参见7.1.4节,第236页)。除array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数
- C c1 = c2;//拷贝初始化
- C c1(c2);//赋值初始化
将一个容器初始化为另一个容器的拷贝
- 将一个新容器创建为另一个容器的拷贝的方法有两种:可以直接拷贝整个容器,或者(array除外)拷贝由一个迭代器对指定的元素范围。
- 为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且,新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换(参见4.11节,第 141页)为要初始化的容器的元素类型即可。
- //每个容器有三个元素,用给定的初始化器进行初始化
- 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);//错误 容器类型必须匹配
- forward_list<string>words(articles.begin(),articles.end());//正确:可以将const char*元素转换为string
- 当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同
- 接受两个迭代器参数的构造函数用这两个迭代器表示我们想要拷贝的一个元素范围。与以往一样,两个迭代器分别标记想要拷贝的第一个元素和尾元素之后的位置。新容器的大小与范围中元素的数目相同。新容器中的每个元素都用范围中对应元素的值进行初始化。
- 由于两个迭代器表示一个范围,因此可以使用这种构造函数来拷贝一个容器中的子序列。例如,假定迭代器it表示authors中的一个元素,我们可以编写如下代码
- //拷贝元素,直到(但不包括)it指向的元素
- deque<string>authList(authors.begin(),it);
列表初始化
- 在新标准中,我们可以对一个容器进行列表初始化(参见3.3.1节,第88页)
- //每个容器有三个元素,用给定的初始化器进行初始化
- list<string>authors=(nMiltonn,"Shakespeare",''Austen"};
- vector<constchar*>articles={"a”,"an","the"};
- 当这样做时,我们就显式地指定了容器中每个元素的值。对于除array之外的容器类型,初始化列表还隐含地指定了容器的大小:容器将包含与初始值一样多的元素。
与顺序容器大小相关的构造函数
- 除了与关联容器相同的构造函数外,顺序容器(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页)的类类型,可以只为构造函数提供一个容器大小参数。如果元素类型没有默认构造函数,除了大小参数外,还必须指定一显式的元素初始值。
- 有顺序容器的构造函数才接受大小参数,关联容器并不支持
标准库array具有固定大小
- 与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:
- array<int,42>//类型为:保存42个int的数组
- array<string,10>//类型为:保存10个string的数组
- 为了使用array类型,我们必须同时指定元素类型和大小:
- array<int,10>::size_type i;//数组类型包括元素类型和大小
- array<int>::size_typej;//错误: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.1节,第102页),但array并无此限制:
- int digs[10]={0,1,2,3,4,5,6,7,8,9};
- intcpy[10]=digs;//错误:内置数组不支持拷贝或赋值
- array<int,10>digits={0,1,2,3,4,5,6,7,8,9};
- array<int,10>copy=digits;//正确:只要数组类型匹配即合法
- 与其他容器一样,array也要求初始值的类型必须与要创建的容器类型相同。此外,array还要求元素类型和大小也都一样,因为大小是array类型的一部分。
9.2.5赋值和swap
- 表9.4中列出的与赋值相关的运算符可用于所有容器。赋值运算符将其左边容器中的全部元素替换为右边容器中元素的拷贝:
- cl=c2;//将cl的内容替换为c2中元素的拷贝
- cl=(a,b,c);//赋值后,cl大小为3
- 第一个赋值运算后,左边容器将与右边容器相等。如果两个容器原来大小不同,赋值运算后两者的大小都与右边容器的原大小相同。第二个赋值运算后,cl的size变为3,即花括号列表中值的数目
- 与内置数组不同,标准库array类型允许赋值。赋值号左右两边的运算对象必须具有相同的类型:
- array<int,10>al={0,1,2,3,4,5,6,7,8,9};
- array<int,10>a2={0};//所有元素值均为0
- al=a2;//替换al中的元素
- a2={0};//错误:不能将一个花括号列表赋予数组
- 由于右边运算对象的大小可能与左边运算对象的大小不同,因此array类型不支持assign,也不允许用花括号包围的值列表进行赋值
- vector::assign() 用来构造一个vector函数,类似于copy函数
使用assign(仅顺序容器)
- 赋值运算符要求左边和右边的运算对象具有相同的类型。它将右边运算对象中所有元素拷贝到左边运算对象中。顺序容器(array除外)还定义了一个名为assign的成员,允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。assign操作用参数所指定的元素(的拷贝)替换左边容器中的所有元素。例如,我们可以用assgin实现将一个vector中的一段char*值赋予一个list中的string:
- list<string>names;
- vector<const char*>oldstyle;
- names=oldstyle;//错误:容器类型不匹配
- names.assign(oldstyle.cbegin(),oldstyle.cend()); //正确:可以将const char*转换为string
- 这段代码中对assign的调用将names中的元素替换为迭代器指定的范围中的元素的拷贝。assign的参数决定了容器中将有多少个元素以及它们的值都是什么。
- 由于其旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器
- assign的第二个版本接受一个整型值和一个元素值。它用指定数目旦具有相同给定 值的元素替换容器中原有的元素:
- // 等价于 slistl. clear ();
- // 后跟slistl. insert (slistl. begin () , 10, "Hiya ! n );
list<string> slistl (1) ; // 1 个元素,为空 - string slistl .assign (10, "Hiya" ) ; // 10 个元素,每 个 都 是 "Hiya!”
使用swap
- swap操作交换两个相同类型容器的内容。调用swap之后,两个容器中的元素将会交换;
- vector<string>svecl(10);//10个元素的vector
- vector<string>svec2(24);//24个元素的vector
- swap(svecl,svec2);
- 调用swap后,svecl将包含24个string元素,svec2将包含10个string。除array外,交换两个容器内容的操作保证会很快--元素本身并未交换,swap只是交换了两个容器的内部数据结构。
- 除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成
- 元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍指向swap操作之前所指向的那些元素。但是,在swap之后,这些元素已经属于不同的容器了。例如,假定iter在swap之前指向svecl[3]的string,那么在swap之后它指向svec2[3]的元素。与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效。与其他容器不同,swap两个array会真正交换它们的元素。因此,交换两个array所需的时间与array中元素的数目成正比。
- 因此,对于array,在swap操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array中对应元素的值进行了交换。在新标准库中,容器既提供成员函数版本的swap,也提供非成员版本的swap。而早期标准库版本只提供成员函数版本的swap。非成员版本的swap在泛型编程中是非常重要的。统一使用非成员版本的swap是一个好习惯。
9.2.6容器大小操作
- 除了一个例外,每个容器类型都有三个与大小相关的操作。成员函数size(参见3.2.2节,第78页)返回容器中元素的数目;empty当size为0时返回布尔值true,否则返回false;max_size返回一个大于或等于该类型容器所能容纳的最大元素数的值。
- forward_list支持max_size和empty,但不支持size,原因我们将在下一节解释。
9.2.7关系运算符
- 每个容器类型都支持相等运算符(==和!=);除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。即,我们只能将一个vector<int>与另一个vector<int>进行比较,而不能将一个vector<int>与一个list<int>或一个vector<double>进行比较。
- 比较两个容器实际上是进行元素的逐对比较。这些运算符的工作方式与string的关系运算(参见3.2.2节,第79页)类似:
- 如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等。
- 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
- 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。
- 下面的例子展示了这些关系运算符是如何工作的:
容器的关系运算符使用元素的关系运算符完成比较
- 只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器
- 容器的相等运算符实际上是使用元素的==运算符实现比较的,而其他关系运算符是使用元素的运算符。如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。例如,我们在第7章中定义的Sales_data类型并未定义==和〈运算。因此,就不能比较两个保存Salesdata元素的容器