顺序容器 |
|
容器适配器 |
|
---|---|---|---|
vector |
支持快速随机访问 |
stack |
后进先出(LIFO) |
list |
支持快速插入/删除 |
queue |
先进先出(FIFO) |
deque |
双端队列 |
priority_queue |
有优先级管理的队列 |
1、容器类型的操作集合形成了以下层次结构:
1)一些操作适用于所有容器类型;
2)另外一些操作只适用于顺序或关联容器;
3)还有一些操作只适用于顺序或关联容器类型的一个子集。
2、容器构造函数
C<T>c; |
创建一个名为c的空容器。C是容器类型名,如vector,T是元素类型,如int或string适用于所有容器 |
Cc(c2); |
创建容器c2的副本c;c和c2必须具有相同的容器类型,并存放相同类型的元素。适用于所有容器。 |
Cc(b,e); |
创建c,其元素是迭代器b和e标示的范围内元素的副本。 适用于所有容器 |
Cc(n, t); |
用n个值为t的元素创建容器c,其中值t必须是容器类型C的元素类型的值,或者是可转换为该类型的值。 只适用于顺序容器 |
Cc(n); |
创建有n个值初始化元素的容器c 只适用于顺序容器 |
3、将一个容器初始化为另一个容器的副本
将一个容器复制给另一个容器时,类型必须匹配:容器类型和元素类型都必须完全相同。
4、初始化为一段元素的副本
系统允许通过一对迭代器间接实现不同种容器之间进行复制:使用迭代器时,不要求容器类型相同,容器内元素类型也可以不相同,只要他们相互兼容,能够将要复制的元素转换为所构建的新容器的元素类型,即可实现复制。
5、分配和初始化指定数目的元素
不提供元素初始化式时,标准库将为该容器实现值初始化,采用这种类型的初始化,元素类型必须是内置或复合类型,或者是提供了默认构造函数的类类型。如果元素类型没有默认构造函数,则必须显式的指定其元素初始化式。
接受容器大小做形参的构造函数只适用于顺序容器,而关联容器不支持这种初始化。
6、容器内元素的约束
C++语言中,大多数类型都可用作容器的元素类型。容器元素类型必须满足
以下两个约束:
•元素类型必须支持赋值运算。
•元素类型的对象必须可以复制。
容器操作的特殊要求
支持复制和赋值功能是容器元素类型的最低要求。此外,一些容器操作对元素类型还有特殊要求。如果元素类型不支持这些特殊要求,则相关的容器操作就不能执行:我们可以定义该类型的容器,但不能使用某些特定的操作。
有在同时指定每个元素的初始化式时,才能使用给定容器大小的构造函数来创建同类型的容器对象。
7、容器的容器
因为容器受容器类型的约束,所以可定义元素是容器的容器:
【注意:】
在指定容器元素为容器类型时,必须如下使用空格:
所有标准库都提供的迭代器运算 |
|
---|---|
*iter |
返回迭代器iter所指向的元素的引用 |
iter-> mem |
对iter进行解引用,获取指定元素中名为mem的成员。等效于(*iter).mem |
++iter/iter++ |
给iter加1,使其指向容器里的下一个元素 |
--iter/iter-- |
给iter减1,使其指向容器里的前一个元素 |
iter1== iter2 iter1!= iter2 |
比较两个迭代器是否相等(或不等)。当两个迭代器指向同一个iter2容器中的同一个元素,或者当它们都指向同一个容器的超出末端iter1!=的下一位置时,两个迭代器相等. |
vector和deque类型迭代器支持的操作 |
|
---|---|
iter+ n iter- n |
在迭代器上加(减)整数值n,将产生指向容器中前面(后面)第n个元素的迭代器。新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一位置 |
iter1+= iter2 iter1-= iter2 |
这里迭代器加减法的复合赋值运算:将iter1加上或减去iter2的运算结果赋给iter1 |
iter1- iter2 |
两个迭代器的减法,其运算结果加上右边的迭代器即得左边的迭代器。这两个迭代器必须指向同一个容器中的元素或超出容器末端的下一位置 只适用于vector和deque容器 |
>,>=,<,<= |
迭代器的关系操作符。当一个迭代器指向的元素在容器中位于另一个迭代器指向的元素之前,则前一个迭代器小于后一个迭代器。关系操作符的两个迭代器必须指向同一个容器中的元素或超出容器末端的下一位置 只适用于vector和deque容器 |
关系操作符只适用于vector和deque容器,因为只有这两种容器为其元素提供快速、随机的访问。
list容器的迭代器既不支持算术运算符,也不支持关系运算符,它只是提供前置/后置的自增、自减运算以及相等/不等运算。
1、对形成迭代器范围的迭代器的要求
迭代器 first和last如果满足以下条件,则可形成一个迭代器范围:
•它们指向同一个容器中的元素或超出末端的下一位置。
•如果这两个迭代器不相等,则对first反复做自增运算必须能够到达last。换句话说,在容器中,last绝对不能位于first之 前。【P270,编译器自己也不能保证上述要求】
2、使用左闭右开区间的意义
1)当first与last相等,迭代器范围为空
2)当first与last不相等时,迭代器范围内至少有一个元素:size= last – fist
3、使迭代器失效的容器操作
一些容器的操作会修改容器的内在状态或移动容器内的元素。这样的操作使所有指向被移动的元素的迭代器失效,也可能同时使其他迭代器失效。使用无效迭代器是没有定义的,可能会导致与悬垂指针相同的问题。使用无效迭代器将会导致严重的运行时错误。
无法检查迭代器是否有效,也无法通过测试来发现迭代器是否已经失效。任何无效迭代器的使用都可能导致运行时错误,但程序不一定会崩溃,否则检查这种错误也许会容易些o(∩∩)o...。
【建议:】
使用迭代器时,通常可以编写程序使得要求迭代器有效的代码范围相对较短。 然后,在该范围内,严格检查每一条语句,判断是否有元素添加或删除,从而相应地调整迭代器的值。
引:
每种顺序容器都提供了一组有用的类型定义以及以下操作:
1)在容器内添加元素;
2)在容器中删除元素;
3)设置容器的大小;
4)(如果有的话)获取容器内的第一个和最后一个元素。
正文:
一、容器定义的类型别名
所有容器都提供的类型别名 |
|
---|---|
size_type |
无符号整型,足以存储容器类型的最大可能容器长度 |
iterator |
容器的迭代器类型 |
const_iterator |
容器的只读迭代器类型 |
reverse_iterator |
按逆序寻址元素的迭代器类型 |
const_reverse_iterator |
元素的只读逆序迭代器 |
difference_type |
足够存储两个迭代器差值的有符号整型,可为负数 |
value_type |
元素类型 |
reference |
元素的左值类型,是value_type&的同义词 |
const_reference |
元素的常量左值类型,等效于constvalue_type& |
逆序迭代器从后向前遍历容器,并反转了某些相关的迭代器操作,例如,在逆序迭代器上做++运 算将指向容器中的前一个元素。
表中最后三种类型使程序员无须直接知道容器元素的真正类型,就能使用它。需要使用元素类型时,只要用value_type即可。如果要引用该类型,则通过reference和const_reference类型实现。在程序员编写自己的泛型程序时,这些元素相关类型的定义非常有用。
使用容器定义类型的表达式看上去非常复杂:
二、begin和end成员
容器的begin和end成员 |
|
---|---|
c.begin() |
返回一个迭代器,它指向容器c的第一个元素 |
c.end() |
返回一个迭代器,它指向容器c的最后一个元素的下一位置 |
c.rbegin() |
返回一个逆序迭代器,它指向容器c的最后一个元素 而不是下一位置! |
c.rend() |
返回一个逆序迭代器,它指向容器c的第一个元素前面的位置 而不是第一个元素! |
上述每个操作都有两个不同版本:一个是const成员,另一个是非 const成员。这些操作返回什么类型取决于容器是否为const。如果容器不是const,则这些操作返回iterator或reverse_iterator类型。如果容器是 const,则其返回类型要加上const_前缀,也就是const_iterator和const_reverse_iterator类型。
三、在顺序容器中添加元素
在顺序容器中添加元素的操作 |
|
---|---|
c.push_back() |
在容器c的尾部添加值为t的元素。返回void类型 |
c.push_front() |
在容器c的前端插入值为t的元素。返回void类型。 只适用于list和deque容器类型。 |
c.insert(p,t) |
在迭代器p所指向的元素前面插入值为t的新元素,返回指向新添加元素的迭代器。 |
c.insert(p,n,t) |
在迭代器p所指向的元素前面插入n个值为t的新元素,返回void |
c.insert(p,b,e) |
在迭代器p所指向的元素前面插入迭代器b和e标记的范围内的元素,返回void。 |
【关键概念】容器元素都是副本
在容器中添加元素时,系统是将元素值复制到容器里。类似地,使用一段元素初始化新容器时,新容器存放的是原始元素的副本。被复制的原始值与新容器中的元素各不相关,此后,容器内元素值发生变化时,被复制的原值不会受到影响,反之亦然。【P274】
1、在容器中的指定位置添加元素
2、插入一段元素
3、添加元素可能会使迭代器失效
在vector容器中添加元素可能会导致整个容器的重新加载,这样的话,该容器所涉及到的所有迭代器都会失效!即使不需要重新加载整个容器,指向新插入元素后面的迭代器也会失效!
任何insert或push操作都可能导致迭代器失效,因此在编写循环将元素插入到vector或deque容器中时,程序必须确保迭代器在每次循环后都得到更新。
4、避免存储end操作返回的迭代器
在容器的任何位置插入任何元素都会使end迭代器失效!!!
四、关系操作符
所有的容器类型都支持用关系操作符来实现两个容器的比较。比较的容器必须具有相同的容器类型,而且其元素类型也必须相同。例如,vector<int> 容器只能与vector<int>容器比较,而不能与list<int>或 vector<double>容器比较。
容器的比较是基于容器内元素的比较。容器的比较使用了元素类型定义的同一个关系操作符:两个容器做!=比较使用了其元素类型定义的!=操作符。如果容器的元素类型不支持某种操作符,则该容器就不能做这种比较运算。
下面的操作类似于string类型的关系运算:
•如果两个容器具有相同的长度而且所有元素都相等,那么这两个容器就相等;否则,它们就不相等。
•如果两个容器的长度不相同,但较短的容器中所有元素都等于较长容器中对应的元素,则称较短的容器小于另一个容器。
•如果两个容器都不是对方的初始子序列,则它们的比较结果取决于所比较的第一个不相等的元素。
五、容器大小的操作
所有容器都提供的大小操作 |
|
---|---|
c.size() |
返回容器c中的元素个数。返回类型为c::size_type |
c.max_size() |
返回容器c可容纳的最多元素个数,返回类型为c::size_type |
c.empty() |
返回标记容器大小是否为0的布尔值 |
c.resize(n) |
调整容器c的长度大小,使其能容纳n个元素,如果n<c.size(), 则删除多出来的元素;否则,添加采用值初始化的新元素 |
c.resize(n,t) |
调整容器c的长度大小,使其能容纳n个元素。所有新添加的元素值都为t |
使用resize操作可能会使迭代器失效,在vector或deque容器上做的resize操作有可能会使得所有的迭代器都失效!
对于所有的容器类型,如果resize操作压缩了容器,则指向已删除的元素的迭代器会失效!
六、访问元素
如果容器非空,那么容器类型的front和back成员将返回容器的第一个和最后一个元素的引用。
【与begin和end的对比:】
1)begin和end返回容器类型的迭代器,而不是引用;
2)end返回容器最后一个元素的下一个位置的迭代器,而back返回容器的最后一个元素的引用!
访问顺序容器内元素的操作 |
|
---|---|
c.back() |
返回容器c的最后一个元素的引用,如果c为空,则该操作未定义 |
c.front() |
返回容器c的第一个元素的引用,如果c为空,则该操作未定义 |
c[n] |
返回下标n的元素的引用,如果n<0或n>=c.size(),则该操作未定义 只适用于vector和deque容器 |
c.at(n) |
返回下标为n的元素的引用,如果下标越界,则该操作未定义 只适用于vector和deque容器 |
使用下标运算的一个可选方案是使用at成员函数,虽然这个函数的行为和下标运算相似,但是如果程序给出的下标无效,at函数会抛出out_of_range异常。
七、删除元素
删除顺序容器内元素的操作 |
|
---|---|
c.erase(p) |
删除迭代器p所指向的元素 返回一个迭代器,它指向被删除元素后面的元素。如果p指向容器内的最后一个元素,则返回的迭代器指向容器的超出末端的下一位置。如果p本身就是指向超出末端的下一位置的迭代器,则该函数未定义 |
c.erase(b,e) |
删除迭代器b和e所标记的范围内所有的元素 返回一个迭代器,它指向被删除元素段后面的元素。如果e本身就是指向超出末端的下一位置的迭代器,则返回的迭代器也指向容器的超出末端的下一位置 |
c.clear() |
删除容器c内的所有元素。返回void |
c.pop_back() |
删除容器c的最后一个元素。返回void。如果c为空容器, 则该函数未定义 |
c.pop_front() |
删除容器c的第一个元素。返回void。如果c为空容器,则该函数未定义 只适用于list或deque容器 |
1、删除第一个/最后一个元素
pop_front操作通常与front操作配套使用,实现以栈的方式处理容器:
【注意:】
pop_front和 pop_back函数的返回值并不是删除的元素值,而是void。要获取删除的元素值,则必须在删除元素之前调用front或 back函数。
2、删除容器内的一个/一段元素
erase的两种形式都返回一个迭代器,它指向被删除元素或元素段后面的元素。也就是说,如果元素j恰好紧跟在元素i后面,则将元素i从容器中删除后,删除操作返回指向j的迭代器。
如同其他操作一样,erase操作也不会检查它的参数。程序员必须确保用作参数的迭代器或迭代器范围是有效的。因此,在删除元素之前,必须确保迭代器不是end迭代器,如果恰巧是end迭代器,则erase的操作未定义!
3、删除容器内的所有元素
同时,erase函数的迭代器版本也提供了删除部分元素的功能:
如果删除时,两个迭代器指向的元素是同一个元素,则不会删除任何元素;如果两个迭代器指向的元素有一个或两个不存在,则会发生运行时错误:
【小心地雷o(∩∩)o...,P282】
erase、pop_front和 pop_back函数使指向被删除元素的所有迭代器失效。对于vector容器,指向删除点后面的元素的迭代器通常也会失效。而对于deque容器,如果删除时不包含第一个元素或最后一个元素,那么该deque容器相关的所有迭代器都会失效。
八、赋值与swap
顺序容器的赋值与swap操作 |
|
---|---|
c1= c2 |
删除容器c1的所有元素,然后将c2的元素复制给c1。 c1和c2的类型(包括容器类型和元素类型)必须相同 |
c.assign(b,e) |
重新设置c的元素:将迭代器b和e标记的范围内所有的元素复制到c中。b和e必须不是指向c中元素的迭代器 |
c.assign(n,t) |
将容器c重新设置为存储n个值为t的元素 |
c1.swap(c2) |
交换内容:调用完该函数后,c1中存放的是c2原来的元素,c2中存放的则是c1原来的元素。 c1和c2的类型必须相同。 该函数的执行速度通常要比将c2复制到c1的操作快 |
与赋值相关的操作符都作用于整个容器。除了swap外,其他操作都可以通过erase和insert来替代。赋值操作符首先删除其左操作数容器的所有元素,然后将右操作数容器的所有元素插入到左边容器中:
尽管赋值前两个容器的长度可能不相等,但是赋值后两个容器的长度都等于右边容器的长度!
【小心地雷:】
赋值和assign操作使左操作容器的所有迭代器失效,swap操作则不会使迭代器失效。完成swap操作后,尽管被交换的元素已经存放在另一容器中,但迭代器仍然指向相同的元素。
1、使用assign
1)带有一对迭代器参数的assign操作允许我们将一个容器的元素赋给另一个不同类型的容器。但是两种容器类型与元素类型必须相互兼容!
2)assign运算的第二个版本需要一个整型数值和一个元素值做参数,它将容器重置为存储指定数量的元素,并且每个元素的值都为指定值:
2、使用swap操作以节省删除元素的成本
swap操作实现交换两个容器内所有元素的功能。要交换的容器的类型必须匹配:操作数必须是相同类型的容器,而且所存储的元素类型也必须相同。调用了swap函数后,右操作数原来存储的元素被存放在左操作数中,反之亦然。
关于swap的一个重要问题在于:该操作不会删除或插入任何元素,而且保证在常量时间内实现交换。由于容器内没有移动任何元素,因此迭代器不会失效。
没有移动元素这个事实意味着迭代器不会失效。它们指向同一元素,就像没作swap运算之前一样。虽然,在swap运算后,这些元素已经被存储在不同的容器之中了。例如,在做 swap运算之前,有一个迭代器iter指向 svec1[3]字符串;实现swap运算后,该迭代器则指向svec2[3]字符串(这是同一个字符串,只是存储在不同的容器之中而已)。
如果,vector 必须重新分配存储空间,用来存放原来的元素以及新添加的元素:则存放在旧存储空间中的元素被复制到新存储空间里,接着插入新元素,最后撤销旧的存储空间。
看上去无法忍受,但是对于大部分应用,使用vector容器是最好的,原因在于,标准库的实现者使用这样的内存分配策略:以最小的代价连续存储元素。由此带来的访问元素的便利弥补了其存储的代价!
为了使vector容器实现快速的内存分配,其实际分配的容量要比当前所需的空间多一些。vector容器预留了这些额外的存储区,用于存放新添加的元素。于是,不必为每个新元素重新分配容器。所分配的额外内存容量的确切数目因库的实现不同而不同。比起每添加一个新元素就必须重新分配一次容器,这个分配策略带来显著的效率。事实上,其性能非常好,因此在实际应用中,比起 list和 deque容器,vector的增长效率通常会更高。
capacity和reserve成员
vector类提供了两个成员函数:capacity和 reserve使程序员可与vector容器内存分配的实现部分交互工作。capacity操作获取在容器需要分配更多的存储空间之前能够存储的元素总数,而reserve操作则告诉vector容器应该预留多少个元素的存储空间。
ivec当前的状态如图:
现在可以如下预留额外的存储空间:
下面的程序将预留的容量用完:
此时,如果给容器添加新元素,则vector必须为自己重新分配空间:
每当vector容器不得不分配新的存储空间时,以加倍当前容量的分配策略实现重新分配。
vector的每种实现都可自由地选择自己的内存分配策略。然而,它们都必须提供reserve和 capacity函数,而且必须是到必要时才分配新的内存空间。分配多少内存取决于其实现方式。不同的库采用不同的策略实现。
此外,每种实现都要求遵循以下原则:确保push_back操作高效地在vector中添加元素。从技术上来说,在原来为空的vector容器上 n次调用push_back函数,从而创建拥有n个元素的vector容器,其执行时间永远不能超过n的常量倍。
分配连续存储元素的内存空间会影响内存分配策略和容器对象的开销。通过巧妙的实现技巧,标准库的实现者已经最小化了内存分配的开销。元素是否连续存储还会显著地影响:
•在容器的中间位置添加或删除元素的代价。
•执行容器元素的随机访问的代价。
程序使用这些操作的程度将决定应该选择哪种类型的容器。vector和 deque容器提供了对元素的快速随机访问,但付出的代价是,在容器的任意位置插入或删除元素,比在容器尾部插入和删除的开销更大。list类型在任何位置都能快速插入和删除,但付出的代价是元素的随机访问开销较大。
1、插入操作如何影响容器的选择
list容器表示不连续的内存区域,允许向前和向后逐个遍历元素。在任何位置都可高效地insert或 erase一个元素。插入或删除list容器中的一个元素不需要移动任何其他元素。另一方面,list容器不支持随机访问,访问某个元素要求遍历涉及的其他元素。
对于vector容器,除了容器尾部外,其他任何位置上的插入(或删除)操作都要求移动被插入(或删除)元素右边所有的元素。
deque容器拥有更加复杂的数据结构。deque队列的两端插入和删除元素都非常快。在容器中间插入或删除付出的代价将更高。deque容器同时提供了list和 vector的一些性质:
•与vector容器一样,deque 容器的中间insert或 erase元素效率比较低
•不同于vector容器,deque容器提供高效地在其首部实现insert和 erase的操作,就像在vector容器尾部的一样。
•与vector容器一样而不同于list容器的是,deque 容器支持对所有元素的随机访问。
•在deque容器首部或尾部插入元素不会使任何迭代器失效,而首部或尾部删除元素则只会使指向被删除元素的迭代器失效。在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器都失效。
2、元素的访问如何影响容器的选择
vector和 deque容器都支持对其元素实现高效的随机访问。由于vector容器的每次访问都是距离其起点的固定偏移,因此其随机访问非常有效率。在list容器中,跳跃访问会变得慢很多。在list容器的元素之间移动的唯一方法是顺序跟随指针。比如从5号元素移动到15号元素必须遍历它们之间所有的元素。
通常来说,除非找到选择使用其他容器的更好理由,否则vector容器都是最佳选择。
3、选择容器的提示
下面列举了一些选择容器类型的法则:
1.如果程序要求随机访问元素,则应使用vector或 deque容器。
2.如果程序必须在容器的中间位置插入或删除元素,则应采用list容器。
3.如果程序不是在容器的中间位置,而是在容器首部或尾部插入或删除元素,则应采用deque容器。
4.如果只需在读取输入时在容器的中间位置插入元素,然后需要随机访问元素,则可考虑在输入时将元素读入到一个list容器,接着对此容器重新排序,使其适合顺序访问,然后将排序后的list容器复制到一个vector容器。
如果程序既需要随机访问又必须在容器的中间位置插入或删除元素,那应该怎么办呢?
此时,选择何种容器取决于下面两种操作付出的相对代价:随机访问list容器元素的代价,以及在vector或 deque容器中插入/删除元素时复制元素的代价。通常来说,应用中占优势的操作(程序中更多使用的是访问操作还是插入/删除操作)将决定应该什么类型的容器。
决定使用哪种容器可能要求剖析各种容器类型完成应用所要求的各类操作的性能。
【最佳实践】
如果无法确定某种应用应该采用哪种容器,则编写代码时尝试只使用vector和lists容器都提供的操作:使用迭代器,而不是下标,并且避免随机访问元素。这样编写,在必要时,可以很方便地将程序从使用vector容器修改为使用list的容器。
本文借鉴:http://blog.csdn.net/column/details/zjf666.html?&page=4