您的位置:首页 > 编程语言 > C语言/C++

C++基础的不能再基础的学习笔记——顺序容器(基本操作)

2018-01-31 18:04 726 查看

顺序容器

在前面我们已经了解到,容器就是某类对象的集合

顺序容器 访问元素的顺序与元素加入时的位置相对应,即顺序访问元素,但同时向容器中添加和删除元素、非顺序访问元素都要付出代价。

一、顺序容器概述

我们所熟悉的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_referenceconst元素引用
构造函数.
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的元素的引用,越界未定义
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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: