天站网站建设企业服务中心组建方案
C++标准库 -- 顺序容器(Primer C++ 第五版 · 阅读笔记)
- 第9章 顺序容器------(持续更新)
 - 9.1、顺序容器概述
 - 9.2、容器库概览
 - 9.2.1 、迭代器
 - 9.2.2 、容器类型成员
 - 9.2.3 、begin 和 end 成员
 - 9.2.4 、容器定义和初始化
 - 9.2.5 、赋值和 swap
 - 9.2.6 、容器大小操作
 - 9.2.7 、关系运算符
 
- 9.3、顺序容器操作
 - 9.3.1 、向顺序容器添加元素
 - 9.3.2 、访问元素
 - 9.3.3 、删除元素
 - 9.3.4 、特殊的forward_list操作
 - 9.3.5 、改变容器大小
 - 9.3.6 、容器操作可能使迭代器失效
 
- 9.4、vector对象是如何增长的
 - 9.5、额外的string 操作
 - 9.5.1、构造string的其他方法
 - 9.5.2、改变string的其他方法
 - 9.5.3、string搜索操作
 - 9.5.4、compare函数
 - 9.5.5、数值转换
 
- 9.6、容器适配器
 
第9章 顺序容器------(持续更新)
所有容器类都共享公共的接口,不同容器按不同方式对其进行扩展。
这个公共接口使容器的学习更加容易—我们基于某种容器所学习的内容也都适用于其他容器。
每种容器都提供了不同的性能和功能的权衡。
9.1、顺序容器概述

下表列出了标准库中的顺序容器, 所有顺序容器都提供了快速顺序访问元素的能力。但是,这些容器在以下方面都有不同的性能折中:
- 向容器添加或从容器中删除元素的代价
 - 非顺序访问容器中元素的代价
 

 除了固定大小的 array 外,其他容器都提供高效、灵活的内存管理。
list和forward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。而且,与vector、deque和array相比,这两个容器的额外内存开销也很大.deque是一个更为复杂的数据结构。与string和vector类似,deque支持快速的随机访问。与string和vector一样,在deque的中间位置添加或删除元素的代价(可能)很高。但是,在deque的两端添加或删除元素都是很快的,与list或forward_list添加删除元素的速度相当。forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此,forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。- 对其他容器而言
size保证是一个快速的常量时间的操作。 
9.2、容器库概览
一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。即,deque定义头文件 deque中,list定义在头文件list中,以此类推。
容器类型上的操作形成了一种层次:
- 某些操作是所有容器类型都提供的。
 - 另外一些操作仅针对顺序容器、关联容器 或 无序容器。
 - 还有一些操作只适用于一小部分容器。
 
对所有容器都适用的操作:

 
 
9.2.1 、迭代器
与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。
使用左闭合范围蕴含的编程假定
[begin, end)
标准库使用左闭合范围是因为这种范围有三种方便的性质。假定begin和 end 构成一个合法的迭代器范围,则
- 如果
begin与end相等,则范围为空 - 如果
begin与end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素 - 我们可以对begin递增若干次,使得 
begin==end 
这些性质意味着我们可以像下面的代码一样用一个循环来处理一个元素范围而这是安全的:
while (begin != end) {*begin = val;	//正确:范围非空,因此begin指向一个元素++begin;		//移动迭代器,获取下一个元素
}
 
9.2.2 、容器类型成员
为了使用这些类型,我们必须显式使用其类名,使用作用域运算符来说明我们希望使用list<string>类的iterator成员及vector<int>类定义的difference_type:
// iter是通过list<string>定义的一个迭代器类型
list<string>::iterator iter;
// count是通过vector<int>定义的一个difference_ type类型
vector<int>::difference_type count;
 
9.2.3 、begin 和 end 成员
begin和end操作生成指向容器中第一个元素和尾元素之后位置的迭代器。
begin和end有多个版本:带r的版本返回反向迭代器;- 以
c开头的版本则返回const迭代器: - 不以
c开头的函数都是被重载过的。也就是说,实际上有两个名为begin的成员。一个是const成员,返回容器的const_iterator类型。另一个是非常量成员,返回容器的iterator类型。rbegin、end和rend的情况类似。 
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
 
