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

C++primer【笔记】 顺序容器

2015-08-11 17:13 218 查看
这一章主要学习了C++的三种顺序容器:vector、list 、deque(双端队列),三种适配器:stack(堆栈)、queue(队列)、priority_queue(有优先级管理的队列)。

1. 顺序容器

1.1初始化

表1.1 容器构造函数

构造函数形式备注
C< T > c;C为类型名,c为空容器,T是元素类型
C c(c2);创建 c 并用 c2 初始化, c 和 c2 必须具有相同的容器类型并且存储的元素也要相同
C c(b,e);创建 c ,其元素是迭代器 b 和 e 表示范围内的元素副本
C (n,t);创建 c 并初始化为 n 个值为 t 的元素,只适用于顺序容器
C c(n)创建 c 并初始化为 n 个值初始化元素,只适用于顺序容器
虽然不能直接将一个容器内的值赋给另一个不同种类的容器,但是可以通过迭代器标记要复制的元素范围进行赋值,前提是真两个容器以及存储的变量类型要相互兼容才行。

容器元素类型必须满足两个约束条件:支持赋值运算;元素类型对象必须可复制。引用不支持一般意义上的赋值运算,所以没有元素是引用类型的元素。

1.2 迭代器

所有容器迭代器都支持以解引用运算从容器中读入一个元素。类似地,容器都提供自增和自减操作符来支持从一个元素到下一个元素的访问。

其中vector 和 deque 容器的迭代器还提供额外的运算:

iter + n;


iter1 += iter2;


iter1 - iter2;


还支持关系操作符。

迭代器范围是一个左闭合的区间 [first,last)[ first, last ), 迭代器 first 和 last 必须满足以下两个条件:指向同一个容器的元素或者超出末端的下一个位置;如果两个迭代器不相等,则 first 反复自增运算后必须达到 last 。

注意使迭代器失效的操作!如 erase 和 insert 。

1.3 容器中添加元素

表1.2 在顺序容器中添加元素的操作:

c.push_back(t)在容器 c 尾部添加值为t的元素,返回 void
c.push_front(t)在容器 c 首部添加值为t 的元素, 只适用于 list 和 deque 容器类型,返回 void
c.insert(p,t)在迭代器 p 指向的元素前插入值为 t 的新元素,返回指向添加的新元素的迭代器
c.insert(p,n,t)在迭代器 p 指向的元素前插入 n 个值为 t 的新元素, 返回void
c.insert(p,b,e)在迭代器 p 指向的元素前插入由迭代器 b 和 e 标记的范围内的元素。返回void
注意 :push_front不适用于vector容器,任何 insert 或 push 操作都可能导致迭代器失效。当编写循环将元素插入到 vector 或 deque 容器中时,程序必须确保迭代器在每次循环后都得到更新。

1.4 访问顺序容器内的元素

表1.3 访问顺序容器元素操作

c.back()返回容器 c 最后一个元素的引用,如果 c 为空则该操作无定义
c.front()返回容器 c 第一个元素的引用,如果 c 为空则该操作无定义
c
返回下标为n的元素引用,如果 n <0 或 n >= c.size(),则该操作未定义只适用于 vector 和 deque 容器
c.at(n)返回下标为 n 的元素的引用。如果下标越界,则该操作未定义只适用于 vector 和 deque 容器

1.5 删除顺序容器内元素

表1.4 删除顺序容器内元素的操作

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.6 赋值操作

表1.5 顺序容器的赋值操作

c1 = c2删除容器 c1 的所有元素, 然后将 c2 的元素复制给 c1。 c1 和c2 的类型(包括容器类型和元素类型)必须相同
c1.swap(c2)交换内容:调用完该函数后,c1 中存放的是 c2 原来的元素,c2 中存放的则是 c1 原来的元素。c1 和 c2 的类型必须相同。该函数的执行速度通常要比将 c2 复制到 c1 的操作快
c.assign(b,e)重新设置 c 的元素:将迭代器 b 和 e 标记的范围内所有的元素复制到 c 中。b 和 e 必须不是指向 c 中元素的迭代器
c.assign(n,t)将容器 c 重新设置为存储 n 个值为 t 的元素
由于 assign 操作首先删除容器中原来存储的所有元素,因此,传递给 assign 函数的迭代器不能指向调用该函数的容器内的元素。

