第九章 顺序容器
前言
一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。标准库还提供了三种容器适配器,分别为容器操作定义了不同的接口,来与容器类型匹配。
9.1 顺序容器概述
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入/删除元素很慢 |
deque | 双端队列(double-ended queue)。支持快速随机访问。在头尾位置插入或删除元素很快 |
list | 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快 |
forward_list | 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快 |
array | 固定大小数组。支持快速随机访问。不能添加或删除元素 |
string | 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快 |
Ⅰ)除了固定大小的array外,其他容器都提供高效、灵活的内存管理,可以添加和删除元素,扩张和收缩容器的大小。容器保存元素的策略对容器操作的效率有着固定的,有时是重大的影响。
1)string和vector将元素保存在连续的内存空间中,元素的下标来计算其地址是非常快速的,但是在两种容器的中间位置添加或删除元素非常耗时。
2)list和forward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问。与vector、deque和array相比,这两个容器的额外内存开销也很大。
3)deque是一个更为复杂的数据结构,元素可以从两端弹出。与string和vector类似,deque支持快速随机访问。与string和vector一样,在两种容器的中间位置添加或删除元素的代价(可能)很高。但是在deque的两端添加或删除元素很快,与list或forward_list添加/删除元素的速度相当。
4)forward_list和array是新C++标准增加的类型。与内置数组相比,array更加安全、更容易使用。forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此,forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。
Ⅱ)选择容器的基本原则
1)除非有很好的理由选择其他容器,否则使用vector是最好的选择。
2)如果程序有很多小元素且空间的额外开销很重要,不要使用list或forward_list。
3)要求随机访问元素,应该使用vector或deque。
4)要求中间插入或删除元素,应该使用list或forward_list。
5)要求在头尾插入或删除元素,且中间不进行插入或删除,应该使用deque。
6)如果只在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素。首先可以考虑在读取输入时使用vector,再调用sort函数重排容器中的元素,从而避免在中间位置添加元素。如果必须在中间位置插入元素,考虑在输入阶段使用list,输入完成将list拷贝到vector中。
9.2 容器库概览
本节将介绍所有的容器都支持的操作。一般说来,每个容器都定义在一个头文件中,文件名和类型名相同,容器均定义为模板类。我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数。
vector<noDefault> v1(10, init); // 正确:提供了元素初始化器
vector<noDefault> v2(10); // 错误!必须提供一个元素初始化器
类型别名 | |
iterator | 此容器类型的迭代器类型 |
const_iterator | 可以读取元素,但不能修改元素的迭代器类型 |
size_type | 无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
different_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型,与value_type&含义相同 |
const_reference | 元素的const左值类型,即constvalue_type& |
构造函数 | |
C c; | 默认构造函数,构造空容器 |
C c1(c2); | 构造 c2 的拷贝 c1 |
C c(b, e) | 构造 c,将迭代器 b 和 e 指定的范围内的元素拷贝到 c(array不支持) |
C c{a, b, c...} | 列表初始化 c |
赋值与swap | |
c1 = c2; | 将c1中的元素替换为c2中元素 |
c1 = {a, b, c...} | 将c1中的元素替换为列表中元素(不适用于array) |
a.swap(b) | 交换a和b的元素 |
swap(a, b) | 与a.swap(b)等价 |
大小 | |
c.size( ) | c中元素的数目(不支持forward_list) |
c.max_size( ) | c可保存的最大元素数目 |
c.empty( ) | 若c中存储了元素,返回false,否则返回true |
添加/删除元素(不适用于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.cend( ) | 返回const_iterator |
反向容器的额外成员(不支持forward_list) | |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin( ), c.rend( ) | 返回指向c的尾元素和首元素之前位置的迭代器 |
c.crbegin( ), c.crend( ) | 返回const_reverse_iterator |
Ⅰ)迭代器范围
1)一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。这两个迭代器通常被称为begin和end,或者是first和last,标记了容器中元素的一个范围,左闭合区间[begin, end)。
2)对构成范围迭代器的要求(编译器不会强制,但是程序员应该满足)
- 它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置。
- 可以通过递增begin来到达end(即end不在begin之前)
while (begin != end) {
*begin = val; // 正确:范围非空,因此begin指向一个元素
++begin; // 移动迭代器,获取下一个元素
}
auto it7 = a.begin(); // 仅当a是const时,it7是const_iterator
auto it8 = a.cbegin(); // it8是const_iterator
Ⅱ)容器类型成员
反向迭代器:反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义发生了颠倒。如对一个反向迭代器执行++操作,会得到上一个元素。
Ⅲ)容器定义和初始化
当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。并且,新容器和原容器中的元素类型也可以不同,只要能将拷贝的元素转换为要初始化的容器的元素类型即可。
// 将一个容器初始化为另一个容器的拷贝
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); // 错误!容器类型不匹配
forward_list<string> words(articles.begin(), articles.end()); // 正确,可以将const char*转换为string
deque<string> authList(authors.begin(), it); // 拷贝元素,直到 it-1 指向的元素
// 与顺序容器大小相关的构造函数
vector<int> ivec(10, -1); // 10个int元素每个都是-1
list<string> svec(10, "hi!"); // 10个string元素每个都是“hi!”
forward_list<int> ivec(10); // 10个int元素每个都是0
deque<string> svec(10); // 10个string元素每个都空字符串
注意:1)只有顺序容器的构造函数才接受大小参数,关联容器不支持。 2)标准库array具有固定大小。
array<int, 42> // 保存42个int的数组
array<string, 10> // 保存10个string的数组
array<int, 10>::size_type i;
array<int>::size_type j; // 错误!array<int>不是一个类型
array<int, 10> ial;
array<int, 10> ia2 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> ia3 = {42};
// 对内置数组类型不能进行拷贝或对象赋值操作
int digs[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int cpy[10] = digs;
// 对array可以进行拷贝或对象赋值操作
array<int, 10> digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> copy = digits;
array<int, 10> a1 = {0}; // 正确
a1 = {0}; // 错误!不能将一个花括号列表赋予数组