当我们对一个非常量对象调用这些成员时,得到的是返回 iterator 的版本。只有在对一个const对象调用这些函数时,才会得到一个const版本。
与const指针和引用类似,可以将一个普通的iterator转换为对应的const_iterator,但反之不行。
我们可以用以支持auto与begin和 end 函数结合使用。过去,没有其他选择,只能显式声明希望使用哪种类型的迭代器:
//是iterator还是const_ iterator依赖于a的类型
auto it7 = a.begin();//仅当a是const时,it7是const_iterator
auto it8 = a.cbegin();// it8是const_iterator,不管容器的类型是什么
 
当不需要写访问时,应使用
cbegin和cend。
9.2.4 、容器定义和初始化
每个容器类型都定义了一个默认构造函数。
- 除
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());
 
当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。
与顺序容器大小相关的构造函数
除了与关联容器相同的构造函数外,顺序容器(array除外)还提供另一个构造函数,它接受一个容器大小和一个(可选的)元素初始值。如果我们不提供元素初始值则标准库会创建一个值初始化器:
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
 
只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
标准库 array具有固定大小
与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:
array<int, 10>::size_type i;		//数组类型包括元素类型和大小
array<int>::size_type j;			//错误:array<int>不是一个类型
 
由于大小是array类型的一部分,array不支持普通的容器构造函数。
如果我们对array进行列表初始化,初始值的数目必须等于或小于array的大小。
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
 
值得注意的是,虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但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.2.5 、赋值和 swap
下表中列出的与赋值相关的运算符可用于所有容器。赋值运算符将其左边容器中的全部元素替换为右边容器中元素的拷贝:
c1 = c2;		//将c1的内容替换为c2中元素的拷贝
c1 = {a,b,c};	//赋值后,c1大小为3
 

与内置数组不同,标准库array类型允许赋值。赋值号左右两边的运算对象必须具有相同的类型;
array<int, 10> al = {0,1,2,3,4,5,6,7,8,9};
array<int,10> a2 = {0];//所有元素值均为0al = a2;//替换a1中的元素
a2 = {0};//错误:不能将一个花括号列表赋予数组
 
由于右边运算对象的大小可能与左边运算对象的大小不同,因此 array类型不支持assign,也不允许用花括号包围的值列表进行赋值。
使用assign(仅顺序容器)
赋值运算符要求左边和右边的运算对象具有相同的类型。它将右边运算对象中所有元素拷贝到左边运算对象中。
顺序容器(array除外)还定义了一个名为assign的成员,允许我们从一个 不同但相容 的类型赋值,或者从容器的一个子序列赋值。
assign操作用参数所指定的元素(的拷贝)替换左边容器中的所有元素。
例如,我们可以用assgin实现将一个vector中的一段char*值赋予一个list中的string:
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; //错误:容器类型不匹配//正确:可以将const char*转换为string
names.assign(oldstyle.cbegin(), oldstyle.cend());
 
由于其旧元素被替换,因此传递给
assign的迭代器不能指向调用assign的容器。
assign的第二个版本接受一个整型值和一个元素值。它用指定数目且具有相同给定值的元素替换容器中原有的元素:
//等价于slist1.clear ();
//后跟slist1.insert(slist1.begin(), 10, "Hiya ! ");
list<string> slist1(1);			// 1个元素,为空string
slist1.assign(10,"Hiya! ");	//10个元素,每个都是“Hiya !”
 
