一个容器就是一些特定类型对象的集合,顺序容器为程序员提供控制元素存储和访问顺序的能力。这种顺序不依赖元素的值,而是与元素加入容器时的位置相对应。
扩展:在以后介绍有序和无序关联容器,根据关键字的键值来存储元素。
标准库还提供三种容器适配器,分别为容器操作定义了不同的接口,来与容器类型适配。
1.顺序容器概述:
表中列出标准库中的顺序容器,所有顺序容器都提供了快速访问元素的能力。但是这些容器在以下方面都有不同的性能折中:
●向容器添加或从容器中删除容器的代价
●非顺序访问容器中元素的代价
vector //可变大小数组,支持快速随机访问,在尾部之外的位置插入或删除元素 可能很慢 deque //双端队列,支持快速随机访问,在头尾位置插入或删除元素很快 list //双向链表,支持双向顺序访问,在list任何位置插入或删除元素都很快 forward_list //单向链表,只支持单向顺序访问,在任何位置插入或删除元素都很快 array //固定大小数组,支持快速随机访问,不能添加或删除元素 string //与vector类似,但专用于保存字符。随机访问快。在尾部删除/插入速度快。 //list与forward_list不支持元素的随机访问
1.除了固定大小的array之外,其他容器都提供高效、灵活的内存管理。我们可以添加和删除元素,扩张和收缩容器的大小。容器保存元素的策略对容器操作的效率有着固有的有时是重大的影响,某些情况下存储策略还会影响特定容器是否支持特定操作。
例如:string和vector将元素保存在连续的内存空间中。由于元素是连续存储的,由元素的下标来计算其地址是非常快速的。但是,两种容器的中间位置添加或删除元素就会非常耗时:在一次插入或删除操作后,需要移动插入/删除位置之后的所有元素,来保持连续存储。而且,添加一个元素有时可能还需要分配额外的存储空间。在这种情况下,每个元素都必须移动到新的存储空间中。
2.list和forward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。而且,与vector、deque 和array,相比,这两个容器的额外内存开销也很大。
3.deque是一个更为复杂的数据结构。与string和vector类似,deque 支持快速的随机访问。与string和vector一样,在deque的中间位置添加或删除元素的代价(可能)很高。但是,在deque的两端添加或删除元素都是很快的,与list或forward_ list 添加删除元素的速度相当。
4.forward_list 和array是新C++标准增加的类型。与内置数组相比,array是一种更安全、更容易使用的数组类型。与内置数组类似,array对象的大小是固定的。因此,array不支持添加和删除元素以及改变容器大小的操作。forward_list 的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此,forward_list 没有size 操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。
5.选择容器的基本原则:
①除非你有更好的理由选择其他容器,否则应该会用vector
②如果程序有很多小的元素,且空间额外开销很重要,则不要使用list或forward_list
③如果程序要求随机访问元素,应使用vector或deque
④如果程序要求在容器中间插入或删除元素,应使用list或forward_list
⑤如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque
⑥如果程序只在读入时需要在容器中间位置插入元素,随后需要随机访问元素:首先确定是否真的需要在容器中间位置添加元素,例如有时可以通过sort重排元素来避免在中间位置插入元素;其次若必须在中间位置插入元素,则考虑在输入阶段使用list,输入完成后,将list中的内容拷贝到一个vector中。
⑦如果程序既需要随机访问元素,又需要在容器中间位置插入元素,则取决于list和forward_list中访问元素与vector或deque中插入/删除元素的相对性能。
6.容器均定义为模板类,对大多数但不是所有容器,我们需要额外提供元素类型信息。
7.顺序容器几乎可以保存任意类型的元素,甚至可以定义一个容器其元素的类型是另一个容器。虽然我们可以在容器中保存几乎任何类型,但某些容器操作对元素类型有其自己的特殊要求。
------------------------------------------------------------------------------------------------分割线-------------------------------------------------------------------------------------------------------------------------------------------
2.容器库概览
●某些操作是所有容器类型都提供的。
●另外一些操作仅针对顺序容器、关联容器或无序容器。
●还有一些操作只适用于一小部分容器。
一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。即
容器均定义为模板类,我们需要额外提供元素类型信息:
list<Sales_data> //保存Sales_data对象的List类型
deque<double> //保存double的deque
对容器可以保存的元素类型的限制
顺序容器几乎可以保存任意类型的元素。特别是,我们可以定义一个容器,其元素的类型是另一个容器:
vector<vector<string>> lines; //vector的vector
//此处lines是一个vector,其元素类型是string的vector
//ps:较旧的编译器可能需要在两个尖括号之间添加空格,例如:vector<vector<string> >
某些类没有默认构造函数,我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数:
//假定noDefault是一个没有默认构造函数的类型 vector<noDefault> v1(10,init); //正确:提供了元素初始化器 vector<noDefault> v2(10); //错误:必须提供一个元素初始化器
当后面介绍容器操作时,我们还会注意到每个容器操作对元素类型的其他限制
容器操作(通用操作)
容器操作(通用操作) | |
类型别名 |
|
iterator | 此容器类型的迭代器类型 |
const_iterator | 只能读不能修改的迭代器类型 |
size_type | 无符号整数类型,足够保存此类容器类型最大可能容器的大小 |
difference_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型;与value_type&含义相同 |
const_reference | 元素const的左值类型(即,const value_type&) |
构造函数 | |
C c; | 默认构造函数,构造空容器(arryay) |
C c1(c2); | 构造c2的拷贝c1 |
C c(b,e); | 构造c,将迭代器b和e指定范围内的元素拷贝到c(array不支持) |
C c{a,b,c,d.....}; | 列表初始化c |
赋值与swap | |
c1=c2 | 赋值 |
c1={a,b,c,d,e....} | 列表赋值(不适用于array) |
a.swap(b) | 交换a和b的元素 |
swap(a,b); | 等价于a.swap(b) |
大小 | |
c.size() | c中元素的数量(不支持forward_list) |
c.max_size() | c可保存的最大元素数量 |
c.empty() | c是否存储了元素 |
添加/删除元素(不适用于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.end() | 返回const_iterator |
反向容器的额外成员(不支持forward_list) | |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin(),c.rend() | 返回指向c的尾元素和首元素之前位置的迭代器 |
c.crbegin(),c.crend() | 返回const_reverse_iterator |
2.1 迭代器
与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。
其中有一个例外不符合公共接口的特点—forward_list迭代器不支持递减运算符(–)
迭代器范围(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递增一次,我们确定循环最终会结束。
2.2 容器类型成员
反向迭代器
反向迭代器就是反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义发生颠倒。
例如:对一个反向迭代器进行++操作,会得到上一个元素。
类型别名
通过类型别名,我们可以在不了解容器中元素类型的情况下使用它。 这些元素相关的类型别名在泛型编程中非常有用.
// iter 是通过list<string>定义的一个迭代器类型
list<string>::iterator iter;
// count是通过vector<int>定义的一个difference_ type 类型
vector<int>::difference_type count;
2.3begin 和 end成员
begin 和 end 生成指向容器中第一个元素与尾元素之后位置的迭代器。这两个迭代器最常见的用途是形成一个包含容器中所有元素的迭代器范围。
begin和end有多个版本:带r的的版本返回反向迭代器;以c开头的版本返回const迭代器:
//begin和end有多个版本,带r的返回反向迭代器;带c的返回const迭代器
list<string> a = {"kl", "ju", "ku"};
auto it1 = a.rbegin(); //list<string>::reverse_iterator
auto it2 = a.cbegin(); //list<string>::const_iterator
auto it3 = a.crbegin(); //list<string>::const_reverse_iterator
==当不需要写访问时,应使用cbeign和cend。==
2.4 容器定义和初始化
每个容器类型都定义了一个默认构造函数。除array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器的大小和元素初始值参数。
容器定义和初始化 | |
C c | 默认构造函数。如果C是一个array,则C中元素按默认方式初始化;否则C为空。 |
C c1(c2) | C c1 = c2 | c1初始化为c2的拷贝。c1和c2必须是相同类型。(即,它们必须是相同类型的容器,保存的元素类型相同;对于array类型,两者的大小还必须相同) |
C c{a,b,c,...} | C c = {a,b,c,...} | c初始化为初始化列表中元素的拷贝。列表中元素类型必须与c的元素类型相容。对于array类型,类表中的元素数目必须等于或小于array的大小,任何遗漏的元素都进行值初始化。 |
C c(b,e) | c初始化为迭代器b和e指定范围中的元素的拷贝。范围中元素的类型必须与c的元素类型相容 |
C seq(n) | seq包含n个元素,这些元素进行了值初始化;此构造函数是explicit的。 |
C seq(n,t) | seq包含n个初始化为值t的元素 |
将一个容器初始化为另一个容器的拷贝
将一个新容器创建为另一个容器的拷贝的方法有两种:
- 可以直接拷贝整个容器。
- (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.beign(),articles.end());
==当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。==
列表初始化
在新标准中,我们可以对一个容器进行列表初始化。
//每个容器有三个元素,用给定的初始化器进行初始化
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个string元素,每个初始化为"hi!" forward_list<int> ilist(10); //10个int元素,每个初始化为0
==只有顺序容器的构造函数才接受大小参数,关联容器不支持。==
标准库array具有固定大小
与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还必须指定容器大小:
array<string, 10>; //类型为:保存10个string的数组
为了使用array类型,我们必须同时指定元素类型和容器大小:
array<int,10>::size_type i; //数组类型包括元素类型和数组大小 array<int>::size_type j; //错误:array<int>不是一个类型
2.5 赋值和swap
下表列出的与赋值相关的运算符可用于所有容器。赋值运算符将其左边容器中的全部元素替换为右边容器中的元素的拷贝:
c1 = c2; //将c1中的内容替换为c2中元素的拷贝 c1 = {a,b,c}; //赋值后,c1大小为3
容器赋值运算 | 定义 |
---|---|
c1 = c2 | 将c1中的元素替换为c2中元素的拷贝。c1和c2必须具有相同的类型 |
c = {a,b,c,...} | 将c中的元素替换为初始化列表中元素的拷贝(array不适用) |
swap(c1,c2) | c1.swap(c2) | 交换c1和c2中的元素。c1和c2必须具有相同的类型。swap通常比从c2向c1拷贝元素快得多 |
seq.assign(b,e) | 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素。 |
seq.assign(il) | 将seq中元素替换为初始化列表il中的元素 |
seq.assign(n,t) | 将seq中的元素替换为n个值为t的元素 |
使用assign(仅顺序容器)
顺序容器定义了一个assign成员,允许我们从一个不同但相容类型赋值,或者从容器的一个子序列赋值。assign操作用参数所指定的元素的拷贝替换左边容器中的所有元素。例如,我们可以用assign实现一个vector中一段char *值赋予一个list中的string:
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; //错误:容器类型不匹配
names.assign(oldstyle.cbegin, oldstyle.cend); //正确:可以将const char*转换为string
==由于其旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器。==
使用swap
swap操作交换两个相同类型容器的内容,调用swap之后,两个容器中的元素将会交换:
vector<string> svec1(10); //10个元素的vector vector<string> svec2(24); //24个元素的vector swap(svec1, svec2);
调用swap后,svec1将包含24个元素,svec2将包含10个元素。除array外,交换两个容器内容的操作保证会很快——元素本身并未交换,swap只是交换了两个容器的内部数据结构。
==除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。==
2.6 容器大小操作
除了一个例外,每个容器类型都有三个与大小相关的操作。成员函数size返回容器中元素的数目;empty当size为0时返回布尔值true,max_size返回一个大于或等于该类型容器所能容纳最大元素的值。forward_list支持max_size和empty不支持size。
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};
v1 < v2 //true;v1[2]<v2[2]
v1 < v3 //false;所有元素都相等,但v3中元素数目更少
v1 == v4 //true;每个元素都相等,且v1和v4大小相同
v1 == v2 //false;v2元素数目比v1小
容器的关系运算符使用元素的关系运算符完成比较
容器的相等运算符实际上是使用元素的==运算符实现比较的,而其他关系运算符是使用元素的<运算符。如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算符。
vector<Sales_data> storeA,storeB;
if(storeA < storeB) //错误:Sales_data没有<运算符
==只有当其元素类型也定义了相应的比较运算符时,才可以使用关系运算符比较两个容器。==
------------------------------------------------------------------------------------------------分割线-------------------------------------------------------------------------------------------------------------------------------------------
3. 顺序容器操作
顺序容器和关联容器的不同之处在于两者组织元素的方式。
3.1 向顺序容器添加元素
除array外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除来改变容器大小
。
- 这些操作会改变容器的大小;array不支持这些操作。
- forward_list有自己专有版本的insert和emplace;forward_list不支持push_back和emplace_back。
- vector和string不支持push_front和emplace_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的元素。返回指向新添加的第一个元素的迭代器;若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插入元素会使所有指向容器的迭代器、引用和指针失效。==
使用push_back
push_back将一个元素追加到一个vector的尾部。除array和forward_list之外,每个顺序容器都支持push_back。
==当我们用一个对象来初始化容器时,或将一个对象插入到容器中,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。==
使用push_front
除了push_back,list、forward_list和deque容器还支持名为push_front的类似操作。此操作将元素插入到容器头部:
list<int> ilist;
//将元素添加到ilist开头
for (size_t i = 0; i < 4; i++) {
ilist.push_front(i); //每个元素都插入到list的新的开始位置。
}
此循环将元素0、1、2、3添加到ilist头部。每个元素都插入到list新的开始位置。即,当我们插入1时,它会被放置在0之前,2被放置到1之前,依此类推。
在容器中的特定位置添加元素
insert成员提供了更一般的添加功能,它允许我们在容器中的任意位置插入0个或多个元素。
vector<string> svec;
list<string> slist;
slist.insert(slist.begin(),"Hello!");
//vector不支持push_front,但我们可以插入到begin()之前
//警告:插入到vector末尾之外的任何位置都可能很慢
svec.insert(svec.begin, "Hello");
==将元素插入到vector、deque和string中任何位置都是合法的。然而,这样做可能很耗时。==
插入范围内元素
除了第一个迭代器参数之外,insert函数还可以接受更多的参数,这与容器构造函数类似。其中一个版本接受一个元素数目和一个值,它将指定数量的元素添加到指定位置之前,这些元素按给定值初始化:
svec.insert(svec.end,10,"Anna");
将10个元素插入到svec的末尾,并将所有元素都初始化为string "Anna"。
接受一对迭代器或一个初始化列表的insert版本将给定范围中的元素插入到指定位置之前:
vector<string> svec = {"quasi","simba","frollo","scar"};
//将svec的最后两个元素添加到slist起始位置
slist.insert(svec.begin(), svec.end() - 2, svec.end());
slist.insert(svec.end(), {"these","words","will","go","at","the","end"});
//运行时错误:迭代器表示要拷贝的范围,不能指定与目的位置相同的容器
slist.insert(slist.begin(),slist.begin(),slist.end());
如果我们传递给insert一对迭代器,它们不能指向添加元素的目标容器。
使用insert的返回值
通过使用insert的返回值,可以在容器中一个特定位置反复插入元素:
list<string> lst;
auto iter = lst.begin();
while(cin >> word) {
iter = list.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对象 c.emplace_back("978-0590353403",25,15.99);
==emplace函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。==
3.2 访问元素
包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用:
//在解引用一个迭代器或者调用front或back之前检查是否有元素
if (!svec.empty()) {
//val和val2是c中第一个元素值的拷贝
auto val = *svec.begin(), val2 = svec.front();
//val3和val4是c中最后一个元素值的拷贝
auto last = svec.end();
auto val3 = *(--last); //不能递减forward_list
auto val4 = svec.back(); //forward_list不支持
}
- 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,就像使用一个越界的下标一样,是一种严重的程序设计错误。==
访问成员函数返回的是引用
在容器中访问元素的成员函数(即,front、back、下标和at)返回的都是引用。如果容器是一个const对象,则返回值是const的引用。如果容器不是const的,则返回值是普通引用,我们可以用来改变元素的值:
if (!svec.empty()) {
svec.front() = "hi"; //给svec的第一个元素赋值
auto &v = svec.back(); //获得指向最后一个元素的引用
v = "last"; //改变svec中的元素
auto v2 = svec.back(); //v2不是一个引用,它是svec.back()的一个拷贝
v2 = "lst"; //未改变svec中的元素
}
下标操作和安全的随机访问
提供快速访问的容器(string、vector、deque和array)也都提供下标运算符。下标运算符并不检查下标是否在合法范围内。使用越界下标是一种严重的程序设计错误,而且编译器并不检查这种错误。
vector<string> svec = {"quasi","simba","frollo","scar"};
for (auto i = 0; i < svec.size(); i++) {
cout << svec[i] << " ";
}
cout << endl;
3.3 删除元素
与添加元素的多种方式类似,(非array)容器也有多种删除元素的方式。
- 这些操作会改变容器大小,所以不适用于array。
- forward_list有特殊版本的erase,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指向尾元素,则返回尾后迭代器。若p是尾后迭代器,则函数行为未定义 |
c.erase(b,e) | 删除迭代器b和e所指定范围内的元素。返回一个指向最后一个被删元素之后的元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器 |
c.clear() | 删除c中的所有元素。返回void |
==删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。==
pop_front和pop_back成员函数
pop_front和pop_back成员函数分别删除首元素和尾元素。
while(!ilist.empty()) {
process(ilist.front()); //对ilist的首元素进行一些处理
ilist.pop_front(); //完成处理后删除首元素
}
从容器内部删除一个元素
成员函数erase从容器指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可也删除由一对迭代器指定的范围内的所有元素。两种形式的erase都返回指向删除的最后一个元素之后位置的迭代器。
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++;
}
}
删除多个元素
接受一对迭代器的erase版本允许我们删除一个范围内的元素:
//删除两个迭代器表示范围内的元素 //返回指向最后一个被删元素之后位置的迭代器 elem1 = slist.erase(elem1,elem2); //调用后,elem1 == elem2
3.4 特殊的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;
}
}
3.5 改变容器的大小
用resize来增大或缩小容器。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部:
list<int> ilist(10, 42);
ilist.resize(15); //将5个值为0的元素添加到ilist的末尾
ilist.resize(25, -1); //将10个-1添加到ilist的末尾
ilist.resize(5); //从ilist末尾删除20个元素
如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能导致迭代器、指针和引用失效。
3.6 容器操作可能使迭代器失效
向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。
在向容器添加元素后:
- 如果容器是vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效。但指向插入位置之后的将会失效。
- 对于deque,插入到除首尾元素之外的任何位置都会导致迭代器、指针和引用失效。
- 对于list和forward_list,指向容器的迭代器、指针和引用仍有效。
编写改变容器的循环程序
添加/删除vector、string或deque元素的循环必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是insert或erase,那么更新迭代器很容易。这些操作都返回迭代器,我们可以用来更新:
vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
auto it = vi.begin();
while(it != vi.end()) {
if (*it % 2) {
it = vi.insert(it, *it); //赋值当前元素
it += 2; //向前移动迭代器,跳过当前元素以及插入到它之前的元素
}else {
it = vi.erase(it); //删除偶元素
//不应向前移动元素,iter指向我们删除的元素之后的元素
}
}
在调用erase后,不必递增迭代器,erase返回的迭代器以及指向序列中下一个元素。调用insert后,需要递增迭代器两次。insert在给定位置之前插入新元素,然后返回指向新元素的迭代器。
不要保存end返回的迭代器
当我们添加/删除vector或string的元素后,或在deque中首元素之外任何位置添加/删除元素后,原来end返回的迭代器总是会失效。
//灾难:此循环的行为是未定义的
vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
auto begin = vi.begin(),
end = vi.end(); //保存尾迭代器的值是一个坏主意
while (begin != end) { //此循环是未定义的
++begin;//向前移动begin,因为我们想在此元素之后插入元素
vi.insert(begin, 8); //插入元素
++begin; //向前移动begin,跳过我们刚刚加入的元素
}
//更安全的方法:在每个循环步添加/删除元素后都重新计算end
while(begin != v.end()) {
//....
++begin; //向前移动begin,因为我们想在此元素之后插入元素
begin = v.insert(begin,42); //插入新值
++begin; //向前移动begin,跳过我们刚刚加入的元素
}
将end操作返回的迭代器保存在一个名为end的局部变量中。在循环体中,向容器中添加了一个元素,这个操作使保存在end中的迭代器失效了。这个迭代器不再指向vi中的任何元素,或是vi中尾元素之后的位置。
==如果在一个循环中插入/删除deque、string或vector中的元素,不要缓存end返回的迭代器。
必须在每次插入操作后重新调用end(),而不能在循环开始前保存它返回的迭代器。==
4. vector对象是如何增长的
为了支持快速访问,vector将元素连续存储——每个元素紧挨着前一个元素存储。
管理容量的成员函数
shrink_to_fit只适用于vector、string和deque。
capacity和reserve只适用于vetor和string。
容器大小管理操作 | 定义 |
---|---|
c.shrink_to_fit() | 请将capacity()减小为与size()相同的大小 |
c.capacity() | 不重新分配内存空间的话,c可以保存多少元素 |
c.reserve(n) | 分配至少容纳n个元素的内存空间 |
==reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。==
capacity和size
容器的size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
vector<int> vi;
//size的值为0,capacity的值依赖于具体实现
cout << "vi:size:" << vi.size()
<< " capacity:" << vi.capacity() << endl;
//向vi添加24个元素
for (vector<int>::size_type i = 0; i < 24; i++) {
vi.push_back(i);
}
//size为24,capacity大于等于24,具体依赖于实现
cout << "vi:size:" << vi.size()
<< " capacity:" << vi.capacity() << endl;
==每个vector实现都可以选择自己内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。==
------------------------------------------------------------------------------------------------分割线------------------------------------------------------------------------------------------------------------------------------------------
5. 额外的string操作
5.1 构造string的其他方法
构造string的其他方法 | 定义 |
---|---|
string s(cp,n) | s是cp指向的数组中前n个字符的拷贝。此数组至少应该包含n的字符 |
string s(s2,pos2) | s是string s2从下标pos2开始的字符拷贝。若pos2>s2.size(),构造函数的行为未定义 |
string s(s2,pos2,len2) | s是string s2从下标pos2开始len2个字符的拷贝。若pos2>s2.size(),构造函数的行为未定义。不管len2的值是多少,构造函数至多拷贝s2.size()-pos2个字符 |
这些构造函数接受一个string或一个const char*参数,还接受指定拷贝多少个字符的参数。
const char* cp = "Hello World!!!";
char noNull[] = { 'h','i' };
string s1(cp); //拷贝cp中的字符直到遇到空字符
string s2(noNull, 2); //从noNull拷贝两个字符
string s3(noNull); //未定义的,noNull不是以空字符结束
substr操作
substr操作返回一个string,它是原始string的一部分或全部的拷贝。
s.substr(pos,n); //返回一个string,包含s中从pos开始的n个字符的拷贝。 //pos的默认值为0,n的默认值为s.size()-pos,即拷贝从pos开始所有的字符。
5.2 改变string的其他方法
string类型支持顺序容器的赋值运算符以及assign、insert和erase操作。
const char *cp = "Stately,plump Buck";
string s("Hello World");
s.assign(cp, 7); //替换s的内容为Stately,赋予s的是从cp指向地址开始的7个元素
s.insert(s.size(), cp + 7); //将字符插入到s[size()]处元素之前的位置
关于insert操作的说明:
string s = "some string";
string s1 = "some other string";
s.insert(0, s1); //在s中0位置之前插入s1的拷贝
s.insert(0, s1, 0, s1.size()); //在s[0]之前插入s1中s1[0]开始的s1.size()个字符
append和replace函数
string类定义了两个额外的成员函数:append和replace,这两个函数可以改变string的内容。
append操作是在string末尾进行插入操作的一种简写形式:
string s("C++ Primer"), s2 = s; // 将s2和s初始化为"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
5.3 string搜索操作
string类提供了6个不同的搜索函数,每个函数都有4个重载版本。每个搜索操作都返回一个string::size_type值,表示匹配发生位置的下标。如果搜索失败,则返回一个名为sting::npos的static成员。
string搜索操作 | 定义 |
---|---|
s.find(args); | 查找s中args第一次出现的位置 |
s.rfind(args); | 查找s中args最后一次出现的位置 |
s.find_first_of(args); | 在s中查找args中任何一个字符第一次出现的位置 |
s.find_last_of(args); | 在s中查找args中任何一个字符最后一次出现的位置 |
s.find_first_not_of(args); | 在s中查找第一个不在args中的字符 |
s.find_last_not_of(args); | 在s中查找args最后一次出现的位置 |
指定在哪里开始搜索
我们可以传递给find操作一个可选的开始位置。
string::size_type pos = 0;
while((pos = name.find_first_of(numbers,pos))
!= string::npo) {
cout << "find number at index: " << pos
<< " element is " << name[pos] <<endl;
++pos; //移动下一个字符
}
逆向搜索
rfind成员函数搜索最后一个匹配,即子字符串最靠右的出现位置:
string river("Mississippi");
auto first_pos = river.find("is"); //返回1
auto last_pos = river.rfind("is"); //返回4
5.4 compare函数
根据s是等于、大于还是小于参数指定的字符串,s.compare()返回0、正数或负数。
5.5 数值转换
int i = 42;
string s = to_string(i); //将整数i表示为字符表示形式
double d = stod(s); //将字符串s转换为浮点数
要转换为数值的string中的第一个非空白符必须是数值中可能出现的字符:
#include <iomanip>
void main() {
string s2 = "pi = 3.1415926";
string s3 = s2.substr(s2.find_first_of("+-.1234567890"));
double d = stod(s3);
cout <<std::setprecision(10)<< d << endl; //设置输出精度
}
6. 容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。适配器是标准库中一个通用的概念。
定义一个适配器
每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。
deque<int> deq;
stack<int> stk(deq); //从deq拷贝元素到stk
//在vector上实现的空栈
stack<string, vector<string>> str_stk;
栈适配器
栈默认基于deque实现,也可以在list或vector之上实现。
栈操作 | 定义 |
---|---|
s.pop() | 删除栈顶元素,但不返回该元素值 |
s.push(item) | s.emplace(args) | 创建一个新元素压入栈顶,该元素通过拷贝或移动item而来,或者由args构造 |
s.top() | 返回栈顶元素,但不将元素弹出栈 |
stack<int> stk; //空栈
for (size_t i = 0; i < 10; i++) {
stk.push(i); //保存0-9十个数
}
while(!stk.empty()) {
int value = stk.top(); //返回栈顶元素,但不将元素弹出栈
cout << value;
stk.pop();
}
队列适配器
queue默认基于deque实现,priority_queue默认基于vector实现;
queue也可以用list或vector实现,priority_queue也可以用deque实现。
queue和priority_queue操作 | 含义 |
---|---|
q.pop() | 返回queue的首元素或priority_queue的最高优先级的元素,但不删除此元素。 |
q.front() | 返回首元素或尾元素,但不删除此元素 |
q.back() | 只适用于queue |
q.top() | 返回最高优先级元素,但不删除该元素[只适用于priority_queue] |
q.push(item) | q.emplace(args) | 在queue末尾或priority_queue中恰当的位置创建一个元素,其值为item,或者由args构造 |
标准库queue使用一种先进先出(first-in,first-out,FIFO)的存储和访问策略。进入队列的对象被放置到对尾,而离开队列的对象从队首被删除。
priority_queue允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。
vector<string> svec = {"quasi","simba","frollo","scar"};
for (auto i = 0; i < svec.size(); i++) {
cout << svec[i] << " ";
}
cout << endl;