1、概述
1、顺序容器,顾名思义,就是能支持快速顺序访问元素的能力。根据实现不同,在以下两个方面有所差异
1)添加、删除元素的代价
2)非顺序访问的代价。
2、对比
注:
双端队列:双端队列(deque,全名double-ended queue)是一个限定插入和删除操作的数据结构,具有队列和栈的性质。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。
双端队列是限定插入和删除操作在表的两端进行的线性表。这两端分别称做端点1和端点2。也可像栈一样,可以用一个铁道转轨网络来比喻双端队列。
3、与内置数组比,array是更安全、更易于使用的数组类型。
4、forward_list的目标是达到与最好的手写单向链表结构相当的性能。它没有size操作。
5、强调:C++新库的容器性能非常好。
6、容器选择基本原则
1)通常,vector是最好的选择。---多用vector。
2)如果程序有很多小元素,且空间的额外开销很重要,则不要使用list或forward_list。
3)如果要求随机访问,应该使用vector或deque
4)如果要求在中间插入、删除数据,则用list或forward_list。
5)如果要在头、尾插入,但不会在中间插入,则用deque
6)如果只有在读取输入时才需要在容器中间位置插入数据,随后访问需要随机,则:
确认是否真需要在中间插入。例如,如果要求插入的数据排序,在插入时找位置,不如插入完毕,一次用sort函数来排序。
如果必须插入,可以先入list,完毕后拷贝到vector。相当于两两配合,结合使用。
7)最佳实践:如果不确定使用哪种,则推荐使用vector和list。公共的操作:使用迭代器,避免随机访问。
2、容器库
1、每个容器的头文件,与容器名相同。容器都是模板类。
定义例子:
list<sales> //保存sales对象的list
deque<double> //保存double的deque
2、顺序容器可保存任意类型的元素,还可以嵌套,即一个容器,其元素是另一个容器。
vector<vector<string>> lines;
3、不同容器也有一些自己的要求。容器操作列表(太长,不一一列举了,讲到时候看吧):
1)类型别名
iterator:此容器类型的迭代器类型
const_iterator:只读迭代器。
size_type:足够保存此种容器类型最大可能容器的大小。
difference_type:两个迭代器之间的距离
value_type:元素类型
reference:元素左值类型(啥意思?后面看有没有解释),与value_type&含义相同。
const_reference :元素const左值类型(const value_type&)。
2)构造函数
C c; 默认构造函数,构造空容器
C c1(c2); 构造c2的拷贝c1
C c(b,e); 构造c,将迭代器b,e之间的元素拷贝到c(array不支持)
C c{a,b,c,...}; 列表初始化
.....抄这个太麻烦,还是先看有用的,再抄吧。
2.1、迭代器
1、在3.4节介绍了迭代器的基本内容,这里是拔高一下。所以先得补充一下3.4的一些基础知识。
2、迭代器提供对对象的间接访问。其对象就是容器中的元素或string对象中的字符。(string特别拿出来说事情)
3、迭代器可以移动,就是指向不同的元素
1)有效:指向某个元素,或者容器中尾元素的下一个(这个特别,但有效)
2)无效:其他情况,就是没指向
4、有迭代器的类型可返回迭代器成员,如begin(返回第一个元素)和end(指向尾元素之后,没啥意义,仅仅是一个标志)成员。
auto b=v.begin(),e = v.end();
容器尾空时,begin=end。
5、几个迭代器操作
*iter 返回迭代器iter所指向元素的引用(有点像指针)
iter->mem 解引用iter并获取该元素名为mem的成员,等价于(*iter).mem
++/-- iter:向前、向后移动iter
iter1==iter2/iter1!=iter2:判断相同或不同
6、例子,把string的第一个字母改为大写
string s("some thing");
if(s.begin() != s.end())//判断s不为空
{
auto it = s.begin();//指向首位的迭代器
*it = toupper(*it); //赋值
}
另一个例子,结合迭代器移动
for(auto it = s.begin(); it!=s.end()&&!isspace(*it);++it)//isspace,判断字符不为空字符
*it=toupper(*it);
注意:所有迭代器都定义了==和!=,但不一定都定义了<,所以判断是否到尾部,用it!=s.end(),比it<s.end()要合理。
6、迭代器类型
1)一般来说,不能也无需知道迭代器的精确类型。标准库使用iterator和const_iterator来表示迭代器的类型。
vector<int>::iterator it;//it能读写vector<int>的元素
string::iterator it2;//it2能读写string对象中的字符
vector<int>::const_iterator it3;//it3只能读元素,不能写元素
string::const_iterator i4;//it4只能读字符,不能写字符。
7、begin和end运算符
概念上面已经说了,这里讲点其他的
1)如果对象是常量,begin和end返回const_iterator;否则,返回iterator。
2)但有时这种个默认行为不太好。用cbegin和cend直接获取常量迭代器
8、(*it).empty(); //先对it解引用,再判断其元素是否为空。
9、也可以用->,把解引用和访问成员放在一起。
10、任何改变vector对象容量的操作,都会导致迭代器失效。
11、迭代器运算,支持:
iter +/-n
iter +/-=n
iter1-iter2
>,>=,<,<=
-------------------
以下回归到第九章
8、迭代器闭合区间[begin end)
1)如果begin==end,则范围为空
2)如果不等,则至少包含一个元素
3)递增begin,可到达end
2.2容器类型成员(看成是用于容器的专用类型,定义变量使用)
size_type:看做无符号整数,表示容器大小。机器无关。
iterator:
const_iterator:
反向迭代器:与正向迭代器相比,操作含义发生了点到。对反向迭代器++,会得到上一个元素。
value_type:元素类型
reference/const_reference:元素类型的引用(value_type& /const value_type&)
为需要使用这些类型,必须显式使用其类名。
list<string>::iterator iter;
vector<int>::difference_type count;
2.3begin 和end成员
这个前面讲多了。记录几个关键信息
1)r版本返回反向迭代器,c版本返回const迭代器
2)可以将一个普通的iterator转换为const_iterator,反之不行。
2.4 容器定义和初始化
1、每个容器定义一个默认构造函数。除array之外,其他容器默认构造函数都会创建一个指定类型的空容器,且都可以可接受指定容器大小和元素初始值的参数。
2、操作列表
拷贝构造
C c; 默认构造函数。如果C是一个array,则c中元素按默认方式初始化(不太理解);否则c为空。
C c1(c2);
C c2 = c2; c1初始化为c2的拷贝。c1c2是相同类型的容器,且保存相同的元素类型。对于array类型,还要求容器大小相同。
C c(b,e) c初始化为迭代器b和e指定范围中的元素拷贝。类型必须相容(注意,这里和上面的拷贝初始化有一些区别,没有要求类型、元素类型相同)。array不适用。
list<string> authors = {"aaa","bbb","ccc"};
vector<const char*> articles = {"a","an","the"}
list<string> list2(authors);//正确,类型、元素类型相同,用拷贝构造
deque<string> list3(authors); //错误,容器类型不同。
vector<string> words(articles);//错误,容器类型不匹配
//正确,const char*可转换为string,相容
forward_list<string> words(articles.begin(),articles.end());
//拷贝构造,直到(但不包括)it)
deque<string> authList(authors.begin(),it);
C c{a,b,c,...}
C c={a,b,c...} c初始化为列表中元素的拷贝。列表中元素类型必须与C相容(这里是相容即可)。对于array,列表中元素数目必须小于等于array大小。任何遗漏的都进行值初始化(内置类型,为0;否则默认初始化)
列表初始化,隐式指定了容器大小(array除外)。
只有顺序容器(不包括array)的构造函数才能接受大小参数
C seq(n) seq包含n个元素。元素进行了值初始化;次构造函数是explicit的。(string不适用)
C seq(n,t) n个初始化为t的值。
vector<int> ivec(10,1);//10个int,每个都初始化为1
list<string> svec(10,"aa");//10个string,每个都是“aa”
forward_list<int> ivec(10);//10个元素,每个都是0
deque<string> sevc(10);//10个元素,每个都是空string
如果元素类型是内置类型或有默认构造函数,只传容器大小参数即可。否则,需要指定初值。
3、array
array比较特殊。因为array大小是不能变得,所以除了指定类型,还指定大小。
array<int, 30>
array<string,10>
大小是array类型的一部分。
array<int,10>::size_type i;//如果没有10这个参数,就错误了。
array<int,10> ia1;//10个int ,默认值
array<int,10> ia2={0,1,2,3,4,5,6,7,8,9};//元素个数要与定义一致
array<int,10> ia3={1,2};//前两个用列表值初始化,后面都为0
不能对内置类型数组拷贝,但可以对array进行拷贝,对比以下:
int digs[10]={0,1,2,3,4,5,6,7,8,9};
int digs2[10]=digs;//错误,内置类型数组不能拷贝
array<int,10> digits={0,1,2,3,4,5,6,7,8,9};
array<int,10> copy =digits;//类型匹配即可拷贝赋值
2.5赋值和swap
1、赋值运算符将左边容器中的全部元素替换为右边容器中元素的拷贝。注意:左边容器的大小会随赋值,变成与右边一致。
2、assign(仅顺序容器,且array除外)
解决不相同但相容的赋值问题。也可以赋值子序列。参数也需要调用迭代器。有两种参数形式。
list<string> name;
vector<const char*> oldstyle;
names = oldstyle;//错误,类型不相同
names.assign(oldstyle.cbegin(),oldstyle.cend());//正确,类型相容。
也可以接受一个整形和一个元素值。
list<string> slist1(1);//1个元素,空string
slist1.assign(10,"123");//10个元素,都是123
3、swap
1)交换实际是交换两个容器的数据结构(array是异类。)
vector<string> svec1(10);
vector<string> svec2(20);
swap(svec1,svec2)
此时,svec1长度20,svec2是10,交换了。
2)除array外,swap不对任何元素进行拷贝、删除或插入,可保证在常数时间完成。
元素不会被移动:除string外(string特殊,相关成员会失效),指向容器的迭代器、引用和指针在swap之后都不会失效,仍然指向swap操作之前所指向的那些元素。
array也是个例外:它真正会交换所有元素(有点傻,所以性能与array大小有关)。但迭代器、引用和指针还是与绑定元素保持关联。
统一使用非成员版本是好习惯。
2.6容器大小
1、1个例外,其容器都有三个大小相关操作
size:返回容器中数目。例外就是forward_list不支持。
empty:判断容器是否为空
max_size:返回大于等于该类型容器所能容纳最大元素数的值
2.7关系运算符
1、==/!=/>/</>=/<=:相同类型+相同类型元素
2、所谓比较,就是对元素进行一一对比。
一般有这种需求吗?
3、顺序容器操作
3.1向顺序容器添加元素
1、几个约束
1)array显然不能再添加了,其他都可以。
2)forward_list有自己专有版本的insert和emplace
3)forward_list不支持push_back和emplace_back
4)vector和string不支持push_front和emplace_front。
5)向vector、string或deque插入元素会导致指向容器的迭代器、引用和指针失效。
2、push_back:追加元素到尾部
string word;
while(cin>>word)
container.push_back(word);//从输入获取字符串,向尾部追加
string是一个字符容器,也可以这样操作
string str;
str.push_back('a');//等同于str+='a'
注意:当用一个对象来初始化容器,或将一个对象插入到容器中时,实际放入容器中的是对象的拷贝,而不是对象本身。放入后,两者之间无其他关联,相互不影响。
3、push_front:插入到头部
4、insert:插入到特定位置
5、emplace:构造而不是拷贝元素。
---上面这些操作的细节,在真正使用时查询即可,不在这里记录了。
3.2访问元素
1、访问元素操作
at和下标只适用于string、vector、deque、array
back不适用于fordward_list
c.back() :尾元素的引用,若c为空,函数行未定义
c.front():首元素,一样,如果为空,未定义
------所以,使用之前需要对c.empty()做判断
c[n]:下标访问,越界后,未定义。
c.at[n]:也是下标访问,不过比c[n]多的是,越界后会抛出out_of_range异常。所以,为确保安全,可以用at。
3.3 删除元素
1、一样,array不适用
2、forward_list有特殊版本的erase(有加就有删)
3、forward_list不支持pop_back;vector 和string不支持pop_front(也是与前面添加对应)
4、c.pop_back()/c.pop_front():删除尾/首元素,如果c为空,未定义
5、c.erase(p): 删除迭代器p所指定的元素。
6、c.erase(b,e):删除迭代器所指范围
7、c.clear():删除所有元素
注意:删除前,要自己检查元素存在。
3.4 forward_list的特殊性
1、因为这家伙是个单向链表,所以需要独特处理。因为是单向,所以只能站在前面处理后面的,所有都是一系列的after。
lst.before_begin():有点类似迭代器end,返回最后元素之后,这个是首元素之前。实际是想用他的next的概念。
lst.cbefore_begin()
lst.insert_after(p,t)/lst.insert_after(p,n,t)/lst.insert_after(p,b,e)/lst.insert_after(p,i1)
emplace_after(p,args)
lst.erase_after(p)/lst.erase_aftger(b,e)
例如:传递erase_after参数prev,则该调用将prev之后的元素删除。返回值指向序列中下一个元素。prev保持不变。
3.5改变容器大小
1、array不支持。改小,容器后面的元素被删除。改大,新元素增加在后面。
c.resize(n)
c.resize(n,t)
如果为提供元素值参数,则采用值初始化。如果容器保存的是类类型元素,且是增大容器,必须提供初始值/默认构造函数。
2、注意:会造成迭代器失效。个人觉得,不需要记这么细致,都按照list/forward_list有效、其它无效来处理保险,重新定位。尾部迭代器更有问题,不要保存,每次重新调用。
添加元素:
vector/string:空间重新分配,则迭代器、指针、引用都失效;未重新分配,插入位置之前的有效,之后的失效(这种情况下,实际也不好用了。当失效处理会保险一点)
deque:插入除首位位置,迭代器、指针、引用都会失效。在首尾添加元素,迭代器失效,其他两个还有效。
list/forward_list:都有效
删除元素:
list/forward_list:都有效
deque:首尾之外删除,都失效;删除deque尾元素,则尾后迭代器失效,但其他不受影响;如果删除首元素,都不收影响。
vector/string:被删除元素之前的有效。当删除元素时:尾迭代器总是无效。
4、vector对象是如何增长的
1、对vector和string,要保证存储空间的连续性。在空间不够时,会申请更大的空间,预留一些,以提升性能。
2、几个操作,体现出vector/string的预分配的概念
c.shrink_to_fit() 将capacity()减小到等于size()
c.capacity() 不重新分配空间的话,可保存的最大元素数
c.reserve(n) 分配至少能容纳n个元素的空间
5、额外的string操作
大概了解一下,用的时候查就可以了。
1、构造方法---直接看例子就可以了。
const char *p = "hello world!!";//以空字符结束
char noNULL[]={'h','i'}; //不以空字符结束
string s1(cp);//拷贝cp,遇到空字符停止 s1=="hello world!!"
string s2(noNULL,2);//从noNULL拷贝2个字符 s2=="hi"
string s3(noNULL);//错误,因为noNULL不是以空字符结束
string s4(cp+6,5);//从cp[6]开始拷贝5个字符。 s4=="world"
string s5(s1,6,5);//从s1[6]开始拷贝5个字符 s5=="world"
string s6(s1,6);//从s1[6]开始,拷贝到末尾 s6="world!!"
string s7(s1,6,20);//从s1[6]开始,只拷贝到末尾 s7=="world!!"
string s8(s1,16);//out_of_range异常
2、substr
s.substr(pos,n); 返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值是0,n的默认值是s.size()-pos,即拷贝从pos开始的所有字符。
3、支持顺序容器的赋值运算符、assign、insert、erase外,还有接受下标的版本。
----具体后面再实验
4、append和replace函数
append:在string尾部插入。可以用insert插入最后,也可以直接用append。
string s("C++ primer"),s2=s;
s.insert(s.size(),"5th ."
s2.append(" 5th.");//s==s2
replace:替换。
5.3string搜索
s.find(args); s中找args第一次出现的位置
s.rfind(args); s中args最后一次出现的位置
s.find_first_of(args); 在s中查找args中任何一个字符第一次出现的位置
s.find_last_of(args);在s中查找args中任何一个字符最后一次出现的位置
s.find_first_not_of(args); 在s中查找第一个不在args中的字符
s.find_last_not_of(args);在s中查找最后一个不在args中的字符
args格式:
c,pos 从s中位置pos开始查找字符c。pos默认为0
s2,pos 从s中位置pos开始查找字符串s2.pos默认为0
cp,mpos 从s中位置pos开始查找指针cp指向的以空字符结尾的C风格字符串。pos默认为0
cp,pos,n 从s中位置pos开始查找指针cp指向的数组前n个字符。pos和n无默认值。
5.4compare比较函数
到时候查
5.5数值转换
到时候查
6、容器适配器
1、标准库还定义了三个顺序容器适配器:stack、queue、priority_queue。
2、本质上,适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。
3、stack适配器接受一个顺序容器(array和forwared_list除外),使其操作像一个stack
可以理解成,用顺序容器,实现了新功能的容器,有对应的操作。
---------------------------
本章基本到此,细节用的时候还需要查询。
2018.5.16 22:07
深圳