使用swap
swap 操作交换两个相同类型容器的内容。调用swap之后,两个容器中的元素将会交换:
vector<string> svec1(10); // 10个元素的vector
vector<string> svec2 (24);// 24个元素的vector
swap (svecl, 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是一个好习惯。 
9.2.6 、容器大小操作
除了一个例外,每个容器类型都有三个与大小相关的操作。
- 成员函数
size返回容器中元素的数目; empty当size为0时返回布尔值true,否则返回false;max_size返回一个大于或等于该类型容器所能容纳的最大元素数的值。forward_list支持max_size和empty,但不支持size,原因我们将在下一节解释。
9.2.7 、关系运算符
每个容器类型都支持相等运算符(==和!=);除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。
关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。
比较两个容器实际上是进行元素的逐对比较。这些运算符的工作方式与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 };vl < v2 // true; v1和v2在元素[2]处不同:v1[2]小于等于v2[2]
v1 < v3 // false;所有元素都相等,但v3中元素数目更少
vl == v4 // true;每个元素都相等,且v1和v4大小相同
vl == v2 // false;v2元素数目比v1少
 
容器的关系运算符使用元素的关系运算符完成比较
只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。
容器的相等运算符实际上是使用元素的==运算符实现比较的,而其他关系运算符是使用元素的<运算符。
- 如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。
 
9.3、顺序容器操作
9.3.1 、向顺序容器添加元素
顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系到了元素如何存储、访问、添加以及删除。上面介绍了所有容器都支持的操作。本章剩余部分将介绍顺序容器所特有的操作。
- 除
array外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小。下表列出了向顺序容器(非array)添加元素的操作。 

当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。在一个
vector或string的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。
- push_back:除
array和forward_list之外,每个顺序容器(包括string类型)都支持push_back; - push_front:除了
push_back,list、forward_list和deque容器还支持名为push_front内类似操作。此操作将元素插入到容器头部; - insert:
insert成员提供了更一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素。vector、deque、list和string都支持insert成员。forward_list提供了特殊版本的insert成员,我们将在后面介绍。 
迭代器可以指向容器中任何位置,包括容器尾部之后的下一个位置,所以迭代器可能指向容器尾部之后不存在的元素的位置,而且在容器开始位置插入元素是很有用的功能,所以
insert函数将元素插入到迭代器所指定的位置之前。
-  
使用
insert的返回值- 通过使用
insert的返回值,可以在容器中一个特定位置反复插入元素: 
 - 通过使用
 
list<string> lst;
auto iter = lst.begin();
while (cin >> word)iter = lst.insert(iter, word); //等价于调用push_front
 
- emplace: 
- 新标准引入了三个新成员一
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函数在容器中直接构造元素。传递给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);
 
9.3.2 、访问元素
包括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返回的迭代器来获得尾元素的引用。

访问成员函数返回的是引用
在容器中访问元素的成员函数(即,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中的元素
}
 
下标操作和安全的随机访问
下标运算符并不检查下标是否在合法范围内。如果我们希望确保下标是合法的,可以使用at成员函数。at成员函数类似下标运算符,但如果下标越界,at 会抛出一个out_of_range异常:
vector<string> svec;		//空vector
cout << svec[0];			//运行时错误:svec中没有元素!
cout << svec.at(0);			//抛出一个out_of_range异常
 
9.3.3 、删除元素
与添加元素的多种方式类似,(非array)容器也有多种删除元素的方式。下表列出了这些成员函数。

pop_front和pop_back返回void。如果你需要弹出的元素的值,就必须在执行弹出操作之前保存它:
while (!ilist.empty(){process(ilist.front()); //对ilist的首元素进行一些处理ilist.pop_front() ;//完成处理后删除首元素
}
 
9.3.4 、特殊的forward_list操作
当添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生改变。为了添加或删除一个元素,我们需要访问其前驱,以便改变前驱的链接。
由于这些操作与其他容器上的操作的实现方式不同,forward_list并未定义insert、emplace和erase,而是定义了名为insert_after、emplace after和erase_after的操作。
forward_list也定义了before_begin,它返回一个首前( off-the-beginning)迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素“之后”添加或删除元素(亦即在链表首元素之前添加删除元素)。

9.3.5 、改变容器大小
如下表所描述,我们可以用resize来增大或缩小容器,与往常一样,array不支持resize。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部:

9.3.6 、容器操作可能使迭代器失效
向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器 失效。
- 一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题。
 
在向容器添加元素后:
- 如果容器是
vector或string,且存储空间被重新分配,则指向容器的迭代器指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效; - 对于
deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。 - 对于 
list和forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。 
当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效,这应该不会令人惊讶。毕竟,这些元素都已经被销毁了。
当我们删除一个元素后:
- 对于
list和forward_list,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效。 - 对于 
deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。 - 对于
vector和string,指向被删元素之前元素的迭代器、引用和指针仍有效注意:当我们删除元素时,尾后迭代器总是会失效。 
由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对
vector、string和deque尤为重要。
编写改变容器的循环程序
添加/删除 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; //向前移动迭代器,跳过当前元素以及插入到它之前的元素}elseiter = vi.erase(iter);//删除偶数元素//不应向前移动迭代器,iter指向我们删除的元素之后的元素
}
 
