1. 顺序容器概述
表一中的顺序容器,提供了对元素快速的顺序访问。但是其他操作的开销则不同:
- 添加或删除元素的开销
- 进行非顺序访问的开销
vector | 可变长度数组。支持快速随机访问。在非尾部插入或删除元素速度很慢 |
deque | 双端队列。支持快速随机访问。快速在头或尾的插入和删除 |
list | 双向链表。只支持双向的顺序访问。可以在list的任何位置快速插入和删除 |
forward_list | 单向链表。只支持单向顺序访问。可在任何位置快速插入和删除 |
array | 定长数组。支持快速随机访问。不能添加或删除元素。 |
string | 专门用来保存字符的容器,类似vector。支持快速随机访问。快速在尾部的 插入和删除。 |
容器中元素的存储方法决定了它所支持的操作,以及操作的效率。比如,string和vector中的元素存储在连续的内存中。因此它们的元素是连续的,支持快速随机访问。但是,在元素序列中的插入或删除操作开销很大。list和forward_list则可以快速的在容器中的任意位置插入或删除元素。但是,不支持随机访问。deque是一个比较复杂的数据结构。它可以像string和vector那样,支持快速随机访问。同时可以在容器的头部和尾部快速的插入和删除元素。
【C++11】
forward_list和array是新标准中加入的。array相比内置数组,更加安全、易用。它具有固定长度,不支持插入和删除元素。forward_list可以与最高效的手写单链表相媲美。forward_list不支持size操作,因为该操作开销很大。
【注】
新版本的库容器要比之前版本的效率高的多。所以,尽可能的使用它们吧。
如何选择使用哪个顺序容器?
可以按照下面的规则:
- 使用vector,除非你有理由使用其他容器
- 如果你的程序有许多小元素,且空间开销很重要时,不要使用list或forward_list
- 如果程序需要随机访问元素,使用vector或deque
- 如果程序需要在容器中间插入或删除元素,使用list或forward_list
- 如果程序需要在头部或尾部插入或删除元素,而不是中间,使用deque
- 如果程序只在读入数据时需要在容器中间插入元素,随后需要随机访问元素:
-- 首先,确定是否真的需要在容器中间插入元素。一般先为vector追加元素,然后调用sort函数排序会更容易。
-- 如果真的需要在中间插入元素,考虑在输入阶段使用list。然后当输入完成后,将list复制到vector。
如果程序既需要随机访问,又需要在中间插入或删除元素。那么就需要比较list或forarad_list访问元素的开销,和vector,deque插入或删除元素的开销。同时还需要考虑程序本身是随机访问次数更多还是插入、删除操作更多。或者还可以考虑使用更复杂的数据结构来支持。
2. 容器库概述
容器的操作种类可以划分为一个层次:
- 一些操作是通用的。
Type Aliases iterator 容器类的迭代器类型 const_iterator 该迭代器只能读,不能改变元素 size_type 无符号整型,足够保存最大可能的容器类的大小 difference_type 有符号整型,足够保存两个迭代器的距离 value_type 元素类型 reference 元素左值类型;与value_type&等价 const_reference 元素的const左值类型(const value_type &) Construction C c; 默认构造函数,空容器 C c1(c2); 创建c1为c2的一个副本 C c(b, e); 复制迭代器b和e范围内的元素(不适用于array) C c{a, b, c...}; 列表初始化 Assignment and swap c1 = c2 用c2中的元素值代替c1中的元素 c1 = {a,b,c...} 用列表中的元素代替c1中的元素 (不适用于array) a.swap(b) 交换a和b中的元素 swap(a, b) 等价于a.swap(b) Size c.size() c中元素的个数(不适用于forward_list) c.max_size() c最多可以保存的元素个数 c.empty() 如果c为空,true Add/Remove Elements
(not valid for array)Note: the interface to these operations varies by container type c.insert(args) 将args指代的元素插入c中 c.emplace(inits) 使用inits构造一个c中的元素 c.erase(args) 删除args指代的元素 c.clear() 删除c中所有元素,返回void Equality and Relational Operators ==, != 所有容器都支持 <, <=, >, >= 关系(非有序关系容器不支持) Obtain Iterators c.begin(), c.end() Return iterator to the first, one past the last element in c c.cbegin(), c.cend() Return const_iterator Additional Members of Reversible Containers (not valid for forward_list) reverse_iterator Iterator that addresses elements in reverse order const_reverse_iterator Reverse iterator that cannot write the elements c.rbegin(), c.rend() Return iterator to the last, one past the first element in c c.crbegin(), c.crend() Return const_reverse_iterator
- 顺序容器,关系容器,非顺序容器的其他操作
Defining and Initializing Containers C c; 默认构造函数。如果C是array,则c中的元素是默认初始化,否则c为空 C c1(c2) c1 is a copy of c2. c1 and c2 must have the same type
(必须是相同类型的容器,且其中的元素类型也相同;对于array,长度也需要相同)C c{a,b,c...} c is a copy of the elements in the initializer list.列表中元素的类型必须与C中元素的类型是相容的。对于array,列表中元素的个数必须小于或等于array的长度,缺少的部分用值初始化 C c={a,b,c...} C c(b,e) c is a copy of the elements in the range denoted by iterators b and e. 元素类型必须和C的元素类型相容。(不适用于array) Constructors that take a size are valid for
sequential containers only(not including array) C seq(n) seq has n value-initialized elements; the constructor is explicit.(Not valid for string). C seq(n, t) seq has n elements with value t.
容器几乎可以保存任何的类型,包括容器本身。但是还是有一些限制的。比如一些容器需要其元素支持某个特定的操作,如果某类型不支持这种操作,那么就不能作为容器的元素。
vector<vector<int>> vvi;
vector<vector<int> > vv_i; // 旧版本需要加一个空格
2.1 迭代器
需要特别指出的是,迭代器的范围是一个前开后闭区间:[begin, end)
2.2 容器的类型成员
容器中定义了一些类型成员,比如size_type,iterator, const_iterator等。这些类型别名可以让我们在使用的时候不需要知道具体类型,在泛型编程中很有用。
2.3 begin和end
【C++11】
c版本的begin和end是新标准加入的,使用auto可以很方便的定义。
vector<string> vs;
const vector<string> c_vs;
auto beg = vs.begin(); // iterator
auto cbeg = c_vs.begin(); // const_iterator
auto cbeg1 = vs.cbegin(); // const_iterator
不带c的版本在内部是重载函数。一个const版本的和一个非const版本的。如果是const对象,则调用const版本,如果是非const对象则调用非const版本。
2.4 定义和初始化容器
除了array之外,每个容器都定义了默认构造函数,以及一个指定容器大小和初始化元素值的构造函数。
根据一个容器来初始化另一个容器有两种方法:第一种是直接复制,容器类型和元素类型必须匹配;第二种是用迭代器指定一个范围,容器类型和元素类型都不需要一致,元素类型要保证是可以转换的。
vector<string> vs = {"hello", "world"};
list<const char*> lcc = {"ok", "I", "want"};
deque<string> ds(vs); // error
forward_list<string> fs(lcc.begin(), lcc.end()); // ok
顺序容器的初始化:
vector<int> vi(10, 1024); // 初始化为10个1024
list<string> ls(10, "ok"); // 初始化为10个"ok"
forward_list<int> fi(10); // 初始化为10个0
deque<string> ds(10); // 初始化为10个空string
需要指出的是,只提供一个元素个数的构造函数,需要元素本身(如果是类类型)具有默认构造函数。
array
array<int, 50> a = {0, 1, 2 ,3}; // 其余元素为0
2.5 赋值和交换
assign operations not valid for associative containers or array | |
---|---|
seq.assign(b,e) | Replaces elements in seq with those in the range denoted by iterators b and e. The iterators b and e must not refer to elements in seq. |
seq.assign(il) | Replaces the elements in seq with those in the initializer list il. |
seq.assign(n,t) | Replaces the elements in seq with n elements with value t. |
与内置数组不同的是,array支持赋值。
array<int,5> arr1 = {1,2,3,4,5};
array<int,5> arr2 = {0};
arr1 = arr2;
【注】赋值操作的等号左右两边的类型必须一致
使用assign
顺序容器还支持assign操作,提供了更加灵活的赋值,只要类型是相容的(即可以相互转换)。
list<string> ls;
vector<const char*> vcc;
ls = vcc; // error
ls.assign(vcc.cbegin(), vcc.cend()); // ok
使用swap
2.6 size操作
size函数返回当前容器中包含的元素个数;如果容器为空,
empty返回ture,否则返回false;
max_size返回容器最大可以容纳的元素个数。
2.7 关系操作符
所以容器都支持相等操作(==和!=);所以容器除了
无序关联容器,都支持关系操作(>, >=, <, <=)。
【注】只有容器的元素支持相应的关系操作时,才能比较该容器。
3. 顺序容器操作
3.1 添加元素
除了array,所以的库容器都支持容器大小在运行时的动态改变。
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.inesrt(p, n, t) | 在迭代器p所指元素的前面,插入n个值为t的元素;返回指向插入的第一个元素的迭代器, 如果n为0,返回p |
c.insert(p, b, e) | 在迭代器p所指元素的前面,插入迭代器指向范围为[b,e)的元素,b,e不能指向c本身。 返回指向插入的第一个元素的迭代器,如果范围为空,返回p |
c.insert(p, il) | il是包含元素值的括号序列。在迭代器p指向的元素前面插入该序列。返回指向插入的 第一个元素的迭代器,如果序列为空,返回p |
不同容器的插入操作效率是不同的。比如在vector中插入(非尾部)元素,需要元素的移动操作,这会导致额外开销。
list<string> ls;
vector<string> vs = {"this", "is", "for", "test"};
ls.insert(ls.begin(), vs.end()-2, v.end());
ls.insert(ls.end(), {"love","coding"});
【c++11】
新标准中,接受一个计数参数或迭代器范围的insert版本会返回指向插入的第一个元素的迭代器(之前的版本返回void)。
【c++11】
emplace_front, emplace, emplace_back是新标准加入的三个操作,与push_front, insert, push_back不同的是,它们用args参数调用元素类型的构造函数在插入的位置创建新的元素,而不是根据传递的元素复制一个副本。
相当于根据参数类型,调用相应的构造函数,在容器所在的内存空间(插入的位置)创建新的元素。
3.2 访问元素
c.back() | 返回容器c的最后一个元素的引用。如果c为空,则结果未定义 |
c.front() | 返回容器c的第一个元素的引用。如果c为空,则结果未定义 |
c[n] | 返回下标n所在位置的元素。如果n>=c.size(),则结果为定义 |
c.at(n) | 返回下标n所在位置的元素。如果n超出范围,则抛出out_of_range 异常 |
3.3 删除元素
c.pop_back() | 删除容器c的最后一个元素。如果c为空,则未定义。返回void |
c.pop_front() | 删除容器c的第一个元素。如果c为空,则未定义。返回void |
c.erase(p) | 删除迭代器p指向的元素,返回指向被删除元素后一位的迭代器。 |
c.erase(b,e) | 删除迭代器范围为[b,e)的元素,返回指向被删除元素后一位的迭代器。 |
c.clear() | 删除c的所有元素,返回void |
3.4 forward_list的专用操作
对于单向链表来说,插入或删除元素,会改变其前面一个元素中的指针。所以它的插入和删除操作不同于其他容器。
lst.before_begin() lst.cbefore_begin() | 指向链表开头前一个位置的迭代器,该迭代器指向的不是链表中的某个元素, 不能解引用。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所指元素的后面插入相应的元素。 |
emplace_after(p, args) | 在迭代器p所指元素的后面,根据args构造一个新的元素 |
lst.erase_after(p) lst.erase_after(b,e) | 删除在迭代器p所指的,或范围为[b,e)的元素。返回被删除元素的后继的迭代器。 |
3.5 改变容器大小
容器(除了array)可以使用resize来改变大小。
c.resize(n) | 重新定义c的大小为n。如果n < c.size(),删除多余的元素。 否则添加新的元素(值初始化) |
c.resize(n,t) | 重新定义c的大小为n。增加的新元素值为t |
如果容器保存的是类类型元素,当resize使得增加元素时,会调用该类的默认构造函数,如果没有默认构造函数,则必须提供与构造函数相应的初始化值。
4. vector如何增长
vector的内存管理是,根据一定的策略事先分配一段内存空间,当元素个数增长到空间不够用时,再分配新的内存空间,然后将元素搬到新的空间,再释放旧的空间。
c.shrink_to_fit() | 将capacity()缩小到与size()相等 |
c.capacity() | 容器c的容量 |
c.reserve(n) | 使c的容量至少可以保存n个元素 |
【c++11】
新标准加入的shrink_to_fit使得可以释放容器中多余的容量。适用于deque, vector和string。
增长测试:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> vi;
cout << "size: " << vi.size() << "; capacity: " << vi.capacity() << endl;
for(vector<int>::size_type idx = 0; idx <= 50; ++idx) {
vi.push_back(idx);
cout << "size: " << vi.size() << "; capacity: " << vi.capacity() << endl;
}
return 0;
}
5. string操作
略过。
6. 容器适配器
适配器是一个通用概念。包括容器适配器,迭代器适配器和函数适配器。
适配器是一种使一种对象按另一种对象的规则操作的机制。
除了前面几节讲到的顺序容器,C++库还定义了三种顺序容器适配器:stack, queue和priority_queue。
定义一个适配器
默认情况下,stack和queue在deque上实现,priority_queue在vector上实现。
stack<int> stk;
也可以指定用来实现的容器:
stack<int, vector<int>> stk_vec
实现适配器的容器是有限制的。所有的适配器都要求有添加和删除元素的能力。所以array不能使用。所以的适配器都要求在容器的末尾添加和删除元素,forward_list也不能使用。stack要求push_back, pop_back和back操作,可以用vector, deque, list实现。queue要求back, push_back, front和push_front操作,可以用list, deque实现。priority_queue除了要求front, push_back和pop_back 操作,还要求随机访问,所以可以用vector 或 deque实现。