容器概述
容器是一种特殊的类型,其对象可以放置其他类型的对象(元素)。通常来讲,容器需要支持以下的操作:
- 对象的添加
- 对象的删除
- 对象的索引
- 对象的遍历
以上所述只是通常的情况,有一些容器只支持其中一部分操作
有多种算法可以用来实现容器,每种方法各有利弊。C++中的容器可以大致分为以下四类:
- 序列容器:其中的对象有序排列,使用整数值进行索引
- 关联容器:其中对象的顺序并不重要,使用键来进行索引
- 适配器:调整原有容器的行为,使得其对外展现出新的类型、接口或者返回新的元素
- 生成器:构造元素序列
关于容器,我们必须提到一个非常重要的概念:迭代器。迭代器用于指定容器中的一段区间,以执行遍历、删除等操作。我们可以使用以下方法来获取迭代器:
(c)begin/(c)end
std::vector<int>x{1,2,3}; auto b = x.begin(); auto e = x.end(); for(auto ptr = b;ptr != e;++ptr){ std::cout << *ptr << " "; }
(c)rbegin/(c)rend
:逆序迭代器std::vector<int>x{1,2,3}; auto b = x.rbegin(); auto e = x.rend(); for(auto ptr = b;ptr != e;++ptr){ std::cout << *ptr << " "; }
每种容器对应的迭代器可以支持的操作并不相同,大体来说,迭代器可以分成以下五类
关于每种迭代器的具体描述请参考这里。当然有些容器并不提供迭代器,在C++20中,我们将提供迭代器的容器称为Range
。
序列容器
C++标准库中提供了多种序列容器模版,包括
- array:元素个数固定的序列容器,不支持对象的添加和删除
- vector:元素在内存中连续储存的序列容器
- forward_list/list:基于链表/双向链表的容器
- deque:vector和list的折中,将整个容器分成若干了段,段内元素连续存储,段之间使用类似链表的结构来进行连接
- basic_string:提供了对字符串专门的支持
在使用时,我们需要使用元素类型来实例化容器模版,从而构造可以保存具体类型的容器。不同容器所提供的接口大致是相同的,但根据容器性质的差异,其内部实现与复杂度不同。如果某个接口对于某些特定的容器的实现复杂度非常高,这些容器会提供相对较难使用的接口或者根本不提供相应的接口。
array容器模版
array是一个具有固定长度的容器,其内部维护了一个内建数组,与内建数组相比提供了复制操作。array主要提供了以下接口:
- 构造:与内建数组相似
std::array<int ,3> x = {1,2,3};
- 获取元素类型
std::array<int ,3>::value_type y
- 元素访问:
[]
、at
、front
、back
、data
x[1]; x.at(1); // 越界会直接崩溃 x.front(); x.back(); x.data(); // 返回指针,指向x中内建数组的第一个元素
- 容量相关(平凡实现):
empty
、size
、max_size
注意
empty
,size
和max_size
的返回值在编译期就已经确定了。如果std::array<type,size_t>
中的size_t
不为0,empty
返回false
,否则返回true
。size
和max_size
的返回值与size_t
的值相同。 - 填充array内的元素:
fill
- 将两个array内的元素进行交换:
swap
注意,array的复制操作和swap操作的实现复杂度非常高
- 比较操作:
==
,<=>
(C++20),具体的参考这里注意,参与比较的两个array的类型要完全相同,包括元素类型和元素个数
- 迭代器:
c(begin)/(c)end/(c)rbegin/(c)rend
接下来的剩余的容器模版只会着重介绍与array不同的地方,对于相同的部分请自行参考cppreference
vector容器模版
与array不同,vector中元素的数目是可以改变的,vector内部的实现如下图所示
其中buffer中浅灰色代表已经使用的内存,深灰色代表已分配但未使用的内存
与array一样,vector也提供了许多接口,其中大部分与array类似,但也有一些特殊的接口
- 容量相关接口:`
capacity
:返回buffer的长度,注意这里不是vector内已有元素的个数reserve
:一次性开辟固定长度的buffer,避免在添加元素时不断拓充buffer带来的性能下降shrink_to_fit
:对buffer进行瘦身
,开辟一块与现有元素个数大小相同的buffer并将所有的元素拷贝到新的buffer中,避免占用过多的内存
- 附加元素接口
push_back
:在vector的结尾添加元素empllace_back
:与push_back
类似,但是是在内存中原地构造,不需要先构造一个临时变量再将临时变量push_back
进去
- 元素插入接口
insert
:在vector中间插入元素emplace
:与insert
类似,但是是在内存中原地构造insert
和emplace
需要使用迭代器进行插入
- 元素删除接口
pop_back
:删除最后一个元素erase
:删除vector中任意一个元素erase
需要使用迭代器进行删除clear
:删除所有元素
在使用这些接口时,需要注意以下几点
- vector不提供
push_front/pop_front
的接口,可以使用insert/erase
来模拟,但是效率不高 - vector的
swap
操作效率很高,只需要交换buffer的指针和对应的size
和cap
的值 - vector的写操作可能会使迭代器失效,主要的原因就是buffer的改变,具体的参考Iterator invalidation
list容器模版
list是使用双向链表实现的序列容器,双向链表的结构如下图所示
与vector相比,list具有以下特点
- 插入和删除的成本较低,但随机访问的成本较高
- list提供了
pop_front
和push_front
接口 - list提供了
splice
接口:将一个list容器中的内容移动到另外一个list容器中,本质上只修改了指针的值,效率非常高 - list的写操作通常不会使迭代器失效
forward_list
使用单向链表实现的序列容器,单向链表的结构如下图所示
相比list,forward_list提供了成本较低的序列容器的实现,forward_list相比list具有以下特点
- forward_list的迭代器只支持递增操作,因此没有
(c)rbegin/(c)rend
等接口 - forward_list不支持使用
size
函数获取元素个数 - forward_list不支持
push_back/pop_back
操作 - forward_list支持许多
XXX_after
操作,具体参考这里
deque容器模版
deque是vector与list的折中,deque中元素的典型布局如下图所示
一种可能的deque实现如下图所示
deque具有以下特点
- 与vector相比,deque的
push_back
和push_front
速度较快 - 与list相比,deque支持随机访问,但性能不如vector
- deque在序列中间插入、删除元素的速度较慢
一般来说,我们很少会使用到deque,只有当我们希望使用一个可以支持类似vector的操作,同时push_front的速度较快的容器时才会考虑deque
basic_string容器模版
basic_string与vector非常类似,但实现了字符串相关的接口,具体的请参考这里。关于basic_string,我们需要注意以下几点
- basic_string使用
char
实例化出了std::string
- basic_string提供了如
find
、substr
等字符串特有的接口 - basic_string提供了数值与字符串转换的接口,比如
to_string
,stoi
、stod
等 - basic_string针对短字符串进行了优化,具体的请参考SSO