在调用erase后,不必递增迭代器,因为erase返回的迭代器已经指向序列中下一个元素。调用insert后,需要递增迭代器两次。记住,insert在给定位置之前插入新元素,然后返回指向新插入元素的迭代器。因此,在调用insert后,iter指向新插入元素,位于我们正在处理的元素之前。我们将迭代器递增两次,恰好越过了新添加的元素和正在处理的元素,指向下一个未处理的元素。
不要保存end返回的迭代器
如果在一个循环中插入/删除 deque、string或 vector中的元素,不要缓存end返回的迭代器。
必须在每次插入操作后重新调用end(),而不能在循环开始前保存它返回的迭代器:
//安全的方法:在每个循环步添加/删除元素后都重新计算end
while (begin != v.end(){//做一些处理++begin; //向前移动begin,因为我们想在此元素之后插入元素begin = v.insert(begin, 42);//插入新值++begin; //向前移动begin,跳过我们刚刚加入的元素
}
 
9.4、vector对象是如何增长的
vector在每次重新分配内存空间时都要移动所有元素,但使用此策略后,其扩张操作通常比list和 deque还要快。
管理容量的成员函数
如下表所示,vector和string类型提供了一些成员函数,允许我们与它的实现中内存分配部分互动。

- 只有当需要的内存空间超过当前容量时,
reserve调用才会改变vector的容量。如果需求大小大于当前容量,reserve至少分配与需求一样大的内存空间(可能更大)。 - 在新标准库中,我们可以调用
shrink_to_fit来要求deque、vector或string退回不需要的内存空间。此函数指出我们不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求。也就是说,调用shrink_to_fit也并不保证一定退回内存空间。 - 容器的
size是指它已经保存的元素的数目; capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
vector的实现采用的策略似乎是在每次需要分配新内存空间时将当前容量翻倍。
每个
vector实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。
9.5、额外的string 操作
除了顺序容器共同的操作之外,string类型还提供了一些额外的操作。这些操作中的大部分要么是提供string类和C风格字符数组之间的相互转换,要么是增加了允许我们用下标代替迭代器的版本。
9.5.1、构造string的其他方法
除了已经介绍过的构造函数,以及与其他顺序容器相同的构造函数外,string类型还支持另外三个构造函数,如下表:

- 通常当我们从一个 
const char*创建string时,指针指向的数组必须以空字符结尾,拷贝操作遇到空字符时停止。 - 如果我们还传递给构造函数一个计数值,数组就不必以空字符结尾。
 - 如果我们未传递计数值且数组也未以空字符结尾,或者给定计数值大于数组大小,则构造函数的行为是未定义的。
 
const char *cp = "Hello World!!!";		//以空字符结束的数组
char noNull[] = {'H','i'};				//不是以空字符结束
string s1(cp);			//拷贝cp中的字符直到遇到空字符;s1 == "Hello World!!!"
string s2(noNull, 2);			//从noNull拷贝两个字符;s2 == "Hi"
string s3(noNull);				//未定义:noNull不是以空字符结束
string s4(cp + 6, 5);			//从cp[6]开始拷贝5个字符;s4 == "World"
string s5(s1, 6, 5);			//从s1[6]开始拷贝5个字符;s5 == "World"
string s6(s1, 6);				//从s1[6]开始拷贝,直至s1末尾;s6 == "World!!!"
string s7(s1, 6, 20);			//正确,只拷贝到s1末尾;s7 == "World!!!"
string s8(s1, 16);				//抛出一个out_of_range异常
 
substr 操作

9.5.2、改变string的其他方法
string类型支持顺序容器的赋值运算符以及assign、insert和erase操作。除此之外,它还定义了额外的insert和erase版本。
- 接受下标的版本:下标指出了开始删除的位置,或是
insert到给定值之前的位置: 
s.insert(s.size(), 5, '!');		//在s末尾插入5个感叹号
s.erase(s.size() - 5, 5);		//从s删除最后5个字符
 
- 接受C风格字符数组的
insert和assign版本:我们可以将以空字符结尾的字符数组insert到或assign给一个string: 
const char *cp = "stately, plump Buck";
s.assign(cp, 7);			// s=="stately"
s.insert(s.size(), cp + 7);	// s == "Stately, plump Buck"
 
- 我们也可以指定将来自其他
string或子字符串的字符插入到当前string中或赋予当前string: 
string s = "some string", s2 = "some other string";
s.insert(0, s2);//在s中位置0之前插入s2的拷贝
//在s[0]之前插入s2中s2[0]开始的s2.size ()个字符
s.insert(0, s2, 0, s2.size());
 

 
append 和replace函数
string类定义了两个额外的成员函数: append和replace,这两个函数可以改变string的内容。
append操作是在string末尾进行插入操作的一种简写形式:
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的一种简写形式:
//将"4th"替换为"5th"的等价方法
s.erase (11, 3);// s == "C++ Primer Ed."
s.insert(11, "5th");// s == "C++ Primer 5th Ed."
//从位置11开始,删除3个字符并插入"5th"s2.replace(11, 3, "5th");//等价方法:s == s2
 
改变string的多种重载函数
 表9.13列出的append、assign、insert和 replace函数有多个重载版本,这些函数有共同的接口。
assign和append函数无须指定要替换string中哪个部分:assign总是替换string中的所有内容,append总是将新字符追加到string末尾。replace函数提供了两种指定删除元素范围的方式。可以通过一个位置和一个长度来指定范围,也可以通过一个迭代器范围来指定。insert函数允许我们用两种方式指定插入点:用一个下标或一个迭代器。在两种情况下,新元素都会插入到给定下标(或迭代器)之前的位置。
并不是每个函数都支持所有形式的参数。例如,
insert就不支持下标和初始化列表参数。类似的,如果我们希望用迭代器指定插入点,就不能用字符指针指定新字符的来源。
9.5.3、string搜索操作
string类提供了6个不同的搜索函数,每个函数都有4个重载版本。表9.14描述了这些搜索成员函数及其参数。
- 每个搜索操作都返回一个
string::size_type值,表示匹配发生位置的下标。 - 如果搜索失败,则返回一个名为
string::npos的static成员。标准库将npos定义为一个const stringsize_type类型,并初始化为值-1。由于npos是一个unsigned类型,此初始值意味着npos等于任何string最大的可能大小。 
find函数完成最简单的搜索。它查找参数指定的字符串,若找到,则返回第一个匹配位置的下标,否则返回npos:
//子字符串"Anna"在"AnnaBelle"中第一次出现的下标
string name ("AnnaBelle");
auto pos1 = name.find("Anna"); // pos1 == 0//搜索(以及其他string操作)是大小写敏感的
string lowercase("annabelle");
pos1 = lowercase.find("Anna"); // posl ==npos
 
一个更复杂一些的问题是查找与给定字符串中任何一个字符匹配的位置
//定位name 中的第一个数字;
string numbers("0123456789"), name ( "r2d2");
//返回1,即,name中第一个数字的下标
auto pos = name.find_first_of(numbers);//搜索第一个不在参数中的字符
string dept("03714p3");//返回5——字符'p'的下标
auto pos = dept.find_first_not_of(numbers);
 

 
9.5.4、compare函数
除了关系运算符外,标准库 string 类型还提供了一组compare函数,这些函数与C标准库的strcmp函数很相似。类似 strcmp ,根据s是等于、大于还是小于参数指定的字符串,s.compare返回0、正数或负数。

9.5.5、数值转换
字符串中常常包含表示数值的字符。例如,我们用两个字符的
string表示数值15——字符1’后跟字符’5’。一般情况,一个数的字符表示不同于其数值。数值15如果保存为16位的short类型,则其二进制位模式为0000000000001111,而字符串"15"存为两个Latin-1编码的char,二进制位模式为0011000100110101。第一个字节表示字符’1’,其八进制值为061,第二个字节表示’5’,其Latin-1编码为八进制值065。
新标准引入了多个函数,可以实现数值数据与标准库string之间的转换:
int i =42;
string s = to_string(i); //将整数i转换为字符表示形式
double d = stod(s);//将字符串s转换为浮点数
 
要转换为数值的string中第一个非空白符必须是数值中可能出现的字符:
string s2 = "pi = 3.14";
//转换s中以数字开始的第一个子串,结果d = 3.14
d = stod(s2.substr(s2.find_first_of("+-.0123456789")));
 

如果
string不能转换为一个数值,这些函数抛出一个invalid_argument异常。如果转换得到的数值无法用任何类型来表示,则抛出一个out_of_range异常。
9.6、容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。
- 适配器( 
adaptor)是标准库中的一个通用概念。 

容器、迭代器和函数都有适配器。

- 本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。
 - 一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
 
例如,
stack适配器接受一个顺序容器(除array或forward_list外),并使其操作起来像一个stack一样。下表列出了所有容器适配器都支持的操作和类型。

定义一个适配器
每个适配器都定义两个构造函数:
- 默认构造函数创建一个空对象;
 - 接受一个容器的构造函数铂贝该容器来初始化适配器。
 
例如,假定deq是一个 deque<int>,我们可以用deq来初始化一个新的stack,如下所示:
stack<int> stk(deq);//从deq拷贝元素到stk
 
默认情况下,
stack和queue是基于deque实现的,priority_queue是在vector之上实现的。
我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
//在vector上实现的空栈
stack<string, vector<string>> str_stk;
//str_stk2在vector上实现,初始化时保存svec的拷贝
stack<string, vector<string>> str_stk2(svec) ;
 
对于一个给定的适配器,可以使用哪些容器是有限制的。所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能构造在 array 之上。类似的,我们也不能用forward_list来构造适配器,因为所有适配器都要求容器具有添加、删除以及访问尾元素的能力。
stack只要求push_back、pop_back和back操作,因此可以使用除array和forward_list之外的任何容器类型来构造stack。queue适配器要求back、push_back、front和push_front,因此它可以构造于list或deque之上,但不能基于vector构造。priority_queue除了front、push_back和pop_back操作之外还要求随机访问能力,因此它可以构造于vector或deque之上,但不能基于list构造。
栈适配器
stack类型定义在stack头文件中。下表列出了stack所支持的操作。下面的程序展示了如何使用stack:
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();					//弹出栈顶元素,继续循环, 返回值为void
}
 

每个容器适配器都基于底层容器类型的操作定义了自己的特殊操作。我们只可以使用适配器操作,而不能使用底层容器类型的操作。
队列适配器
queue和priority_queue 适配器定义在queue头文件中。下表列出了它们所支持的操作。

 
 priority_queue 允许我们为队列中的元素建立优先级。
- 新加入的元素会排在所有优先级比它低的已有元素之前。饭店按照客人预定时间而不是到来时间的早晚来为他们安排座位,就是一个优先队列的例子。
 - 默认情况下,标准库在元素类型上使用
<运算符来确定相对优先级。 
注:仅供学习参考,如有不足,欢迎指正!!!
