顺序容器
在前面我们已经了解到,容器就是某类对象的集合。
顺序容器 访问元素的顺序与元素加入时的位置相对应,即顺序访问元素,但同时向容器中添加和删除元素、非顺序访问元素都要付出代价。
一、顺序容器概述
我们所熟悉的vector、deque(双端队列)、list(双向链表)、forword_list(单向链表)、array(固定大小的数组)、string、堆、栈等都是顺序容器。
新标准库的容器比旧版本要快的多,性能几乎与最精心优化过的同类数据结构一样好(一般会更好)。因此C++程序应该使用标准库容器,而不是原始数据结构。
通常情况下,使用vector是最好的选择,除非有更好的理由选择其他容器。
二、容器库概览
容器类型上的操作形成了一种层次:
- 所有容器都提供的操作
- 仅针对顺序容器的操作、仅针对关联容器的操作、仅针对无序容器的操作
- 只适用于一小部分容器的操作
接下来,我们先了解所有容器都提供的操作。
一般来说,每个容器都定义在与类型同名的头文件中,并且容器都定义为类模板。
#include <deque>
#include <list>
deque<double> d;
list<list<string>> s;
我们几乎可以将所有类型保存在容器中,但是对于某些类型,一些容器的操作是不可用的。
例如:顺序容器可以接受一个容器大小的参数来初始化,初始化时会调用该类型的默认构造函数,若类型没有默认构造函数,则不能用这种初始化容器的方式。
vector<type> a(5,typedata); //正确
vector<type> b(5); //错误
//type无默认构造函数
. | 所有容器都提供的操作 |
---|---|
类型别名 | . |
iterator | 容器类型的迭代器 |
const_iterator | 只读迭代器 |
size_type | 容器大小 |
value_type | 元素类型 |
reference | 元素引用 |
const_reference | const元素引用 |
构造函数 | . |
Type t; | 默认构造函数 |
Type t1(t2); | 拷贝t2构造t1 |
Type t(b,e); | 将迭代器b和e指向区间的元素拷贝导t3 |
Type t{a,b,c,…}; | 列表初始化 |
swap | . |
a.swap(b); | 交换ab |
swap(a,b); | 交换ab |
大小 | . |
c.size(); | 元素个数(不支持forward_list) |
c.maxsize(); | 可保存的最大元素个数 |
c.empty(); | 判断是否为空容器 |
添加/删除元素(array除外) | . |
c.insert(args); | 将元素拷贝到c |
c.emplace(inits); | 由inits构造c的元素 |
c.erase(args); | 删除args指定的元素 |
c.clear() | 清除所有元素,返回void |
关系运算符 | . |
==、!= | 所有容器都支持 |
<、<=、>、>= | 无序关联容器不支持 |
获取迭代器 | . |
c.begin()、c.end() | 首元素、尾元素下一位置的迭代器 |
c.cbegin()、c.cend() | const_iterator |
反向容器的额外成员 | . |
reverse_iterator | 逆序寻址的迭代器 |
const_reverse_iterator | |
c.rbegin()、c.rend() | 首元素之前的元素、尾元素 |
c.crbegin()、c.crend() | 返回const_reverse_iterator |
接下来我们对上表中的操作做进一步的解释。
1. 容器定义和初始化
需要注意的是,当调用容器的默认构造函数时,会创建一个空的对象。除了array类型(大小固定),array会按元素类型进行初始化元素。
只有顺序容器(不包括array)的构造函数可以接受大小参数。
. | |
---|---|
Type t(n); | 该构造函数是explicit的 |
Type t(n,value); |
① 将一个容器初始化为另一容器的拷贝
直接拷贝
Type t1(t2);
。两容器类型及元素类型需相同。迭代器范围拷贝(array不可用)
Type t(b,e);
。此时,不要求两容器类型相同,也不要求元素类型相同,可以转化即可。其中,b与e为新对象的begin与end,表示首元素和尾元素的下一元素。
list<string> authors = { "Fancy", "Naomi","Blue" };
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());//正确
② 标准库array
标准库array是大小固定的数组,因此,对象的大小也是类型的一部分,在定义array对象时,必须给出大小。
array<int,5> a;
array<string,10> b;
我们无法对内置数组进行拷贝,但是对于array对象可以
int a[5] = {1,2,3,4,5};
int b[5] = a;//错误
array<int,5> a{1,2,3,4,5};
array<int,5> b(a); //正确
③ 赋值和swap
c1 = c2;
若两边容器大小不同,则赋值后,c1大小等于c2大小。
swap(c1,c2);
swap比元素拷贝快的多,因为不换元素换迭代器(array换元素)。
赋值操作要求等号左右对象类型必须相同,因此定义了assign,对于除了array之外的顺序容器,可使用assign进行类型不同但可相互转化的赋值操作。
. | |
---|---|
seq.assign(b,e); | 将seq替代,b和e不可指向seq的元素 |
seq.assign(list); | 将seq替代为列表list中的元素 |
seq.assign(n,value); | 将seq替代为n个value |
list<string> names;
vector<const char*> oldStyle;
names = oldStyle;//错误
names.assign(oldStyle.begin(), oldStyle.end());//正确
需要注意的是,赋值操作会使左边容器内部的迭代器、引用、指针失效,而swap则不会(array、string除外)。
三、顺序容器操作
1. 向顺序容器添加元素
除array外所有标准库容器都提供了灵活的内存管理,在运行时可以动态添加、删除元素来改变容器大小。
. | |
---|---|
c.push_back(value) | 尾部创建值为value的元素,返回void |
c.emplace_back(args) | 尾部创建由args创建的元素,返回void |
c.push_front(value) | 头部创建值为value的元素,返回void |
c.push_front(args) | 头部创建由args创建的元素,返回void |
c.insert(p,value) | 在迭代器p指向元素前添加值为value的元素,返回新添加元素的迭代器 |
c.emplace(p,args) | 在迭代器p指向元素前添加由args创建的元素,返回新添加元素的迭代器 |
c.insert(p,n,value) | 在迭代器p指向元素前添加n个值为value的元素,返回新添加的第一个元素的迭代器;n = 0,则返回p |
c.insert(p,b,e) | 在迭代器p指向元素前添加b和e指向区间的元素,返回新添加的第一个元素的迭代器;若范围为空,则返回p |
c.insert(p,list) | list是花括号元素列表,在迭代器p指向元素前添加list,返回新添加的第一个元素的迭代器;若列表为空,则返回p |
其中,vector和string不支持push_front 和 emplace_front。
forward_list不支持push_back和emplace_back,并且有自己专有版本的insert和emplace。
向一个vector、string(内存重新分配)和deque(插入中间位置)插入元素会使所有指向容器的迭代器、引用和指针都失效。而list和forward_list会一直有效。
使用insert:
vector<string> svec;
list<string> slist;
svec.insert(svec.begin(), "Hello!");//慢
svec.insert(svec.end(), 3,"Thank you!");
slist.push_front("Hello!");
slist.insert(slist.end(), svec.begin() + 1, svec.end());
slist.insert(slist.end(), { "Thank","You","Very","Much!" });
for (auto c : svec)
cout << c << ' ';
cout << endl;
for (auto c : slist)
cout << c << ' ';
cout << endl;
使用insert的返回值:
list<string> name;
auto it = name.begin();
string word;
while (cin >> word ) {
it = name.insert(it, word); //相当于push_front
}
对于push_back、push_front、insert而言,传递的参数是容器元素类型的对象,将对象拷贝到容器中。
而emplace_back、emplace_front、emplace,传递的参数是构造容器元素类型对象的值,在容器管理的内存直接构造元素。
class Naomi {
private:
string name;
unsigned int age;
public:
Naomi() = default;
Naomi(string n,int a):name(n),age(a){}
string GetName() { return name; }
unsigned int GetAge() { return age; }
};
list<Naomi> naomi;
naomi.emplace(naomi.end(),"fancy", 18);//调用Naomi的构造函数
naomi.emplace_back(Naomi("sixday", 18));
for (auto c : naomi)
cout << c.GetName() << c.GetAge() << ' ';
cout << endl;
2. 访问元素
. | |
---|---|
c.front( ) | 首元素的引用,容器为空未定义 |
c.back( ) | 尾元素的引用(forward_list不支持),容器为空未定义 |
c[n] | 下标为n的元素的引用,越界未定义 |
c.at(n) | 下标为n的元素的引用,越界抛出异常 |
其中,forward_list不支持back( )函数,at和下标只适用于vector、string、deque、array。
在调用front和back之前,一定要确保容器非空,否则像越界访问一样严重错误。
需要注意的是,访问成员函数返回的是引用。
vector<int> num = { 0,4,3,0 };
if (!num.empty()) { //确保容器非空
num.front() = 1; //改变
auto &k = num.back(); //改变
k = 1;
auto g = num.back(); //不改变
g = 2;
}
3. 删除元素
与添加元素类似,除array外的其他容器也有删除元素的方式。
. | |
---|---|
c.pop_back() | 删除尾元素,容器为空未定义,返回void |
c.pop_front() | 删除首元素,容器为空未定义,返回void |
c.erase(p) | 删除迭代器p所指的的元素,返回被删元素后第一个元素的迭代器。若被删元素为尾元素,则返回尾后迭代器(off-the-end) |
c.erase(b,e) | 删除迭代器b和e所指范围内的元素,[b,e) |
c.clear() | 清空容器 |
其中,forward_list不支持pop_back,并且有特殊的erase,并且vector、string不支持pop_front。
若删除vector、string中元素,元素之后的迭代器、引用和指针都会失效。若删除deque非首尾位置的元素容器的迭代器、引用和指针都会失效。
4. 特殊的forward_list操作
为什么forward_list(单向链表)这么特殊呢?
因为,当我们在单向链表的非头位置添加或删除元素时,该位置前驱元素的指针域都要改变,而我们没有简单的方法去获取一个元素的前驱,因此之前所定义的“- -”、insert、erase、push_back、pop_back对于单向链表来说效率很低。
因此,我们只能通过访问前驱元素来进行操作,forward_list提供了before_begin(首前迭代器),指向单链表首元素之前的位置。
. | |
---|---|
(c)before_begin | 首前迭代器,不能解引用 |
fl. insert_after(p,value) | 在迭代器p之后插入value,返回插入元素的迭代器 |
fl. emplace_after(p,args) | 在迭代器p之后插入由args构造的元素,返回插入元素的迭代器 |
fl. insert_after(p,n,value) | 在迭代器p之后插入n个value,返回最后插入元素的迭代器 |
fl. insert_after(p,b,e) | 在迭代器p之后插入[b,e),返回最后插入元素的迭代器 |
fl. insert_after(p,list) | 在迭代器之后插入list列表,返回最后插入元素的迭代器 |
fl. erase_after(p) | 删除迭代器p之后的元素,返回被删元素之后的迭代器 |
fl. erase_after(b,e) | 删除(b,e),返回被删元素之后的迭代器 |
由上表可见,一般容器的insert操作,是在迭代器参数指向元素之前插入,而insert_after是在迭代器参数指向元素之后插入;一般容器的erase操作,是删除迭代器参数指向的元素,而erase_after删除迭代器参数指向元素之后的元素。
在使用forward_list进行操作时,要关注两个迭代器,一个指向当前元素,一个指向前驱元素。
forward_list<int> flst = { 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 == 0) curr = flst.erase_after(prev);
else {
prev = curr;
curr++;
}
}
//去除flst中的偶数
5. 改变容器大小
除array外的其他容器是可以改变大小的。
. | |
---|---|
c.resize(n) | 调整容器大小为n,若n < c.size(),则删除多余元素;若n > c.size(),则对新元素进行默认初始化 |
c.resize(n,value) | 调整容器大小为n,新元素初始化为value |
对vector、string、deque调整容器大小可能会导致迭代器、引用、指针失效。而缩小容器大小时,无论什么类型,所有指向被删除元素的迭代器、引用、指针均失效。
需要注意的是,resize 仅仅改变容器的元素, 不会改变容器的内存空间。
vector<int> number = { 1,2,3,4 };
number.resize(3);
for (auto c : number)
cout << c << ' ';
cout << endl; //输出 1 2 3
cout << number.capacity() << endl; //内存空间依然为4,不改变
number.resize(5);
for (auto c : number)
cout << c << ' ';
cout << endl; //输出 1 2 3 0 0
number.resize(7, 2);
for (auto c : number)
cout << c << ' ';
cout << endl; //输出 1 2 3 0 0 2 2