文章目录
顺序容器概述
容器 | 描述 |
---|---|
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢 |
deque | 双端队列。支持快速随机访问。在头尾位置插入、删除速度很快。 |
list | 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快 |
forward_list | 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快 |
array | 固定大小数组。支持快速随机访问。不能添加或删除元素 |
string | 与vector相似的容器,但专门用于保存字符。支持快速随机访问。在尾部插入、删除速度快 |
存储策略的影响
- string和vector将元素保存在连续的内存空间中。由于元素是连续存储的,根据下标来计算地址是很快速的。然而,这样会导致在删除或插入除尾部以外的元素时,需要移动插入/删除之后的所有元素来保证连续存储。更糟糕的是,这样可能会需要分配额外的存储空间。
- list和forward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问。这两个容器的额外内存开销也很大(记录元素之外还需要记录地址)。
- deque是一个复杂的数据结构。与string和vector类似,deque支持快速的随机访问。与string和vector一样,在deque的中间位置添加或删除元素的代价很高。但是,在deque的两端添加和删除元素都很快,与list或forward_list添加元素的速度相当。
容器选择原则
- 除非有很好的理由选择其他容器,否则使用vector。
- 如果程序有很多小的元素,且空间的额外开销很重要,那么不要使用list或forward_list。
- 如果程序要求随机访问元素,应使用vector或deque。
- 如果程序要求在容器的中间插入或删除元素,应使用list或forward_list。
- 如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque。
- 如果程序只有在读取输入时才需要在容器的中间位置插入元素,随后需要随机访问元素。考虑两个方面:这个需求是否是真的?比如可以通过vector的push_back()与sort函数避免在中间位置添加元素;如果需求是真的,可以先采用list再拷贝给vector。
我们需要更加添加/删除元素的需求、访问顺序的需求以及空间开销的需求灵活选择容器。
容器库概览(适用于所有容器的操作)
定义
- 每个容器都定义在同名头文件中;
- 定义时,需要在尖括号中指定元素的类型。
操作
构造函数 | 含义 |
---|---|
C c | 默认构造函数,构造空的容器 |
C c1(c2) | 构造c2 的拷贝c1 |
C c(b,e) | 构造c,将迭代器b和e指定的范围内的元素拷贝到c(array不支持) |
C c{a,b,c...} | 列表初始化c |
赋值 | 含义 |
---|---|
c1=c2 | 将c1 中的元素替换为c2 中的元素 |
c1={a,b,c,...} | 将c1 中的元素替换为c2 中的元素(array不支持) |
交换 | 含义 |
---|---|
a.swap(b) | 交换a和b的元素 |
swap(a,b) | 交换a和b的元素 |
大小 | 含义 |
---|---|
c.size() | c中元素的数目(不支持forward_list) |
c.max_size() | c中可保存的最大元素数目 |
c.empty() | 非空,返回false;空,返回true |
添加/删除元素 | 含义 |
---|---|
c.insert(args) | 将args中的元素拷贝进c |
c.emplace(inits) | 使用inits构造c中的一个元素 |
c.erase(args) | 删除args指定的元素 |
c.clear() | 删除c中所有元素,范围void |
说明:
- 不适用于array;
- 在不同容器中,操作的接口不同(对应的函数名称不一样,但功能大体一致)。
关系运算符 | 含义 |
---|---|
==,!= | 所有容器都支持相等/不等运算符 |
<,<=,>,>= | 无序关联容器不支持 |
获取迭代器 | 含义 |
---|---|
c.begin(),c.end() | 返回指向c的首元素和尾元素之后位置的迭代器 |
c.cbegin(),c.cend() | 返回const_iterator |
迭代器
迭代器范围由一对迭代器表示。
end表示的迭代器不会指向最后一个元素,而是指向尾元素之后的位置。
迭代器范围中的元素包含first所表示的元素以及first开始直至last(但不包含last)之间的所有元素。这种元素范围被称为左闭合区间 [ b e g i n , e n d ) [begin,end) [begin,end)。
要求:begin和end必须指向相同的容器;end和begin可以指向相同的位置,但不能指向begin之前的位置。
这样一种左闭合范围蕴含的性质是:如果begin==end,那么迭代器范围为空;反之,迭代器范围中至少包含一个元素。
while (bgein != end){
*begin = val;
++begin;
}
容器类型成员
通过类型别名,可以在不了解容器中元素类型的情况下使用它。有:
类型别名 | 含义 |
---|---|
iterator | 迭代器类型 |
const_iterator | 可以读取元素,但不能修改元素的迭代器类型 |
size_type | 无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
different_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型;与value_type&含义相同 |
const_reference | 元素的const左值类型 |
说明:
- 以string为例,所有用于存放string类的size函数返回值的变量,都应该是
string::size_type
。 list<string>::iterator iter;
iter是通过list<string>
定义的一个迭代器类型。
begin和end成员
运算符 | 含义 |
---|---|
*iter | 得到iter所指元素的引用 |
iter->mem/(*iter).mem | 如果iter所指元素是一个类,可以直接获取其中的成员 |
--iter | 指向上一个元素 |
++iter | 指向下一个元素 |
iter1==iter2 | 判断是否相等 |
iter1!=iter2 | 判断是否不等 |
算术运算 | 含义 |
---|---|
iter + n | 移动迭代器 |
iter - n | 移动迭代器 |
iter += n | 加法的复合赋值语句 |
iter -= n | 减法的复合赋值语句 |
iter1 - iter2 | 两个迭代器之间的距离 |
>、>=、<、<= | 位置关系 |
容器定义和初始化
- 拷贝初始化:相同容器类型、相同元素类型(对于array来说还需要具有相同的大小)。
- 列表初始化:元素类型相容(对于array来说列表大小与array大小相同)。
- 默认构造函数:对于array来说是非空的,对于其他来说则是空的。
- 迭代器初始化(拷贝初始化):元素类型相容(array不适用)。
- 对于顺序容器而言(array除外):
C seq(N)
,seq包含n个元素,这些元素进行了值初始化(string不适用);C seq(n,t)
包含n个值为t的元素。
关于拷贝初始化,有两种。其中,迭代器初始化是不要求元素类型一样的。
如果元素类型是内置类型或者具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。如果没有默认构造函数,除了大小参数之外还需要一个显式的元素初始值。
当定义一个array时,除了指定元素类型,还需要指定容器大小array<int,42>
赋值和swap
赋值运算的本质为将左边容器中的全部元素替换为右边容器中的拷贝。
如果两边的大小不同,那么赋值后的长度为右边的容器大小。
关于赋值操作,对于顺序容器而言还有三种(不适用于array)
赋值 | 含义 |
---|---|
seq.assign(b,e) | 将迭代器范围内的元素赋值给seq(迭代器不能指向seq) |
seq.assign(i1) | 将seq中的元素替换为初始化列表i1中的元素 |
seq.assign(n,t) | 将seq中的元素替换为n个t |
除了array之外,swap知识交换了两个容器的内部数据结构,不对任何元素进行拷贝、删除或插入操作,可以在常数时间内完成。
因为元素不会被移动,所以除了string之外,指向容器的迭代器、引用和指针都不会失效,但是这些元素是属于不同容器的。
与其他容器不同的是,swap两个array是会真正交换元素的。
顺序容器操作
添加元素
添加 | 含义 |
---|---|
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指向的元素之前。返回指向新添加的第一个元素的迭代器;若范围为空,则返回p |
c.insert(p,i1) | 将花括号包围起来的元素值列表插入到迭代器p指向的元素之前,返回指向新添加的第一个元素的迭代器;若列表为空,则返回p |
对于vector、string、deque插入的元素会使所有指向容器的迭代器、引用和指针失效。
forward_list有自己版本的insert和emplace;
forward_list不支持push_back和emplace_back;
vector和string不支持push_front和emplace_front。
需要注意的是,容器的初始化或者插入都是对象的一个拷贝,所以说,容器内和原始对象的任何一方做出改动都不会互相影响。
条件 | 成员 |
---|---|
支持push_back | vector,string,list,deque |
支持push_front | list,forward_list,deque |
insert可以实现任何位置的插入。
通过insert的返回值,可以在容器中的一个特定位置反复插入元素。
list<string> lst;
auto iter = lst.begin();
while (cin >> word)
iter = lst.insert(iter,word); //等价于调用push_front
关于emplace
// 在c的末尾构造一个Sales_data对象
c.emplace_back("978-059",25,15.99);
c.push_back(Sales_data("978-059",25,15.99));
使用emplace时,会在容器管理的内存空间中直接创建对象;而调用push_back则会创建一个局部临时对象,并将其压入容器中。
访问元素
访问 | 含义 |
---|---|
c.back() | 返回c中尾元素的引用。若c为空,函数行为未定义 |
c.front() | 返回c中首元素的引用。若c为空,函数行为未定义 |
c[n] | 返回c中下标为n的元素的引用。n是一个无符号整数,函数行为未定义 |
c.at(n) | 返回下标为n的元素的引用。如果下标越界,则抛出一out_of_range异常 |
at和下标操作只适用于string、vector、deque和array;back不适用于forward_list。
访问元素的成员函数返回的都是引用。
删除元素
删除 | 含义 |
---|---|
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。 |
与插入操作相对应的:
- forward_list有自己版本的erase;
- forward_list不支持pop_back;
- vector和string不支持pop_front;
- 删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效;
- 指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。
特殊的forward_list操作
对于单向链表而言,当添加或删除一个元素时,删除或添加的元素之前的那个元素的后继需要改变。因此,我们需要访问其前驱。对于list而言就没有这个烦恼,因为双端队列的每一个Node同时保存了前驱和后继。所以对于单向链表来说访问前驱比登天还难。所以说就不支持push_back
和pop_back
。
另外,也是基于这个原因,在forward_list
中添加或删除元素的操作是通过改变给定元素之后的元素来实现的。也就是我已知当前元素,在这个元素后面进行添加或删除的操作。
于是有
forward_list专属 | 含义 |
---|---|
lst.befor_begin() | 返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用。 |
lst.cbefor_begin() | - |
lst.insert_after(p,t) | 在迭代器p之后的位置插入元素。返回最后一个插入元素的迭代器。 |
lst.insert_after(p,t,n) | - |
lst.insert_after(p,b,e) | - |
lst.insert_after(p,il) | - |
emplace_after(p,args) | - |
lst.erase_after(p) | 删除迭代器p之后的元素。返回一个指向被删除元素之后的迭代器。 |
lst.erase_after(b,e) | 删除从b之后到e之前的元素。 |
改变容器大小
改变大小 | 含义 |
---|---|
c.resize(n) | 调整c的大小为n个元素。如果n<c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化。 |
c.resize(n,t) | 多出来的元素的值初始化为t。 |
需要注意的是:
- 如果resize缩小容器,那么指向被删除元素的迭代器、引用和指针都会失效;
- 对vector、string、deque进行resize可能会导致迭代器、指针和引用失效。
容器操作可能使迭代器失效
想容器中添加元素或从容器中删除元素会使得指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。
向容器添加元素后:
- 如果容器是vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果没有重新分配,指向插入位置之前的元素的迭代器、指针、引用仍然有效。
- 对于deque插入到首尾之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在元素的引用和指针不会失效。
- 对于list和forward_list,指向容器的迭代器、指针和引用仍然有效。
被删除元素的迭代器、指针和引用是失效的,向容器删除一个元素后:
- 对于list和forward_list,指向容器其他位置的迭代器、引用和指针仍然有效。
- 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外的其他元素的迭代器、引用和指针也会失效。如果是删除deque的尾元素,那么尾后迭代器失效,其他不受影响;删除首元素也是同理。
- 对于vector和string,指向被删元素之前元素迭代器、引用和指针仍然有效。
容器适配器
适配器是一种机制,能使某种事物的行为看起来像另外一种事物。
标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。
已stack为例,stack适配器接受一个顺序容器(除array或forward_list外),并使其操作起来像一个stack。
定义一个适配器
每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。
例如,假定deq是一个deque<int>,我们可以用deq来初始化一个新的stack,如:stack<int> stk(deq);
。
默认情况下,stack和queue是基于deque实现的,priority_queue实在vector之上实现的。
还有两种定义方式:stack<string,vector<string>> str_stk //在vector上实现的空栈
和stack<string,vector<string>> str_stk2(svec)
适配器限制
- stack只要求push_back、pop_back和back的操作,因此可以用除了array和forward_list之外的任何容器来构造;
- queue要求push_back、pop_front、front的操作,因此可以构造于list和deque之上,不可以用vector和forward_list;
- priority_queue除了front、push_back、pop_back之外还要求随机访问能力,所以只能构造于vector和deque之上,不可以用list和forward_list。
栈适配器
下面程序展示了如何使用stack。
#include <stack>
using std::stack;
stack<int> intStack;
for (size_t ix=0; ix != 10; ++ix)
intStack.push(ix)
while (!intStack.empty()){
int value = intStack.top();
intStack.pop();
}
stack<int> intSatck
,定义了一个保存整型元素的栈intStack,初始时为空,默认是deque实现的。
栈的操作 | 含义 |
---|---|
s.pop() | 删除栈顶元素,但不返回该元素值 |
s.push(item) | 创建一个新元素压入栈顶,该元素通过拷贝或移动item而来,或者由args构造 |
s.emplace(args) | - |
s.top() | 返回栈顶元素,但不将元素弹出栈 |
每个容器适配器都是基于底层容器类型的操作定义了自己的特殊操作。我们只可以用适配器操作。
队列适配器
队列 | 含义 |
---|---|
q.pop() | 删除queue的首元素或priority的最高优先级的元素 |
q.front() | 返回首元素或尾元素,但不删除此元素 |
q.back() | 只适用于queue |
q.top() | 返回最高优先级元素,但不删除该元素,只适用于priority_queue |
q.push(item) | 在queue末尾或priority_queue中恰当的位置创建一个元素,其值为item或者由args构造 |
q.emplace(args) | - |