文章目录
10 顺序容器
一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖元素的值,而与元素加入容器的位置相对应。
10.1 顺序容器类型
10.1.1 顺序容器概述
type | overview |
---|---|
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢。 |
deque | 双端队列。支持快速随机访问。在头尾位置插入或者删除元素很快。 |
list | 双向链表。仅支持双向顺序访问。在list中任何位置进行插入或者删除操作速度很快。 |
forward_list | 单向链表。仅支持单向顺序访问。在链表任何位置进行插入或者删除操作速度很快。 |
array | 固定大小数组。支持快速随机访问。不能添加或删除元素。 |
string | 与vector相似的容器,但专门用于字符保存。随机速度快。在尾部插入或者删除速度快。 |
看上去,好像除了固定大小的array
外,其他容器都提供高效灵活的内存管理。
不过,有得必有失,就像string
和vector
将元素连续存储在空间中,虽然使用元素下标来计算其地址非常的快速,但在两种容器的中间位置添加或者删除元素就会非常耗时(在插入或者删除时,需要移动插入或者删除的元素位置之后的所有元素);而list
和forward_list
就可以快速添加和删除,但代价是他们不支持元素的随机访问(访问一个元素我们必须遍历整个容器),而且相比vector
、deque
、array
,这两个容器的额外内存开销也很大。
deque
相对复杂。deque
与string
和vector
类似,支持快速随机访问,在中间位置添加或者删除元素的代价较高;但deque
的两端添加或者删除元素都是很快的(与list
和forward_list
相当)。
我们再瞧瞧array
,与内置数组相比,array
是一种更安全、更容易使用的数组类型。与内置数组相似,array
对象的大小是固定的。因此,array不支持添加和删除元素以及改变容器的大小。
而 forward_list
的设计目标是达到与最好的书写的单向链表数据结构相当的性能,因此, forward_list
没有size
操作,因为保存或计算其大小就会比手写链表多出额外的开销。
亲爱的,当你用上了C++,你就该尽可能使用C++标准库程序,而不是更原始的数据结构,如内置数组啥的。
10.1.2 选择容器指南
通常,我们按以下方式选取容器:
- 除非你有很好的理由选择其他容器,否则就用
vector
。 - 如果你程序有很多小元素,且空间额外开销很重要,请不要用
list
或forward_list
。 - 如果你程序要求随机访问元素,
vector
或deque
。 - 如果你程序要求在容器中间位置插入或删除元素,
list
或forward_list
。 - 如果你程序要求在容器头尾位置插入或删除元素,但不会在中间位置进行插入或删除,
deque
。 - 如果你程序只有读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则:首先确认是否真的需要在容器中间插入元素(比如有情况就能使用
vector
追加元素,并使用sort
排序,从而避免在中间插入元素);如果确实需要,那我们就可以在输入阶段使用list
,输入完成后拷贝到vector
中。
10.2 容器操作
使用之前,我们引用其类型同名头文件,比如list
就引入#include <list>
。
基本上顺序容器可以保存任意类型的元素,特别的,我们也可以定义一个容器,其元素类型是另外一个容器:
std::list<std::string> l;
还可以容器里放容器:
std::list<std::vector<std::string>> l;
而每种顺序容器都提供了一组有用的类型定义以及以下操作:
- 在容器中添加元素;
- 在容器中删除元素;
- 设置容器大小;
- (如果有的话)获取容器内的第一个和最后一个元素
10.2.1 容器定义的类型别名
类型别名 | 含义 |
---|---|
size_type | 无符号整型,足以存储此容器类型的最大可能容器长度 |
iterator | 此容器类型的迭代器类型 |
const_iterator | 元素只读迭代器类型 |
reverse_iterator | 按逆序寻址元素的迭代器类型 |
const_reverse_iterator | 元素只读逆序迭代器类型 |
difference_type | 足够存储两个迭代器差值的有符号整型,可为负数 |
value_type | 元素类型 |
reference | 元素的左值类型,是value_type& 的同义词 |
const_value_type | 元素的常量左值类型,等效于const value_type& |
10.2.2 迭代器
容器迭代器支持的操作可以回顾 4.3 迭代器,但有一个例外,forward_list
不支持递减运算符--
。
当然还有一些具体的迭代器运算,必须具体问题具体分析。
也可以看看
C++中的迭代器(2)——迭代器运算
10.2.2.1 迭代范围
范围之前说过了,这里再重复一遍:
[begin,end)
因为end
指向尾元素之后的元素,故不能取。
10.2.2.2 使用左闭合蕴含的编程假定
也就是把之前我们编程时的经验总结一下:
- 如果
begin
与end
相等,则范围为空。 - 如果
begin
与end
不等,则范围内至少包含一个元素,且begin
指向第一个元素。 - 我们可以递增
begin
,直到begin==end
。
代码表示一下,你就熟悉了:
vector<int> v{1, 3, 5, 7, 9};
vector<int>::iterator begin = v.begin();
auto end = v.end();
while (begin != end)
{
cout << *begin << endl;
++begin;
}
10.2.2.3 begin和end成员
begin
和end
操作可以生成指向容器的第一个元素和尾元素之后位置的迭代器。这两个迭代器最常见的用途就是形成一个包含容器所有元素的迭代器范围。
而且begin
和end
还有很多版本:
带r开头的反向迭代器和带c开头的返回const迭代器
c.cbegin() //返回const_iterator
c.cend()
c.rbegin() //按逆序寻址元素的迭代器
c.rend()
c.crbegin() //返回const_reverse_iterator
c.crend()
故,我们应该改写刚刚的例子:
vector<int> v{1, 3, 5, 7, 9};
vector<int>::const_iterator cbegin = v.cbegin();
auto cend = v.cend();
while (cbegin != cend)
{
cout << *cbegin << endl;
++cbegin;
}
当然这里的const是指的底层const(不能修改指向的对象)
当不需要写访问的时候,应该使用cbegin和cend
10.2.3 容器定义和初始化
每个容器类型都定了默认构造函数,除了array
之外,其他容器的默认构造函数都会创建指定类型的空容器对象,而且都可以接受指定容器大小和元素初始值的参数。
构造函数 | 含义 |
---|---|
C c | 默认构造函数。如果C 是一个array ,则c 中的元素按默认方式初始化;否则c 为空 |
C c(c2) C c=c2 | 创建容器c2 的副本c ;c2 和c 必须具有相同的容器类型,并存放相同类型的元素。 |
C c(b, e) | 创建容器c ,其中元素是迭代器b和e标示的范围内元素的副本。(array 不适用) |
C c{a,b,c...} C c= {a,b,c...} | 列表初始化c |
C c(n) | 创建有n 个初始化元素的容器c 。此构造函数是explicit的(取消了隐式转换),(string 不适用) |
C c(n, t) | 使用n 个为t 的元素创建容器c ,其中值t 必须是容器类型C 的元素类型的值,或者是可以转换为该类型的值。 |
为了创建一个容器为另一个容器拷贝,两个容器的类型以及元素类型必须匹配。不过,当传递迭代器的参数来拷贝一个范围时,就不要求容器类型是相同的了(用迭代器构造)。而且,新容器中的元素也不一定要完全相同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。
list<string> author = {"mico", "rich"};
vector<const char *> articles = {"a", "an", "the"};
deque<string> d1(author);//报错,容器类型不匹配
deque<string> d2(author.cbegin(), author.cend());//正确
forward_list<string> wrods(articles.cbegin(), articles.cend());//正确
10.2.4 array具有固定大小
类似是一个保存10个int
型的array
:
array<int, 10> a;
不过,由于大小是array
类型的一部分,array
不支持普通的容器构造函数。这些构造函数都会隐式或者显式地确定容器大小。
而且array
固定大小的特性也影响了它所定义的函数的行为。与其他容器不同,一个默认构造的array
是非空的,它包含了与其大小一样多的元素(这些元素都被默认初始化了)。
当然也可以进行列表初始化,不过当初始化数目小于array
大小时,这些列表初始化靠前的元素,所有剩余元素都会进行默认初始化。
内置数组是不能进行拷贝或对象赋值,但array
可以。
10.2.5 赋值和swap
操作 | 功能 |
---|---|
c1=c2 | 删除容器c1 中所有的元素,然后将c2 的元素复制给c1 。c1 和c2 的类型(包括容器类型和元素类型)必须相同 |
c1.swap(c2) | 交换内容:调用完该函数后,c1 中存放的是c2 原来的元素,c2 中存放的是原来c1 的元素。c1 和c2 的类型必须相同。该函数的执行速度通常要比将c2 复制到c1 的操作快 |
swap(c1, c2) | 等价于c1.swap(c2) |
除了array
,swap
不会进行拷贝、删除、插入,它仅仅交换了两个容器的内部数据结构(元素本身没有交换),因此可以保证在常数时间内完成。
由于容器内没有移动任何元素,故,除了string
,指向容器迭代器、引用、指针都不会失效,只不过指向另外一个容器了罢了。
vector<int> v1 = {1, 2, 3};
vector<int> v2 = {4, 5, 6};
auto it = v1.cbegin() + 2;//3
swap(v1, v2);
cout << *it << endl;//3
不过,string
调用swap
会导致迭代器、引用、指针失效。
与其他容器不同,swap
两个array
会真正交换它们的元素。
所以指针、引用、迭代器都指向了交换后的值。
array<int, 3> a1 = {1, 2, 3};
array<int, 3> a2 = {4, 5, 6};
auto it = a1.cbegin() + 2;//3
swap(a1, a2);
cout << *it << endl;//6
swap分为成员函数和非成员函数版本。非成员函数版的swap在泛型编程中非常常见。统一使用非成员函数版本的swap是一个好习惯。
assign
操作仅顺序容器(除了array
)可以使用,即array
和关联容器不能使用。
操作 | 功能 |
---|---|
c.assign(b, e) | 重新设置c 的元素,将迭代器b 和c 标记范围内的所有元素复制到c 中。b 和e 必须不是指向c 中元素的迭代器 |
c.assign(il) | 重新设置c 的元素为初始化列表il 的元素 |
c.assign(n, t) | 将c 重新设置为存储n 个值为t的元素 |
如果你想把用不同类型的容器来进行赋值:
如果直接:
list<string> names;
vector<const char *> oldstyle;
names = oldstyle;//错误,容器类型不符合
但我们可以使用assign
:
names.assign(oldstyle.cbegin(), oldstyle.cend());
10.2.6 容器大小操作
操作 | 功能 |
---|---|
c.size() | 返回容器c 中元素个数,返回类型为c::size_type (forward_list 不支持) |
c.max_size() | 返回容器c 可容纳的最多元素个数,返回类型为c::size_type |
c.empty() | 返回标记容器大小是否为0 的布尔值 |
10.2.7 关系运算符
==
和!=
如果两个容器具有相同的大小且所有元素都两两对应相等,则两个容器相等;否则不等。
>
、<
、<=
、>=
如果两个容器大小不同,但较小容器中的每个元素都等于较大容器的中的对应元素,则较小容器小于较大容器;
如果两个容器都不是另外一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。
不过,只有当其元素定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。
10.3 顺序容器操作
顺序容器与关联容器不同于两者组织元素的方式。这些不同之处直接关系到元素如何存储、访问、添加和删除。之前我们介绍了所有容器的操作,现在我们来介绍顺序容器支持的操作。
10.3.1 添加元素
这些操作都会改变容器大小,故均不适用于array
。
操作 | 功能 |
---|---|
c.push_back(t) | 在容器c 的尾部添加值为t 的元素,返回void 类型(forward_list 不支持) |
c.push_front(t) | 在容器c 的前端添加值为t 的元素,返回void 类型(vector 和string 不支持) |
c.insert(p, t) | 在迭代器p 所指向的元素前面插入值为t 的新元素,返回指向新添加元素的迭代器 |
c.insert(p, n, t) | 在迭代器p 所指向的元素前面添加插入n 个值为t 的新元素,返回指向新添加元素的迭代器 |
c.insert(p, b, e) | 在迭代器p 所指向元素前面插入由迭代器b 和e 标记范围的元素,返回指向新添加元素的迭代器 |
平时我们使用push_back
意为在容器末尾添加,而且在list
、forward_list
和deque
容器中还支持push_front
,在容器开头添加。
deque
和vector
一样提供了随机访问元素的能力,但它也具备了vector
的不支持的push_front
。deque
能保证在容器首尾插入和删除元素都只花常数时间,但它也和vector
一样,在首尾之外的位置插入元素会很耗时。
虽然一些容器不支持push_front
,但我们可以使用奇招:
list<string> l1={"this" ,"world!"};
l1.insert(l1.cbegin(),"hello");
当然要完成push_front
,还有一点,就是迭代器指向新添加的元素:
list<string> l1={"this" ,"world!"};
auto iter = l1.begin();
string word;
while (cin >> word)
iter = l1.insert(iter, word);//等价push_front
新标准引入了三个新成员——emplace_front
、emplace
、emplace_back
。
这些操作构造而不拷贝元素,对应的是push_front
、insert
、push_back
。
当调用push
或insert
成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。
而我们调用emplace
成员函数时,则是将参数传递给元素类型的构造函数。emplace
成员使用这些参数在容器管理的内存空间中直接构造元素。
c++之emplace的使用
就比如我们可以把
list<string> l1;
string word(5,'c');
l1.push_back(word);
写成
list<string> l1;
l1.emplace_back(5,'c');
10.3.2 访问元素
操作 | 功能 |
---|---|
c.back() | 返回容器c 的最后一个元素的引用,如果c 为空,则该操作未定义(forward_list 不适用) |
c.front() | 返回容器c 的第一个元素的引用,如果c 为空,则该操作未定义 |
c[n] | 返回下标为n 的元素的引用,如果n<0 或n>c.size() ,则该操作未定义(只适用于vector 、deque 、string 、array ) |
c.at(n) | 返回下标为n 的元素的引用。如果下标越界,则该操作未定义(只适用于vector 、deque 、string 、array ) |
包括array
在内的每个顺序容器都有一个front
成员函数,而除了forward_list
之外的所有顺序容器都有一个back
成员函数。这两个操作分别返回首元素和尾元素的引用:
if (!c.empty())
{
//val和val2类型相同,都是c中的第一个元素值的拷贝
auto val = *c.begin(), val2 = c.front();
auto last = c.end();
//val3==val4
auto val3 = *(--last);//如果是forward_list则不能递减
auto val4 = c.back();//如果是forward_list则没有back
}
在调用front
、back
或者解引用迭代器之前,要保证c
非空,否则if
中的行为将未定义。
而且,因为返回的是引用,所以有这样的性质:
if (!c.empty())
{
c.front() = "kimo";
auto &v = c.back();
v = "rimo";//改变
auto v2 = c.back();
v2 = "mori";//未改变
}
10.3.3 删除元素
这些操作都会改变容器大小,故均不适用于array
。
操作 | 功能 |
---|---|
c.pop_back() | 删除容器c 的最后一个元素,返回void 。如果c 为空容器,则该操作未定义(不支持forward_list ) |
c.pop_font() | 删除容器c 的第一个元素,返回void 。如果c 为空容器,则该操作未定义(不支持vector 和string ) |
c.erase(p) | 删除迭代器p 所指向的元素,返回一个迭代器,它指向被删除元素后面的元素。如果p 指向容器内的最后一个元素,则返回的迭代器指向容器的超出末端的下一位置,如果p 本身就是指向超出末端的下一位置的迭代器,则该函数未定义(forward_list 有特殊版本的erase ) |
c.erase(b, e) | 删除迭代器b 和e 标记的范围内的所有元素。返回一个迭代器,它指向被删除元素段后面的元素。如果e 本身就是指向超出末端的下一位置的迭代器(尾后迭代器),则返回的迭代器也指向容器末端的下一位置(尾后迭代器) |
c.clear() | 删除容器c 内的所有元素,返回void |
pop_back()
和pop_font()
这些操作都返回void,如果你需要弹出元素值,那么你需要在执行弹出操作之前进行保存。
我们在容器内部删除一个元素很简单:
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;
或者删除多个元素
list<int> lst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto elem1 = lst.begin();
auto elem2 = ++lst.begin();
elem1 = lst.erase(elem1, elem2);
删除的范围是[elem1, elem2)
,删除结束后elem1==elem2
10.3.4 特殊的forward_list
为了更好的理解forward_list
为什么有特殊版本的添加和删除,我们可以思考假如我们在一个单链表删除了一个元素会怎么样:
就是在我们删除或添加一个元素时,我们需要访问它们之前的元素。因为这些操作上的差异,forward_list
并未定义insert
、emplace
、erase
,而是定义了insert_after
、emplace_after
、erase_after
的操作。例如我们刚刚的例子,我们为了删除elem3
,应该用指向elem2
的迭代器调用erase_after
。
为了支持这些操作,forward_list
也定义了before_begin
,它返回一个首前迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素之后添加或者删除元素。
操作 | 功能 |
---|---|
lst.before_begin() | 返回指向链表首元素之前的不存在的元素的迭代器,此迭代器不能解引用。 |
lst.cbefore_begin() | 返回const_iterator |
lst.insert_after(p,t) lst.insert_after(p,n,t) lst.insert_after(p,b,e) lst.insert_after(p,il) | 在迭代器p 之后的位置插入元素。t 是对象,n 是数量,b 和e 是表示范围的一对迭代器,il 是一个花括号列表。返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回p 。若p 就是尾后元素,则行为函数行为未定义。 |
lst.emplace_after(p,args) | 使用args 在p 指定的位置创建一个元素,返回一个指向这个新元素的迭代器。如果p 指向的是尾后元素,则函数行为未定义。 |
lst.erase_after(p) lst.erase_after(b,e) | 删除迭代器p 之后的元素。或者删除 b 之后直到e (不包含e )的范围的元素。返回一个指向指向被删除元素之后的迭代器,若不存在这样的元素,则返回尾后迭代器。如果p 指向的是尾后元素,则函数行为未定义。 |
把刚刚的删除奇数的例子用单链表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;
}
}
10.3.5 改变容器大小
我们使用resize
来增大或者缩小容器,当然,因为改变了容器的大小,所以array
是不支持的。
操作 | 功能 |
---|---|
c.resize(n) | 调整c 的大小为n 个元素,若n<c.size() ,则多出来的元素被丢弃;若必须添加新元素,对新元素进行初始化。 |
c.resize(n,t) | 调整c 的大小为n 个元素。任何新添加的元素都初始化为t |
如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素加到容器后部:
list<int> lst(10, 42);//10个42
lst.resize(15);//把5个0加到lst的末尾
lst.resize(25, -1);//把10个-1加到lst的末尾
lst.resize(5);//从lst末尾删20个元素
如果resize
缩小容器,则指向被删除元素的迭代器、引用和指针都会失效。
而对vector
、string
或deque
进行resize
都可能会导致迭代器、指针和引用失效。