使用 swap 操作以节省删除元素的成本,swap 操作实现交换两个容器内所有元素的功能。要交换的容器的类型必须匹配:操作数必须是相同类型的容器,而且所存储的元素类型也必须相同。调用了 swap 函数后,右操作数原来存储的元素被存放在左操作数中,反之亦然。关于 swap 的一个重要问题在于:该操作不会删除或插入任何元素,而且保证在常量时间内实现交换。由于容器内没有移动

任何元素,因此迭代器不会失效。关于swap操作,这里介绍的比较详细: 简单的程序诠释C++ STL算法系列之十五:swap

1.7 vector容器的自增长

为了支持快速的随机访问,vector 容器的元素以连续的方式存放——每一个元素都紧挨着前一个元素存储。已知元素是连续存储的,当我们在容器内添加一个元素时,想想会发生什么事情:如果容器中已经没有空间容纳新的元素,此时,由于元素必须连续存储以便索引访问, 所以不能在内存中随便找个地方存储这个新元素。 于是, vector 必须重新分配存储空间,用来存放原来的元素以及新添加的元素:存放在旧存储空间中的元素被复制到新存储空间里,接着插入新元素,最后撤销旧的存储空间。如果 vector 容器在在每次添加新元素时,都要这么分配和撤销内存空间,其性能将会非常慢,简直无法接受。

对于不连续存储元素的容器, 不存在这样的内存分配问题。例如, 在 list 容器中添加一个元素,标准库只需创建一个新元素,然后将该新元素连接在已存在的链表中,不需要重新分配存储空间,也不必复制任何已存在的元素。由此可以推论:一般而言,使用 list 容器优于 vector 容器。但是,通常出现的反而是以下情况:对于大部分应用,使用 vector 容器是最好的。原因在于,标准库的实现者使用这样内存分配策略:以最小的代价连续存储元素。由此而带来的访问元素的便利弥补了其存储代价。

vector 类提供了两个成员函数:capacity 和reserve 使程序员可与 vector 容器内存分配的实现部分交互工作。capacity操作获取在容器需要分配更多的存储空间之前能够存储的元素总数, 而 reserve操作则告诉 vector 容器应该预留多少个元素的存储空间。

为了说明 size 和 capacity 的交互作用,考虑下面的程序:

#include<iostream>
#include<vector>
#include<string>
using namespace std;
int main(){
//initial size and capacity
vector<string> v(10,"WB");
cout << "Size is: " << v.size() << endl;
cout << "Capacity is: " << v.capacity() << endl;
//new size and capacity after inserting 1 element
v.push_back("WB");
cout << "Size is: " << v.size() << endl;
cout << "Capacity is: " << v.capacity() << endl;
// size and capacity when capacity runs out
while(v.size() != v.capacity())
v.push_back("WB");
cout << "Size is: " << v.size() << endl;
cout << "Capacity is: " << v.capacity() << endl;
//new size and capacity when 1 element added
v.push_back("WB");
cout << "Size is: " << v.size() << endl;
cout << "Capacity is: " << v.capacity() << endl;
return 0;
}


每当 vector 容器不得不分配新的存储空间时,以加倍当前容量的分配策略实现重新分配。vector 的每种实现都可自由地选择自己的内存分配策略。然而,它们都必须提供 vector 和 capacity 函数,而且必须是到必要时才分配新的内存空间。分配多少内存取决于其实现方式。不同的库采用不同的策略实现。

1.8 容器的选用

程序使用这些操作的程序将决定应该选择哪种类型的容器。vector 和 deque容器提供了对元素的快速随机访问,但付出的代价是,在容器的任意位置插入或删除元素,比在容器尾部插入和删除的开销更大。list 类型在任何位置都能快速插入和删除,但付出的代价是元素的随机访问开销较大。

1. 如果程序要求随机访问元素,则应使用 vector 或 deque 容器。

2. 如果程序必须在容器的中间位置插入或删除元素,则应采用 list 容器。

3. 如果程序不是在容器的中间位置,而是在容器首部或尾部插入或删除元素,则应采用 deque 容器。

4. 如果只需在读取输入时在容器的中间位置插入元素, 然后需要随机访问元素,则可考虑在输入时将元素读入到一个 list 容器,接着对此容器重新排序, 使其适合顺序访问, 然后将排序后的 list 容器复制到一个 vector容器。

2. 容器适配器

除了顺序容器,标准库还提供了三种顺序容器适配器:queue、priority_queue 和 stack。 适配器(adaptor) 是标准库中通用的概念,包括容器适配器、迭代器适配器和函数适配器。本质上,适配器是使一事物的行为类似于另一事物的行为的一种机制。容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现。例如,stack(栈)适配器可使任何一种顺序容器以栈的方式工作。

2.1 栈适配器

表2.1 栈容器适配器支持的操作

s.empty()如果栈为空,则返回 true,否则返回 false
s.size()返回栈中元素的个数
s.pop()删除栈顶元素的值,但不返回其值
s.top()返回栈顶元素的值,但不删除该元素
s.push(item)在栈顶压入新元素
默认情况下,栈适配器建立在 deque 容器上,因此采用 deque 提供的操作来实现栈功能。

2.2 队列和优先级队列

标准库队列使用了先进先出(FIFO)的存储和检索策略。进入队列的对象被放置在尾部,下一个被取出的元素则取自队列的首部。标准库提供了两种风格的队列: FIFO 队列 (FIFO queue, 简称 queue), 以及优先级队列 (priority queue)。

priority_queue 允许用户为队列中存储的元素设置优先级。这种队列不是直接将新元素放置在队列尾部,而是放在比它优先级低的元素前面。标准库默认使用元素类型的 < 操作符来确定它们之间的优先级关系。要使用这两种队列,必须包含 queue 头文件。

表2.2 队列和优先级队列支持的操作

q.empty()如果队列为空,则返回 true,否则返回 false
q.size()返回队列中元素的个数
q.pop删除队首元素,但不返回其值
q.front返回队首元素的值,但不删除该元素该操作只适用于队列
q.back()返回队尾元素的值,但不删除该元素该操作只适用于队列
q.top()返回具有最高优先级的元素值,但不删除该元素该操作只适用于优先级队列
q.push(item)对于 queue,在队尾压入一个新元素,对于 priority_quue,在基于优先级的适当位置插入新元素

3. 小结

最经常使用的容器类型是 vector,它支持对元素的快速随机访问。可高效地在 vector 容器尾部添加和删除元素, 而在其他任何位置上的插入或删除运算则要付出比较昂贵的代价。deque 类与 vector 相似,但它还支持在 deque 首部的快速插入和删除运算。list 类只支持元素的顺序访问,但在 list 内部任何位置插入和删除元素都非常快速。

容器定义的操作非常少,只定义了构造函数、添加或删除元素的操作、设置容器长度的操作以及返回指向特殊元素的迭代器的操作。其他一些有用的操作,如排序、查找,则不是由容器类型定义,而是由标准算法定义。

在容器中添加或删除元素可能会使已存在的迭代器失效。当混合使用迭代器操作和容器操作时,必须时刻留意给定的容器操作是否会使迭代器失效。许多使一个迭代器失效的操作,例如 insert 或erase,将返回一个新的迭代器,让程序员保留容器中的一个位置。使用改变容器长度的容器操作的循环必须非常小心其迭代器的使用。

以上内容为学习笔记,摘选自《c++ primer》第四版
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: