内容:
9.1 顺序容器概览
9.2 容器库概览
9.3 顺序容器操作
9.4 vector如何增长
9.5 string的其他操作
9.6 容器适配器
本章小结
专业术语
本章是第三章的后续内容,用于完善对标准库顺序容器的知识介绍。顺序容器中元素的顺序对应于元素被添加的位置。标准库还定义了几个关联容器,他们的位置与关键字(key)有关。将在第十一章介绍关联容器相关的知识。
所有的容器类共享同样的接口,不同的容器按照自己的方式进行扩展。这些通用接口使标准库更容易掌握——我们基于某种容器所学的内容也适用于其他的容器。每种容器对性能和功能提供了不同的权衡策略。
容器保存有指定类型的对象集合。顺序容器让程序员控制元素被存放和读取的顺序。顺序不依赖元素的值。而是依赖于元素被放入容器中的位置。相对的,对于有序关联容器和无序关联容器的顺序则依赖于关键字的值。
标准库还提供了三个容器适配器,分别为容器的操作提供了不同的接口。在本章的最后介绍这部分知识。
注意:本章是基于3.2,3.3,3.4小节的内容。因此会假定读者已经熟悉了这些小节中的内容了。
表9.1列出了顺序容器,每个顺序容器都提供了快速访问他们元素的方法。但是这些容器也根据下面的操作,在性能之间也有所权衡:
除了array以外,其他的容器都是大小可变的容器,这些容器提供了高效的,灵活的内存管理。我们也可以增加和移除元素,增大或者减小容器的大小。容器对元素存储采取的策略对操作的性能有固有的,有时是重大意义的影响。在某些情况下,策略还会影响特定容器是否支持特定操作。
例如,string和vector在连续内存中保存他们的元素。因为元素是连续存放的,所以根据索引来计算元素的地址就非常的块。但是在这些容器中间增加元素,就非常耗时:增加或者移除之后的元素必须移动以保持其连续性。而且,增加一个元素有时需要额外分配空间。此时,每个元素都需移动到新分配的空间中。
list和forward_list设计的初衷就是在任何位置增加和移除元素要快。作为代价,这两个类型不支持元素的随机访问。只能通过迭代来访问某个元素。而且这些容器和vector,deque,array比起来他们的内存消耗也较大。
deque是更复杂的数据结构,跟string,vector,一样,deque支持随机访问。同时在中间增加和移除元素是非常耗性能的。但是,在两端添加和删除的速度很快。与list或者forward_list添加元素的速度相当。
forward_list和array是c++11新标准新增的。与内置数组相比,array更加安全,更易于使用。跟内置的数组一样,标准库array有固定的大小。因此,array不支持增加元素或者删除元素,或者改变容器大小的操作。forward_list的目的是达到手写的单向链表的性能。因此,forward_list没有size的操作,因为存储和计算他们的大小会比手写列表多出很多性能消耗。对于其他容器而言,size被保证为,快速的,常量时间的操作。
注意:新的标准库容器比以前的版本更快,具体原因将在13.6小节中介绍。标准库容器几乎和精心优化的数据结构性能一样好(甚至是更好)。因此,现代c++程序应该使用库容器而不是基础数据机构,比如内置数组。
决定使用哪种顺序容器
提示:通常,当没有原因要用其他容器时,使用vecotr是最好的。
此处有几个重要的规则用于选取哪种容器:
如果程序即需要随机访问,又需要在中间插入元素,那么应该如何选择?这将依赖于两种性能的比较:第一种是:访问list或者forward_list的性能;第二种是插入和删除vector或者deque的性能。通常情况下,应用程序的主要操作,将决定选择哪种容器。在这种情况下,分别使用两种容器,然后进行性能测试是有必要的
经验之谈:
如果不能确定使用哪种容器,那么就只使用vector和list的公共操作:使用迭代器,而不是下标 ,这样避免随机访问元素。这种方式在有必要使用vector或者list的时候是非常方便的。
容器上面的操作形成了一种层次:
本段将介绍所有容器的通用操作,本章的剩下部分将聚焦于仅适用于顺序容器的操作。第十一章将会介绍关联容器相关的操作。
通常情况下,定义容器的头文件名和容器的类型名相同。即,deque在deque头文件中,list在list头文件中。容器是类模板(3.3小节)。跟vector一样,我们必须提供额外的信息才能产生特定的容器类型。对于大多数的容器(不是所有容器),我们必须提供的额外信息为元素的类型:
list<Sales_data> //list保存有Sales_data对象
deque<double> //deque保存有double
对容器可以保存的类型的限制
几乎所有的类型都可以作为容器的元素类型。事实上,我们还可以定义元素类型为容器类型的容器。定义这种类型跟定义容器是一样的:在尖括号类提供元素类型:
vector<vector<string>> lines;
此处lines是vector类型,他的元素为vector,而他的元素的元素类型为string。
注意:旧版编译器可能需要在两个尖括号中插入一个空格,如:
vector<vector<string> >
虽然容器可以存储几乎所有的类型,但是某些容器对元素类型有自己的要求。我们可以为不支持特定操作的类型定义容器,此时只能使用那些没有特殊要求的容器。
例如,顺序容器的构造器有一个版本带有一个大小的实参,此时使用的是元素类型的默认构造器。某些类没有默认构造器,因此,就不能通过这种方式来定义相应的容器:
//假定noDefault是一种没有默认构造器的类型
vector<noDefault> v1(10,init); //正确:提供了元素的初始值
vector<noDefault> v2(10); //错误:没有提供元素初始值
在后续的介绍中,还会注意到每个容器操作对元素类型的其他限制。
跟容器一样,迭代器也有相同的接口:如果一个迭代器支持某种操作,那么对于提供操作的每个迭代器这种操作都是一样的。例如,所有标准库容器迭代器都可以从容器中访问元素,要访问元素都通过解引用运算符完成。同样的,所有的标准库容器迭代器都使用自增运算符来移动元素到一下个位置。
容器迭代器支持表3.6(第三章中)中的所有操作。唯一的一个例外是:forward_list迭代器不支持自减运算符。表3.7(第三章中)中的迭代器操作仅适用于string,vector,deque和array,而不能用在其他类型的迭代器上面。
迭代器范围
注意:迭代范围的概念是标准库的基础
迭代器范围由同一容器的一对迭代器标识,要么指向某个元素,要么指向尾后元素。这两个迭代器通常称为begin与end,或者称为first与last,他们用于标记容器的元素范围。
名字叫做last的迭代器虽然常常使用,但是却有点误导,因为第二个迭代器从不指向要标识范围的最后一个元素。相反,他用于指向最后一个元素的后面位置。这个范围内的元素包括first指向的元素,但是不包括last所指向的元素。
元素范围称为:左闭合区间。用标准的数学写法为:
[ begin, end )
这个表明,范围从begin开始,从end结束,但是不包括end。begin迭代器和end迭代器必须指向相同的容器。end迭代器可以等于begin迭代器,但是不能指向begin之前的元素。
对构成范围的迭代器的要求
两个迭代器,begin和end,构成了一个迭代器范围。需要满足下面条件:
- 指向同一个容器的元素,或者尾后元素。
- 通过自增begin能够到达end。换句话说,end不能再begin之前。
警告:编译器不会强制这些要求。保证程序符合这些要求是程序员的责任。
隐含地使用左闭合区间进行编程
标准库使用左闭合区间,因为这种范围有三个方便的属性。加入begin和end标识了一个有效的迭代器范围,那么:
这些特性就意味着,我们可以安全的编写循环处理程序,例如下面的循环处理每一个元素:
while(begin != end){
*begin = val;
++begin;
}
给上面的begin和end一个有效的迭代器范围,就可得知,如果begin == end,这个范围就是空的。此时,直接退出循环。如果范围不为空,begin就指向了第一个元素。因此,在while的循环体内,解引用begin是安全的,因为begin肯定指向一个元素。最后,因为循环体自增了begin,所以循环最终会结束。
每个容器都定义有几个类型,列在表9.2中。我们已经使用了其中之三:size_type,iterator,const_iterator.
除了我们使用的迭代器类型以外,大多数的容器还提供了反向迭代器。简要的说,反向迭代器就是反向遍历容器的一种迭代器。例如,++作用在一个反向迭代器中,返回的是前一个元素。在10.4.3小节中将有更多细节介绍反向迭代器.
剩下的类型别名,可以让我们直接使用元素的类型,而不用知道元素的具体类型.如果需要使用元素类型,则使用value_type.如果需要使用这种类型的引用,则使用reference或者const_reference.这些跟元素有关的类型,在常用编程中非常有用,我们将在第十六章中详细介绍。
为了使用这些类型,必须使用其类名:
//iter是定义在list中的迭代器
list<string>::iterator iter;
//count是定义在vecto中的difference_type
vector<int>::difference_type count;
这些声明使用了作用域运算符,来表明我们想使用list
begin和end返回容器的第一个迭代器和尾后迭代器。这个两个迭代器通常用来格式化迭代器范围,而这个范围表示了这个容器中的所有元素。
在表9.2中所示,begin和end有好几个版本:带有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类型。另外一个为非const成员,他返回容器的iterator类型。rbegin,end和rend都类似。当我们在非const对象上面调用这些函数的时候,就会调用返回iterator版本的那个函数。只有在const对象上面调用这些函数的时候,才会调用返回const_iterator版本的那个函数。跟const的指针和引用一样,可以将普通的iterator转换成const_iterator,但是,反之则不能。
在c++11新标准中,引入了c开头版本的begin和end函数,用于支持auto和begin与end的结合使用。在以前,只能显示的声明我们要使用的迭代器:
//显示的指定类型
list<string>::iterator it5 = a.begin();
list<string>::const_iterator it6 = a.begin();
//根据a的类型返回iterator或者const_iterator
auto it7 = a.begin(); //如果a是const,那么为const_iterator
auto it8 = a.cbegin(); //it8是const_iterator
当我们使用auto时,返回的iterator类型取决于容器的类型。与如何使用迭代器毫无相关。c开头的版本让我们获取到const_iterator,而不用管容器的类型。
经验之谈:当没必要对对象写操作时,使用cbegin,和cend
每一个容器类型都有一个默认的构造器,唯一例外就是array类型,默认构造器使用指定类型创建空的容器。其他的构造器需要传递一个表示容器大小的实参,然后各元素使用值初始化,同样也有一个例外是array。
初始化容器为其他容器的副本
有两种方法,创建一个容器作为另外一个容器的副本:一种,直接复制容器(除了array);第二种,复制一对迭代器所标识的元素范围。
为了创建一个容器作为另外一个容器的副本,这个容器和元素必须类型匹配。当传递迭代器范围进行拷贝时,不需要容器类型完全相同。因此,新容器和旧容器的元素类型可以是不同的,只要被复制的元素类型可以转换为被初始化的元素类型:
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处的元素,但是不包括it
deque<string> authList(authors.begin(),it);
列表初始化
在c++11新标准下,可以列表初始化一个容器:
list<string> authors ={“Milton”,”Shakespeare”,”Austen”};
vector<const char*> articles = {“a”,”an”,”the”};
此处,我们显示的指定了元素的值。除array类型以外,初始值列表页隐式的指定了容器的大小:容器大小将和初始值列表的个数相同。
顺序容器大小相关的构造器
除了有与关联容器相同的构造器以外,顺序容器还有一个构造器,其形参为一个表示容器大小的int,和一个可选的元素初始值。如果没有提供元素初始值,那么标准库使用值初始化这些元素。
vecto<int> ivec(10,-1); //是个int元素,每个都被初始化为-1
list<string> svec(10,”hi!”); //十个元素,每个元素都是“hi!”
forward_list<int> ivec(10); //十个元素,每个元素都被初始化为0
deque<string> svec(10); //十个元素,每个元素都是空字符串
如果元素类型是内置类型,或者有默认构造器的类类型,则可以使用带有大小实参的构造器。如果元素类型,没有默认构造函数,那么除了大小以外还必须显示的提供一个元素初始值。
注意:带有大小的构造器只对顺序容器有效;他们不支持关联容器
arrays库有固定大小
就像内置数组的大小是数组的一部分一样,array库的大小也是它自身类型的一部分。当我们定义array的时候,除了指定元素类型以外,还需要提供容器的大小:
array<int ,42> //类型:array保存有42个int
array<string,10> //类型:array存有10个strings
为了使用array类型,必须指定元素类型和大小:
array<int,10>::size_type i; //array类型包含元素的类型和大小
array<int>::size_type j; //错误:array不是一种类型
因为大小是array类型的一部分,所以,array不支持常见的容器构造器。因为这些构造器,或隐式或显式的,决定了容器的大小。让用户传递一个大小值给构造器是多余的并且是错误的。
array固定大小的特性也影响了array构造器的行为。不像其他容器,默认构造的array不是非空的:它包含了与其大小一样多的元素。这些元素被默认初始化,就像内置类型的数组一样。如果我们列表初始化array,那么列表中的值只能等于或者小于array的大小。如果少于array的大小,那么剩下的元素则进行值初始化。在上述两种情况中,如果元素类型是类类型,那么这个类类型必须要要有默认构造函数。
array<int ,10> ival; //十个默认初始化的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来说,元素类型和大小也必须一样,因为大小也是array类型的一部分。
赋值相关的操作,已经列在表9.4中。表9.4中的所有操作都适用于所有的容器。赋值运算符,将右边的容器元素复制到左边的容器中:
c1 = c2; //将c1中的内容,替换为c2中的内容
c1 = {a,b,c}; //赋值之后,c1的大小为3
第一个赋值之后, 左边和右边的容器相等。如果容器的大小不相同,赋值之后,两个容器的大小都与右边容器的大小相同。第二个赋值之后,c1的大小为3,他的元素的值为大括号中提供的值。
不像内置类型,array库类型,不允许赋值,左边和右边的操作数必须类型相同
:
array<int,10> a1 = {0,1,2,3,4,5,6,7,8,9};
array<int,10> a2 = {0}; //所有的元素都是0
a1 = a2; //用a2中的元素副本代替a1中的元素
a2 = {0}; //错误:不能用大括号括起来的列表赋值给一个array
因为右操作数的大小可能与左操作数的大小不同,所以array类型就不支持赋值操作了,也不支持用大括号列表进行赋值。
使用assgin(仅顺序容器)
赋值运算符需要左右两边的操作数有相同的类型。他将右边操作数的内容全部复制到左边操作数中。顺序容器还定义了一个叫做assign的成员,这个成员支持从不同但兼容的类型中进行赋值,也支持从容器的子序列中进行赋值。assign 运算符用实参中的元素替换所有左操作数中的元素。例如,可以使用assign将vector中的某段char* 赋值给元素为string的list。
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; //错误容器类型不匹配
//正确:可以从const char* 转换成string
names.assign(oldstyle,cbegin(),oldstyle.cend());
调用assign用迭代器标记的元素替换names中的所有元素。assign的实参决定了容器中有多少元素,以及这些元素的值。
警告:因为已经存在的元素被替换了,所以传递的到assign的迭代器,不能是调用assign的容器的迭代器。
assign的第二个版本带有一个整数值,和一个元素值。他用指定的元素个数和值,替换容器中的所有元素。
···cpp
//等价于slist1.clear();
// 然后跟着 slist1.insert(slist1.begin(),10,”Hiya!”);
list slist1(1); //只有一个元素,这个元素为空字符串
slist.assign(10,”Hiya!”); //十个元素,每一个元素的值都是Hiya!
···
使用swap
swap交换两个相同容器的内容。调用swap之后,两个容器中的内容就已经交换:
vector<string> svec1(10); //vector有十个元素
vector<string> svec2(24); //vector有24个元素
swap(svec1,svec2);
调用swap之后,svec1有24个元素,svec2有十个元素。除了array以外,交换两个容器的内容会非常快,因为他们只是交换了两个容器的数据结构,而元素本身并没有交换。
注意:除了array,swap操作不会复制,删除,或者插入任何元素,它保证运行时间是常量不会随着元素的个数增加而增加。
元素没有移动,事实上就意味着:跟容器关联的迭代器,引用,指针都没有失效。跟swap调用之前比较,他们都指向了相同的元素。但是,调用了swap之后,这些元素已然在另外一个容器中了。例如,swap调用之前,iter表示svec1[3]处的字符串,swap调用之后,则表示svec2[3]处的字符串。跟容器不同的是,字符串上面调用swap,可能让相关的迭代器,引用和指针无效。
而array的swap行为跟其他的容器又不像了,swap两个array不会交换元素。因此,交换两个array的时间则和array的元素大小正相关。
调用swap之前,和之后,array相关的指针,引用和迭代器已然指向的是同一个元素。当然,array的元素的值与另外一个array元素的值进行了交换。
在新标准中,容器提供了成员和非成员的swap版本。在库早起提供的版本中只有swap的成员版本。非成员版本在泛型编程中非常重要。使用非成员的swap版本是一个好习惯。
除一特例之外,容器类型有三个大小相关的操作:size成员返回容器的大小,empty成员判断容器是否为空,max_size成员返回一个大于或者等于容器元素的数,这个数表明这个容器可以保存的最大的元素个数。而forward_list提供max_size和empty但是不提供size,具体原因将在下一小段中介绍
每个容器类型都支持等于运算符(==和!=);除无序关联容器外,其他的所有容器也支持关系运算符(>,>=,<,<=).运算符两边的运算对象,必须是相同的容器类型,并且元素类型也要一样。即,vector
比较两个容器,实际是两个容器中的元素挨个比较。这些运算符的运算方式跟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 <v3; //false
v1 == v4; //true
v1 == v2; //false
关系运算符使用元素的关系运算符
注意:仅仅当容器的元素定义了相应的关系运算符才可在容器上面使用关系运算符
容器的相等运算符,使用元素的==运算符。并且使用的关系运算符,也是使用的元素的<运算符。如果元素不支持某种运算符,那么保存有这种类型的容器就不能使用这种运算符。 例如在第七章种定义的Sales_data,它没有定义==和<运算符,因此不能对保存有Sales_data的容器,运行这些运算符:
vector<Sales_data> storeA,storeB;
if(storeA < storeB) //错误:Sales_data没有定义小于运算符
顺序容器和关联容器在组织他们的元素时,使用的方式不同。这些差异影响了元素的存储,访问,增加和删除的方式。前面的小段介绍了所有容器(表9.2)的常用操作。本章的后续小节将会介绍只适用于顺序容器的操作。
除array以外,所有的库容器都提供了灵活的内存管理。可以在运行时,动态的增加或者删除元素从而改变容器的大小。表9.5列出了向顺序容器增加元素的操作。
当我们使用这些操作的时候,我们必须谨记:容器使用了不同的策略来分配元素并且这些策略会影响性能。除了在vector或者string的尾部增加元素,或者在deque的两端增加元素,其他位置的增加元素,都需要移动元素。因此,增加元素到vector或者string可能导致整个对象重新分配内存,然后将旧元素移动到新的内存中。
使用push_back
在3.3.2小节中,我们看到push_back将一个元素放在了vector中的末尾。除了array和forward_list以外,每个顺序容器都支持push_back。
例如,下面的循环每次读取一个string,到word中:
string word;
while(cin >> word)
container.push_back(word);
调用push_back在container的末尾创建一个新的元素,container的大小加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’;相同
}
关键概念:容器元素是副本
当使用一个对象初始化容器,或者插入一个对象到一个容器中,这个对象的副本被放置在容器中,而不是对象本身。就像传递对象给非引用类型的形参一样,一旦操作完成,就跟原始值没有任何关系了。后续改变了容器中的元素,并不会影响原始值,反之,亦然。
使用push_front
除了push_back以外,list,forward_list和deque容器还支持一个类似叫做push_front的操作。这个操作将一个新元素放置在容器的头部:
list<int> ilist;
for(size_t ix=0;ix!=4;++ix)
ilist.push_front(ix);
这个循环将0,1,2,3依次加在ilist的前面。每个元素插入ilist的新的起始处,即,当插入1时,它在0的前面,插入2的时候,在1的前面,以此类推。因此,在循环中的增加的元素会形成逆序的排序。这个循环执行完成之后,ilist保存的序列为3,2,1,0
注意deque,它提供了跟vector一样的快速随机访问元素的机制,但是vector没有提供push_front操作。deque保证在头部,或者尾部插入或者删除元素是常量时间。跟vector一样,在头部,尾部以外的地方插入元素,消耗的时间跟元素的个数成正相关。
在容器指定位置增加元素
push_back和push_front可以让我们在容器的头部或者尾部,非常方便的增加元素。更常用的操作是,insert让我们在指定位置,增加一个或者多个元素。insert成员被vector,deque,list和string支持。forward_list则提供了这个成员的一个特殊版本,这将在9.3.4中介绍。
每个insert函数的第一个形参都是一个迭代器。这个迭代器表示了在容器的哪个位置插入元素,他可以是容器的任何位置,包括容器的尾后位置。因为迭代器可能指向不存在的尾后元素,并且这也提供了在容器的头部插入元素的方式,所以insert将插入迭代器所指位置之前,例如下面的语句:
slist.insert(iter,”Hello!”);//在iter之前插入Hello!
在iter所指位置之前插入Hello!
即使一些容器没有提供push_front的操作,但是他们对insert的并没有这种相似的限制。所以可以使用insert将元素插入容器的头部,而不用管容器是否有push_fornt:
vector<string> svec;
list<string> slist;
//等价于调用slist.push_front(“Hello!”)
slist.insert(slist.begin(),”Hello!”);
//vector没有提供push_front,但是可以使用insert,达到相同的效果
//警告:除了在vector的尾部插入元素以外,其他的位置的插入,可能非常慢
svec.insert(svec.begin(),”Hello!”);
警告:在vector,deque,string的任何位置插入元素都是合法的,但是这样做可能造成性能上面的损耗。
插入一组元素
出现在第一个实参之后的传递给insert的实参,类似于容器构造器的参数。这个版本带有一个元素个数,和一个元素值。这个版本将这个在指定的位置之前,插入一组指定数目的元素。
svec.insert(svec.end(),10,”Anna”);
这句代码在svec后面插入,10个元素,每个元素都用Anna进行初始化
带有一对迭代器或者初始值列表的insert版本,在给定位置之前插入给定范围的元素:
vector<stirng> v = {“quasi”,”simba”,”frollo”,”scar”};
//在slist的开始处,插入v的最后两个元素
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());
当我们传递一对迭代器时,不能指向被插入的容器。
在c++11新标准中,带有一个数字,或者范围的insert版本,返回被插入元素的第一个元素的迭代器(在以前的版本中,这个函数返回值为void)。如果范围为空,则没有元素被插入,则这个函数返回它的第一个形参。
使用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的第一个元素。
使用emplace 操作
c++11新标准引入三个新的成员——emplace_front,emplace,emplace_back.这些操作构造元素而不是复制元素。这些操作对应于push_front,insert,push_back.
当我们调用push或者insert成员时,我们传递一个元素类型的对象,然后这些对象被复制到容器中。当调用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都创建了一个新的Sales_data对象。在emplace_back调用中,直接在容器管理的空间中创建对象。为了调用push_back创建了一个局部临时对象,这个对象被push到容器中
emplace函数的参数依赖于元素类型。参数必须匹配元素类型的构造器:
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函数在容器中构造函数,因此实参必须于元素类型的某个构造器匹配
表9.6中列出了可以用于访问顺序容器的操作。如果容器没有元素,那么这些元素的行为未定义:
每个顺序容器,包括array,带有有一个front成员;除了forward_list以外他们也有一个back成员。这两个成员分别返回第一元素和最后一个元素。
if(!c.empty()){
auto val = *c.begin(),val2 = c.front();
auto last = c.end();
auto val3 = *(--last);
auto val4 = c.back();
}
这个程序用两种不同的方法获取c中的第一个和最后一个元素的引用。最直接的方法是调动fornt和back.这跟调用begin,然后解引用这个返回的迭代器一样,同样的back也跟调用end然后自减迭代器之后,再解引用一样。
在这个程序中两点值得注意:其一,end迭代器返回的是尾后元素,他是一个不存在的元素。为了获得最后一个元素,必须先自减这个迭代器。其二,在调用front或者back之前,需要先判断c是否为空。如果c为空,这些在if内部的操作是未定义的。
访问操作返回的是引用
在容器中访问元素的成员返回的是引用。如果容器是const对象,返回的引用也是const。如果容器不是const,则返回一个普通的引用,这个引用可以用来改变其值。
if(!c.empty()){
c.front() = 42; //赋值42给第一个元素
auto &v = c.back();//得到最后一个元素的引用
v = 1024; //改变元素的值
auto v2 = c.back();//v2不是引用,他是c.back()的一个副本
v2 = 0; //不会改变c中元素的值
}
通常情况下,如果使用了auto来存储从这些操作中的返回值,并且我们还想使用这个变量来改变元素的值,那么我们必须记住这个变量需要定义成引用类型。
下标和安全的随机访问
提供了快速随机访问的容器(string,vector,deque)也提供了下标运算符。正如所见,下标运算符返回这个容器指定位置元素的引用。下标使用的索引必须在“范围”(即,大于等于0,小于容器的大小)内。保证这个索引的有效,需要程序来负责;下标运算符不会检查这个索引是否越界。使用越界的索引是非常严重的编程问题,但是对于这种问题,编译器并不会检查。
如果想要保证所有值有效,我们需要使用at成员来代替。at成员的行为跟下标运算符的行为类型。但是如果索引非法,at会抛出一个out_of_range异常。
···cpp
vector svec; //空的vector
cout << svec[0]; //运行时错误:svec中没有元素
cout << seve.at(0); //抛出一个out_of_range异常
···
有几个向容器中增加元素的成员,那么就有几个从容器中移除元素的成员。这些成员列在了表9.7中
警告:移除元素的成员,不会检查他们的实参。因此程序员必须保证在移除以前,这个元素已经存在。
pop_front和pop_back成员
pop_front和pop_back分别移除第一个和最后一个元素。vector没有push_front,也就没有pop_front。同样的forward_list没有pop_back.跟访问元素的成员一样,这些成员也不能对空容器进行操作。
这些操作返回void。如果你需要即将弹出的元素,你必须在调用pop之前存储这个值:
while(!ilist.empty()){
process(ilist.front()); //使用ilist的front元素进行某些操作
ilist.pop_front(); //操作完成,删除第一个元素
}
从容器内部删除一个元素
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 != lst.end())
if(*it %2)
it = lst.erase(it);
else
++it;
在每次迭代中,检查当前元素是否为奇数,如果是,则擦除这个元素,然后设置it为被擦除元素的后面哪一个元素的迭代器。如果*it是偶数,则自增it,那么在下一次迭代中,对下一个元素进行相同的操作。
移除多个元素
迭代器对的erase函数,让我们删除一组元素:
//删除两个迭代器之间的元素
//返回最后一个被删元素后面的元素
elem1 = slist.erase(elem1,elem2);
``
elem1指向了被删元素的第一个元素,elem2指向了最后一个被删元素的尾后元素。
为了删除容器中的所有元素,我们可以调用clear或者传递begin和end迭代器给erase函数:
```cpp
slist.clear();
slist.erase(slist.begin(),slist.end());
为了了解为什么forward_list对于增加和移除都有特殊的版本,我们考虑一下当我们从一个单向链表中移除元素的时候,什么是必须发生的。示例如9.1,移除元素则改变了序列中的链条。此时,移除elem3,改变elem2;因为elem2指向了elem3,再移除之后,elem2需要指向elem4.
当我们增加或者删除元素时,被增加或者删除元素的前一个元素则有一个不同的后续。为了增加或者移除元素,我们需要访问它的前续,因为需要更新元素的链条。但是,forward_list是一个单向链表,要想在单向链表中获得前序非常不易。基于此原因,增加或者删除元素是通过改变给定元素之后的元素来实现的。这样,我们总是能够访问到被添加或者被删除影响到的前续元素。
因为这些操作和其他容器不同,所以,forward_list没有定义,insert,emplace,erase.而定义了insert_after,emplace_after,erase_after.例如,在上图中,为了删除elem3,传递elem2的迭代器并调用erase_after。为了支持这种操作,forward_list也定义了before_begin.它返回头结点之前的迭代器。这个迭代器可以让我们在头结点之前增加和删除元素。
当我们增加或者删除forward_list中的元素时,必须对两个元素进行跟踪,一个就是我们操纵的元素,另外一个就是这个元素的前序元素。例如,重写349页的循环——移除奇数值,此时使用forward_list:
forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9};
auto prev = flst.before_begin();
auto curr = flst.begin();
while(curr != flst.end()){
…
if(*curr%2)
curr = flst.erase_after(prev);
else{
prev = curr;
++curr;
}
...
}
此处,curr表示正在操作的元素,prev表示其前序元素。调用begin初始化curr,目的是检查第一个元素是否为奇数或者偶数。使用before_begin初始化prev,这个函数返回curr的前序元素迭代器,这个迭代器是一个不存在的迭代器。
一旦查找到奇数元素,就将prev传递给erase_after.这个函数调用,将删除prev后面的元素。即,擦除curr指向的元素。然后设置curr为erase_after的返回值,这样curr就指向了下一个元素,并且prev指向的元素不变。prev依然是curr的前序元素。如果curr指向的元素不是奇数,那么就不得不移动prev和curr了,就像else里面编写的一样。
可以使用resize来改变容器的大小,同样array这个例外排除在外。resize描述见表9.9。如果当前大小大于请求的大小,那么将从容器的末尾删除元素。如果当前的元素小于请求的大小,元素则被增加到容器的末尾。
list<int> ilist(10,42); //十个42
ilist.resize(15); //增加5个0元素在末尾
ilist.resize(25,-1); //增加10个-1元素在末尾
ilist.resize(5); //从末尾删除20个元素
resize函数有一个可选的参数,这个参数用来初始化被加入的元素。如果这个参数不存在,增加的元素将执行值初始化。如果容器的元素是类类型,改变容器大小的时候,必须指定初始值或者元素由默认构造器。
增加或者删除容器中的元素,可能造成指向容器元素的指针,引用,迭代器失效。使用失效的指针,引用和迭代器,是非常严重的程序错误,这些跟使用未初始化的指针造成的错误是类似的。(2.3.2)
增加元素到一个容器之后:
1.如果容器重新分配了大小,那么vector或者string的迭代器,指针,或者引用就会失效。如果没有重新分配,则插入位置之前的迭代器,指针和引用依然有效,但是插入位置之后的迭代器,指针和引用依然无效
2.对一个队列的迭代器,指针,引用增加元素,(队尾,队头除外)也会让起失效。如果我们在队头或者队尾,增加元素,迭代器会失效,但是指向已经存在的元素的指针和引用依然有效。
3.指向list和forward_list的迭代器,引用,指针(包括尾后和头前迭代器)依然有效。
当然,对于已经移除了的元素,这个元素对应的迭代器,指针和引用是无效的。毕竟这个元素已经被销毁了。
当移除一个元素之后:
1.指向list和forward_list的迭代器,引用和指针(包括尾后和头前迭代器)依然有效。
2.如果移除deque的元素(除了队首,队尾以外),则迭代器,指针和引用会失效。如果移除的元素是队尾元素,那么尾后迭代器失效,其他迭代器,引用和指针不受影响。如果移除的是队首元素,其他迭代器也不会受影响。
3.对于vector和string,对于移除元素之前的元素,都不会受到影响。注意:当移除元素的时候,尾后指针总是失效。
警告:
当运行时使用已经失效的迭代器,指针和引用,是一个严重的运行时错误。
建议:管理迭代器
当你使用容器的迭代器(或者引用或者指针),尽量将迭代器最小化在一个程序片段中,避免分散开来,因为这样更容易进行对其管理。因为增加或者删除元素会使得迭代器失效。你需要保证这些迭代器有效。这些建议对vector,string,deque尤其重要
写一个改变容器的循环
对于vector,string,deque的增加和移除元素,必须考虑迭代器,引用或者指针可能失效的问题。程序必须保证迭代器,引用或者指针在每次循环中都有效。如果调用insert或者erase,刷新迭代器非常容易。这些操作返回迭代器,这个迭代器可以用来重置另外一个迭代器。
vector<int> vi = {0,1,2,34,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);
}
这个程序移除奇数元素,并且重复每个偶数元素。在每次调用insert和erase之后,刷新迭代器,因为,这两个函数会让迭代器失效。
调用erase之后,不需要增加迭代器,因为迭代器返回的是被移除元素之后的元素。调用insert之后,增加迭代器两次。因为,插入操作,是在指定的位置插入元素,然后返回的是插入元素的迭代器。因此,insert返回的迭代器,指向了新的元素,即旧元素的前面的哪一个元素。因此,需要对迭代器加2,以跳过这个两个元素。
避免存储end返回的迭代器。
当对一个vector或者string,增加或者移除元素的时候,或者对deque增加或者移除除第一个元素之外的其他元素,由end返回的迭代器总是失效的。因此,循环应该总是调用end,而不是使用end的副本。另外一个原因,c++标准总是将end的调用设计的很快。
作为一个例子:一个循环处理每一个元素,并且在原始元素之后,增加一个新的元素。我们想循环,忽略新增的元素,只处理原始元素。因此,每次增加元素之后,都需要将迭代器重新定位到下一个元素数据。如果尝试,通过存储end的返回值,来优化这个循环,这将是一个非常致命的错误:
auto begin = v.begin(), end = v.end();
while(begin != end){
++begin;
begin = v.insert(begin,42);
++begin;
}
这段代码的行为是未定义的。最常见的问题是:无限循环。这个问题是因为,我们存储了end返回来的值。在循环体内,新增了元素。增加元素会使得end的迭代器失效。因此,这个以前的迭代器,指向了一个不存在的元素,后者指向了以前的一个元素。
建议:
在插入或者删除deque,string或者vector的元素时,不要缓存end的返回值。
对于存储end返回值的迭代器,应该每次插入之后,重新计算这个迭代器。
while(begin != v.end()){
++begin;
begin = v.insert(begin,42);
++begin;
}
为了快速随机的访问,vector的存储是连续存储的。每一个元素紧挨着上一个元素。通常,我们不应该关心一个库类型的具体实现,而应该关心怎么使用。但是,对于vector和string,他们的实现已经成为了它的接口的一部分了。
假设元素是连续的,并且容器的大小是可变的,如果增加元素给vector或者string,会发生什么:如果对于新元素,没有足够的空间,那么容器就不能将这个新元素,存放在另外一个地方,因为所有的元素必须连续。此时,容器必须重新分配内存,去保存已经存在的元素和新元素,并将旧元素,迁移到新的地址里面去,然后增加新元素,最后销毁以前的内存。如果vector每次增加元素都要对内存进行分配和释放,那么性能将变得不可接受。
为了避免这种消耗,库的实现使用了,减少分配次数的策略。当需要分配新的内存时,总是分配比实际更多的内存。容器将这些多余的空间作为保留,用于存放后续增加的元素。因此,就不需要为每次新增的元素,分配内存。
这个策略比每次新增元素再分配内存更加高效。事实上,他的性能也足够好,尽管每次分配内存之后,vector必须进行移动操作,但是他通常也比list和deque更加高效。
管理容量的成员函数
vector和string通常提供了成员函数,让我们能够跟他们的实现的内存分配的部分进行交互。如表9.10 。
注意:
reserver不会改变容器中元素的个数。它只影响vector预分配的内存
只有当请求的空间大小大于当前的容量时,reserve才会改变vector的容量。如果请求的大小大于当前的容量,reserver分配至少跟请求的大小一样的空间(甚至可能更大)。
如果请求的大小小于或者等于已经存在的容量,reserve什么也不会做。事实上,调用reserver时,传递的是一个小于容量的值,他不会造成容器变小。因此,调用reserve之后,容量将大于或者等于传递给reserve的参数。
所以,调用reserve并不会减少容器使用的空间。同样的,resize成员函数,只改变容器中的元素个数,而不是容量。因此,无法通过使用resize去减少容器预分配的容量。
在c++11新库中,可以调用,shrink_to_fit,请求,deque,vector,或者string,返还不需要的内存。这个函数表明,我们不在需要多余的空间。但是,具体的实现是开放的,这些实现可以忽略这个函数的请求。因此,不保证调用shrink_to_fit一定返还内存。
capacity 和size
搞清楚capacity和size是非常重要的。容器的size指的是,该容器中保存的元素的个数;容器的capactity是,在重新分配内存前,这个容器可以保存多少元素。
下面的代码之处了size和capacity之间的差别:
vector<int> ivec;
cout << “ivec::size:” << ivec.size()
<< “capacity:” << ivec.capacity() << endl;
for(vector<int>::size_type ix = 0;ix!=24;++ix)
ivec.push_back(ix);
cout << “ivec::size:” << ivec.size()
<< “ capacity: ” << ivec.capacity() << endl;
运行上面的代码,结果如下:
ivec::size : 0 capacity : 0
ivec::size :24 capacity: 32
我们知道一个空的vector 他的size是0.显然,我的库的实现,也将capactiy设置为0.当我们增加元素到vector中时,vector的size和元素的个数相同。而capacity至少和size一样大,可以更大。到底要比size大多少,完全由库的实现决定。在我的机器上面,增加24个元素,容器的capacity变为32.
我们还可以在分配一点额外的空间:
ivec.reserve(50);
cout << “ivec::size: ” << ivec.size()
<< “ capacity: ” << ivec.capacity() << endl;
此时,这个输出表明:reserve额外分配了,我们请求的空间:
ivec::size 24 capacity: 50
接下来就可以用完保留的空间,可以通过如下方法使用:
while(ivec.size() != ivec.capacity()){
ivec.push_back(0);
}
cout << “ivec::size : ” << ivec.size()
<< “ capacity : ” << ivec.capacity() << endl;
输出表明,我们用完了保留空间,size和capacity相等:
ivec::size: 50 capacity:50
因为,我们仅仅使用保留空间,所以不需要vector再去进行空间分配。事实上,只要操作不超过vector的capacity大小,vector就不会重新分配。
如果,此时,我们再增加一个元素,那么vector不得不重新进行内存的分配了。
Ivec.push_back(42);
cout << “ivec::size:” << ivec.size()
<<” capacity:” << ivec.capacity() << endl;
这个程序的输出如下:
ivec::size: 51 capacity:100
这个表明这个vector的实现,使用了双倍的策略,即如果要分配空间,则将其当前容量翻倍。
我们可以调用shrink_to_fit请求,让超出size的不必要的内存返还给操作系统:
ivec.shrink_to_fit();
cout << “ivec::size: ” << ivec.size()
<< “ capacity:” << ivec.capacity() << endl;
调用shrink_to_fit 只是一个请求,标准库不一定就将内存返还给操作系统:
注意:
每一个vector的实现,都可以使用自己的分配策略。但是,必须是在必要的时候才分配内存。
只有当如下情况时,vector才会重新分配内存,1,当size和capacity相等时,进行插入操作。2,调用resize或者reserve时,传递的参数大于capacity。至于每次分配多少额外的内存,则由具体的实现来定义。
每种实现,必须保证:使用push_back增加元素的时,效率足够好。从技术角度讲,调用push_back n次来初始化一个空的vector,不能超过n的常数倍。
string除了提供常见的顺序容器的操作以外,还提供了其他额外的操作。这些操作的大部分,或者提供c风格字符串和string字符串之间的相互转换,或者,增加使用下标来代替迭代器的版本。
string库定义了大量函数,这些函数都有相同的格式。初次阅读本段,可能有点烦,因此读者可以快速浏览它。一旦你知道什么操作是可用的之后,你可以再次返回寻找这些操作的细节。
除了3.2.1节 介绍的构造器以外,string还有其他相同顺序容器的构造器,见表9.3. string类型还支持表9.11中其他三个构造函数。
这些构造函数,带有一个string或者const char* ,还带有一个额外的参数(可选参数),用于指定由多少字符需要复制。当我们传递一个string,也可以指定从这个string的某个位置开始复制。
const char *cp = “Hello World!!!”;//空字符结尾
char noNull = {‘H’,’i’}; //非空字符结尾
string s1(cp); // 复制cp中空字符之前的所有字符到s1中,s1 = “Hello World!!!”
string s2(noNull,2); //从no_null中复制两个字符;s2 == “Hi”
string s3(noNull); //行为未知:noNull不是以空字符结尾的
string s4(cp+6,5); //复制5个字符,从cp【6】开始; s4 == “World”
string s5(s1,6,6); //复制5个字符,从s1【6】kaishi ,s5 == “World”
string s6(s1,6); //复制从s1【6】开始,直到结尾,s6 == “World!!!”
string s7(s1,6,20); //正确,复制只会到s1的结尾处, s7 == “World!!!”
string s8(s1,16); //抛出,out_of_range异常
通常,当我们从cons char*中创建一个string对象时,这个指针指向的字符数组必须是空字符结尾。因为复制字符,直到遇到空字符结束。如果我们传递一个数字,则没有必要以空字符结尾了。如果既没有传递数字,也没有空字符结尾,,或者,给的数字超出了数组的实际大小,那么这些的行为都是未知的。
当我们复制string的时候,我们可以提供一个额外的开始位置和一个数字。开始位置必须是小于或者等于给定字符串的大小。如果开始位置大于给定字符串的大小,这个构造器就会抛出一个out_of_range的异常。当传递一个数字的时候,表示有多少的字符需要被复制,从给定的位置开始算起。无论你传递的数字有多大,标准库最多拷贝string的大小这么多的字符,不会超过这个大小。
substr函数返回原始字符串的子串的副本。可以传递substr一个开始位置,和数字。
string s(“hello world”);
string s2 = s.substr(0,5); //s2 = hello
string s3 = s.substr(6);; //s3 = word
string s4 = s.substr(6,11); //s4 = word
string s5 = s.substr(12); //抛出out_of_range 异常
string 类型支持顺序容器的赋值操作,还有assign,insert,和erase操作。同时,他还定义了额外的insert和erase版本。
除了insert和erase的迭代器版本以外,string还提供了一个索引的版本。这个索引表示erase开始的位置,或者insert插入之前的未知:
s.insert(s.size(),5,’!’);//插入五个感叹号,在s的末尾。
s.erase(s.size()-5,5);//擦除s的最后五个元素
string库还提供了c风格字符串版本的insert和assign函数。例如,可以使用空字符结尾的c风格字符串,传递给insert和assign函数:
const char *cp = “Stately, plump Buck”;
s.assign(cp,7); // s == “Stately”
s.insert(s.size(),cp+7); //s== “Stately, plump Buck”
首先调用assign代替s的内容。赋值到s中的内容,从cp指向的位置开始。我们要赋值的字符,必须小于或者等于cp指向的字符数组的个数。
当调用insert时,表明我们想在s.size()之前插入字符。在这个例子中,从cp后7位开始复制字符串,直到遇到空字符。
还可以向insert和assign中插入其他string:
string s = “some string”,s2 = “som other string”;
s.insert(0,s2); //插入s2的副本,在0位置之前
s.insert(0,s2,0,s2.size());//从s2[0]开始,插入s2.size()个字符。
append和replace函数
string类还定义了另外两个成员,append和replace,这两个也可以改变string的内容。表9.13总结了这些函数。append函数,是insert函数的一个简化版:
string s(“C++ Primer”),s2 = s; //初始化s和s2为C++ Primer
s.insert(s.size(),” 4th Ed.”); //s == “C++ Primer 4th Ed.”
s2.append(“ 4th Ed.”); //等价于将4th Ed.添加在s2的末尾; s== s2
replace函数则是erase和insert的简化版
s.erase(11,3); //s == “C++ Primer Ed.”
s.insert(11, “5th”); // s == “C++ Prmier 5th Ed.”
s2.replace(11,3 ,“5th”); //等价于 s == s2
s.replace(11,3,”Fifth”); //s == “C++ Primer Fifth Ed.”
这个调用中,我们删除了三个字符,但是插入了5个字符。
改变string的多个重载函数
表9.13中的append,assign,insert和replace函数有几个重载函数。根据需要增加什么样的字符,以及字符串的什么部分需要改变,这些函数的参数有不同的版本,幸运的是,这些函数都有相同的接口。
assign 和append函数不需要指定string的哪部分被改变。assign总是改变整个内容,而append总是增加到字符串的末尾。
replace函数提供两种方式来指定需要被移除的字符。一种是指定起始位置和长度,另外一种是通过指定迭代器的范围。insert函数提供两种方式来指定插入的位置:一种是index,另外一种是迭代器。在每种插入方式中,插入的元素都是指定位置之前的元素。
有几种方式可以指定增加到string中的字符。新的字符,可以来自于另一个string,还可以来自于字符指针,也可以来自于大括号括起来的字符列表,或者一个字符和一个数字。当字符来自于string或者字符指针的时候,还可以传递一个额外的参数用于控制,需要复制多少字符。
并不是每一个函数都支持这些参数的版本,例如,insert就没有,传递索引和初始化列表的版本,相似的,如果我们想要使用迭代器来表明插入的位置,那么新字符就不能是字符串指针。
string类提供了6个不同的搜索函数,每个搜索函数都有4个重载版本。表9.14列出了每个搜索函数和他的参数。这些搜索函数返回一个类型为string::size_type的值,这个值表示匹配的位置的索引。如果没有匹配的,则返回一个静态成员叫做string::npos。标准库定义npos为一个类型为string::size_type,初始值为-1的值。因为npos是一个无符号的类型,因此npos的初始值是一个string的最大的可能值。
警告:
string的搜索函数返回string::size_type类型,他是一个无符号的类型。因此,使用int或者其他有符号的类型保存这些函数返回的值,是一个不好的编程习惯。
find函数做最简单的搜索,他搜寻参数,并且返回匹配到的第一个索引,如果没有匹配到则返回npos。
string name(“AnnaBelle”);
auto pos1 = name.find(“Anna”); pos1 == 0
返回0,Anna子串在AnnaBelle中被发现。
搜索是大小写敏感的。当查找string中查找某个值时,如下:
string lowercase(“annabelle”);
pos1 = lowercase.find(“Anna”); //pos1 == npos
上面的代码将使得pos1的值为npos。因为Anna并不能在annabelle中匹配到。
一个更加复杂的问题是:寻找一个字符串中的任意一个字符,例如:下面查找名字中的第一个数字:
string number(“0123456789”),name(“r2d2”);
auto pos = name.find_first_of(numbers);
还可以使用find_first_not_of去找寻,参数里面没有的第一个字符。例如,为了找到一个字符串中第一个非数字的索引,我们可以如下写:
string dept(“03714p3”);
auto pos = dept.find_first_not_of(numbers);
指定从何处开始搜寻
可以给find函数传递一个可选的开始位置。这个可选的开始位置表明了从哪一个位置开始搜索。默认情况下,这个位置为0. 一种常见的编程是:使用这个可选的参数,去循环查找一个字符串里面的所有匹配:
string::size_type pos =0;
while((pos=name.find_first_of(numbers,pos))!=string::npos){
cout << “found number at index: ”<< pos
<< “ element is ” << name[pos] << endl;
++pos;
}
在while中的循环条件语句,将pos赋值为第一个查找到的位置,并且从pos表示的当前位置开始查找。只要find_first_of返回一个有效的index,我们就打印当前的结果,并且增加pos。
如果不增加pos,循环永远不会终止。要明白这个问题,则将这个自增语句去掉,看看会发生什么。它会在第二次循环中从pos开始处查找,并且马上返回pos,因为pos处就是匹配到的地方。这样会一直循环,永不终止。
反向搜索
迄今为止我们使用的find,是从左向右进行搜索。标准库还提供了相似的函数,用于从右向左进行搜索。rfind函数搜索最后,即,子字符串最靠右的出现位置:
string river(“Mississippi”);
auto first_pos = river.find(“is”); //返回1
auto last_pos = river.rfind(“is”);//返回4
find返回1,表示is的第一个位置。而rfind返回4,表明is的最后一个位置。
find_last函数跟find_first函数的行为相似,它返回最后一个匹配,而不是第一个:
find_last_of 返回匹配给定字符串中任意一个字符的最后一个字符。
find_last_not_of 返回不匹配给定字符串中的任意一个字符的最后一个字符。
这些函数都带有一个可选的参数,这个参数用于表明从字符串的那个位置开始进行搜索。
除了关系运算符,string库还提供了一组跟c库的strcmp函数类似的函数。跟strcmp一样,s.compare根与参数字符串相比较,分别返回0,正数,负数,分别表示等于,大于,小于。
表9.15,由六个compare函数。基于参数是两个字符串比较,还是一个字符串和一个字符数组比较,相应的参数会有不同。在这两种情况下,都可以比较整个字符串或者部分字符串。
string经常包含代表数字的字符。例如,数值15,可以用两个字符表示,一个字符1,一个字符5.通常字符代表的数字,和数本身的值是不同的。数字15存储在16位的短整形中,它的二进制位表示为0000000000001111。但是,字符串15代表了两个Latin-1字符,他的二进制位为:0011000100110101.第一个字节表示了字符1,他的八进制表示为061,第二个字节代表为5,它的八进制为065
c++11 新标准引入了几个函数,这些函数可以在string和数字之间相互转换。
int I = 42;
string s = to_string(i); //将i转换成对应的字符表示
double d = stod(s); //将s转换成浮点
这里,我们调用to_string将42转换成对应的string,然后调用stod将string转换成浮点数。
第一个在string中的非空白字符,要将其转换成对应的数字值,这个字符必须是对应的数字字符:
string s2 = “pi = 3.14”;
d = stod(s2.substring(s2.find_first_of(“+-.0123456789”)));
为了调用stod,首先调用find_first_of函数,获取字符串中的第一个数字字符的位置。然后从这个位置开始,到字符串末尾,取子字符串,传递给stod函数。stod函数从第一个字符开始,直到遇到第一个非数字字符。然后将查找到的数字字符,转换成对应的双精度浮点值。
第一个空白字符必须是符号(+或者-)或者数字。字符串可以是0x后者0X开头表示的十六进制。对于转换成浮点的函数,字符串可以是点(.)开始,并且可以包含表示科学计数法的e或者E。而对于转换成整数的函数,根据进制的基值,字符串可以包含相应的字母字符,以表示超出9的部分。
注意:如果字符串不能够转换成数字。这些函数都会抛出一个invalid_argument异常。如果转成成了一个无法表述的值,将抛出一个out_of_range的异常。
除了顺序容器以外,标准库还定义了三个顺序容器的适配器:stack,queue,priority_queue.适配器在标准库中是一个通用概念。容器,迭代器,函数都有适配器。本质上,适配器是一种让一件事情看起来像另外一件事情的机制。容器适配器接收一个已经存在的容器类型,然后让其像一个不同的类型。例如,stack适配器,接收一个顺序容器(除array和forward_list),然后让这个容器的操作就像stack一样。表9.17列出了所有的容器适配器都支持的操作和类型。
定义一个适配器
每个适配器都定义了两个构造器:默认构造器创建一个空的对象。还有一个构造器需要传递一个容器,并且使用这个容器来初始化这个适配器。例如,加入deq是deque
stack<int> stk(deq); //复制deq中的元素到stk中
默认情况下,stack和queue是基于deque实现的。priority_queue基于vector实现。我们可以通过在创建适配器时候,命名一个顺序容器在第二个类型参数中。如:
//空的stack,其基于vector的
stack<string,vector<string>> str_stk;
//str_stk2 基于vector实现,并且初始化值为svec中的值。
对于给定的适配器,那些容器可以被使用,是有限制的。所有的适配器都需要增加和移除元素的能力。因此,适配器不能构造array之上。同样的,也不能基于forward_list来构造,因为所有的适配器都需要增加,删除,和访问最后一个元素的能力。stack仅需要push_back,pop_back,back操作,因此可以使用任何剩下的容器。queue适配器,需要back,push_back,front和push_front,因此它可以基于list或者deque,但是不能基于vector。priority_queue除了front,push_back,pop_back以外还需要随机访问的能力,因此可以基于vector后者deque,但是不能是list。
stack适配器
stack类型定义在stack头文件中。表9.18列出了stack支持的操作。下面的程序说明了stack的使用:
stack<int> intStack; //空的stack
for(size_t ix = 0;ix!= 10;++ix)
intStack.push(ix);
while(!intStack.empty()){
int value = intStack.top();
intStack.pop();
}
讲解如下:
stack<int> intStack; //空的stack
定义intStack为一个空的stack,这个stack用于保存整数元素。for循环添加十个元素。这十个元素从0开始。while循环遍历整个stack,获取top值,并将其弹出,直到整个stack为空。
每个容器适配器都定义了自己的操作。只能使用适配器自己的操作,而不能使用容器的操作。例如:
intStack.push(ix);
调用intStack的底层的deque对象的push_back函数。尽管stack是基于deque实现的,但是也不能直接访问deque的操作。不能在stack上面调用push_back;必须使用stack的操作push
queue 适配器
queue和priority_queue适配器定义在queue头文件中。表9.19 列出了支持的操作
标准库queue使用先进,先出的存储方案和策略。进入队列中的对象,被放置在队尾,对象则从队列的头部离开。FIFO的一个例子,就是:餐馆根据客人到达的先后,设置客人的位置。
priority_queue根据元素的优先级来设置。新增的元素被放置在低优先级的前面。对应的例子就是:餐馆根据客人的预约时间来排座位,而不是根据他们到达时间。默认情况下,标准库使用小于符号来决定相应的优先级。在11.2.2小节中将会学习如何覆盖这些默认操作。
本章小节 略
专业术语 略
翻译仓促,难免错误,望指正