C++ Primer 第四版读书笔记(八)之顺序容器
容器容纳特定类型对象的集合。
标准库vector类型,是一个顺序容器。它将单一类型元素聚集起来称为容器,然后根据位置来存储和访问这些元素,这就是顺序容器。顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。
标准库定义了三种顺序容器类型:vector、list和deque。它们的差别在于访问元素的方式,以及添加或删除元素相关操作的运行代价。标准库还提供了三种容器适配器。实际上,适配器是根据原始的容器类型所提供的操作,通过定义新的操作接口,来适应基础的容器类型。
顺序容器 | |
vector | 支持快速随机访问 |
list | 支持快速插入/删除 |
deque | 双端队列 |
顺序容器适配器 | |
stack | 后进先出(LIFO)栈 |
deque | 先进先出(FIFO)队列 |
priority_quede | 有优先级管理的队列 |
1、一些操作适用于所有容器类型。
2、另外一些操作则只适用于顺序或关联容器类型。
3、还有一些操作只适用于顺序或关联容器类型的一个子集。
一、顺序容器的定义
所有的容器是类模版。要定义某种特殊的容器,必须在容器明后加一对尖括号,尖括号里面提供容器中存放的元素的类型。
所有的容器类型都定义了默认构造函数,用于创建指定类型的空容器对象。默认构造函数不带参数。
注:为了使程序更清晰、简短,容器类型最常用的构造函数是默认构造函数。在大多数的程序中,使用默认构造函数能达到最佳运行时性能,并且使容器更容易使用。
1.1 容器元素的初始化
除了默认构造函数,容器类型还提供其他的构造函数,使程序员可以指定元素初值。
C<T> c; | 创建一个名为c的空容器。C是容器类型名,如vector,T是元素类型,如int或string。适用于所有容器。 |
C c(c2); | 创建容器c2的副本c;c和c2必须具有相同的容器类型,并存放相同类型的元素。适用于所有容器。 |
C c(b, e); | 创建c,其元素是迭代器b和e标示的范围内元素的副本。使用与所有容器。 |
C c(n, t); | 用n个值为t的元素创建容器c,其中值t必须是容器类型C的元素类型的值,或者是可转换为该类型的值,只适用于顺序容器 |
C c(n); | 创建有n个值初始化元素的容器c,只适用于顺序容器 |
1.1.1 将一个容器初始化为另一个容器的副本
当不使用默认构造函数,而是用其他构造函数初始化顺序容器时,必须指出该容器有多少个元素,并提供这些元素的初值。同时指定元素个数和初值的一个方法是将新创建的容器初始化为同一个类型的已存在容器的副本。
1.1.2 初始化为一段元素的副本
尽管不能直接将一种容器内的元素复制给另一种容器,但系统允许通过传递一对迭代器间接实现该功能。使用迭代器时,不要求容器类型相同。容器内的元素类型也可以不相同,只要它们相互兼容,能够将要复制的元素转换为所构建的新容器的元素类型,即可实现复制。
迭代器标记了要复制的元素范围,这些元素用于初始化新容器的元素。迭代器标记出要复制的第一个元素和最后一个元素。
1.1.3 分配和初始化指定数目的元素
创建顺序容器时,可显示指定容器大小和一个(可选的)元素初始化式。容器大小可以是常量或非常量表达式,元素初始化式则必须是可用于初始化其元素类型的对象的值。
创建容器时,除了指定元素个数,还可选择是否提供元素初始化式。
1.2 容器内元素的类型约束
C++语言中,大多数类型都可用作容器的元素类型。容器元素类型必须满足以下两个约束:
1、元素类型必须支持赋值运算;
2、元素类型的对象必须可以复制。
1.2.1 容器操作的特殊要求
支持赋值和复制功能是容器元素类型的最低要求。此外,一些容器操作对元素类型还有特殊要求。如果元素类型不支持这些特殊要求,则相关的容器操作就不能执行:我们可以定义该类型的容器,但不能使用某些特定的操作。
1.2.2 容器的容器
因为容器受容器元素类型的约束,所以可定义元素是容器类型的容器。
二、 迭代器和迭代器范围
在整个标准库中,经常使用形参为一对迭代器的构造函数。
每种容器类型都提供若干共同工作的迭代器类型。与容器类型一样,所有迭代器具有相同的接口:如果某种迭代器支持某种操作,那么支持这种操作的其他迭代器也会以相同的方式支持这种操作。
*iter | 返回迭代器iter所指向的元素的引用 |
iter->mem | 对iter进行解引用,获取指定元素中名为mem的成员。等效于(*iter).mem |
++iter | 给iter加1,使其指向容器里的下一个元素 |
iter++ | 给iter加1,使其指向容器里的下一个元素 |
--iter | 给iter减1,使其指向容器里的前一个元素 |
iter-- | 给iter减1,使其指向容器里的前一个元素 |
iter1 == iter2 | 比较两个迭代器是否相等(或不等)。当两个迭代器指向同一个容器中的同一个元素,或者当它们都 指向同一个容器的超出末端的下一个位置时,两个迭代器相等 |
iter1 != iter2 | 比较两个迭代器是否相等(或不等)。当两个迭代器指向同一个容器中的同一个元素,或者当它们都 指向同一个容器的超出末端的下一个位置时,两个迭代器相等 |
vector和deque容器的迭代器提供额外的运算
C++定义的容器类型中,只有vector和deque容器提供下面两种重要的运算集合:迭代器算术运算,以及使用== 和 != 之外的关系操作符比较两个迭代器(==和!=这两种关系运算适用于所有容器)。
iter+n | 在迭代器上加(减)整数值n,将产生指向容器中前面(后面)第n个元素的迭代器。 新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一位置。 |
iter-n | 在迭代器上加(减)整数值n,将产生指向容器中前面(后面)第n个元素的迭代器。 新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一位置。 |
iter1 += iter2 | 这是迭代器加减法的复合赋值运算:将iter1加上或减去iter2的运算结果赋值给iter1 |
iter1 -= iter2 | 这是迭代器加减法的复合赋值运算:将iter1加上或减去iter2的运算结果赋值给iter1 |
iter1 - iter2 | 两个迭代器的减法,其运算结果加上右边的迭代器即得左边的迭代器。这两个迭代器必 须指向同一个容器中的元素或超出容器末端的下一位置。只适用于vector和deque容器 |
>, >=, <, <= | 迭代器的关系操作符。当一个迭代器指向的元素在容器中位于另一个迭代器指向的元素之前, 则前一个迭代器小于后一个迭代器。关系操作符的两个迭代器必须指向同一个容器中的元素 或超出容器末端的下一位置。只适用于vector和deque容器 |
关系操作符只适用于vector和deque容器,只是因为只有这两种容器为其元素提供快速、随机的访问。他们确保可根据元素位置直接有效地访问指定的容器元素。这两种都支持通过元素位置实现的随机访问,因此他们的迭代器可以有效地实现算术和关系运算。
2.1 迭代器范围
C++语言使用一对迭代器标记迭代器范围。这两个迭代器分别指向同一个容器中的两个元素或超出末端的下一位置,通常将它们命名为first和last,或者beg和end,用于标记容器中的一段元素范围。
注:对形成迭代器范围的迭代器的要求:
迭代器first和last如果满足以下条件,则可形成一个迭代器范围:
1、它们指向同一个容器中的元素或超出末端的下一位置。
2、如果这两个迭代器不相等,则对first反复做自增运算必须能够达到last。换句话说,在容器中,last绝对不能位于first之前。
使用左闭合区间的编程意义
因为左闭合区间有两个方便使用的性质,所以标准库使用此类区间。假设first和last标记了一个有效的迭代器范围,于是:
(1)、当first与last相等时,迭代器范围为空;
(2)、当first与last不相等时,迭代器范围内至少有一个元素,而且first指向该区间中的第一个元素。此外,通过若干次自增运算可以使first的值不断增大,知道first==last为止。
2.2 使迭代器失效的容器操作
使用迭代器编写程序时,必须留意哪些操作会使迭代器失效。使用无效迭代器将会导致严重的运行时错误。
使用迭代器时,通常可以编写程序使得要求迭代器有效的代码范围相对较短。然后,在该范围内,严格检查每一条语句,判断是否有元素添加或删除,从而相应地调整迭代器的值。
三、顺序容器的操作
每种顺序容器都提供了一组有用的类型定义以及一下操作:
1、在容器中添加元素
2、在容器中删除元素。
3、设置容器的大小。
4、(如果有的话)获取容器内的第一个和最后一个元素。
3.1 容器定义的类型别名
size_type | 无符号整型,足以存储此容器类型的最大可能容器长度 |
iterator | 此容器类型的迭代器类型 |
const_iterator | 元素的只读迭代器类型 |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 元素的只读(不能写)逆序迭代器 |
difference_type | 足够存储两个迭代器差值的有符号整型,可为负数 |
value_type | 元素类型 |
reference | 元素的左值类型,是value_type&的同义词 |
const_reference | 元素的常量左值类型,等效于const value_type& |
3.2 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类型。
3.3 在顺序容器中添加元素
c.push_back(t) | 在容器c的尾部添加值为t的元素。返回void类型 |
c.push_front(t) | 在容器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类型。 |
在容器中添加元素时,系统是将元素值复制到容器里。类似地,使用一段元素初始化新容器时,新容器存放的是原始元素的副本。被复制的原始值与新容器中的元素各不相关,此后,容器内元素值发生变化时,被复制的原值不会受影响,反之亦然。
3.3.1 在容器中的指定位置添加元素
3.3.2 插入一段元素
3.3.3 添加元素可能会使迭代器失效
在vector容器中添加元素可能会导致整个容器的重新加载,这样的话,该容器涉及的所有迭代器都会失效。即使需要重新加载整个容器,指向新插入元素后面的那个元素的迭代器也会失效。
注:任何insert或push操作都可能导致迭代器失效。当编写循环将元素插入到vector或deque容器中时,程序必须确保迭代器在每次循环后都得到更新。
3.3.4 避免存储end操作返回的迭代器
在vector或deque容器中添加元素时,可能会导致某些或全部迭代器失效。假设所有迭代器失效是最安全的做法。这个建议特别适用于由end操作返回的迭代器。在容器的任何位置插入任何元素都会使该迭代器失效。
3.4 关系操作符
所有的容器类型都支持用关系操作符来实现两个容器的比较。比较的容器必须具有相同的容器类型,而且其元素类型也必须相同。
容器的比较是基于容器内元素的比较。容器的比较使用了元素类型定义的同一个关系操作符:两个容器做 != 比较使用了其元素类型定义的 != 操作符。如果容器的元素类型不支持某种操作符,则该类容器就不能做这种比较运算。
下面的操作类似于string类型的关系运算:
1、如果两个容器具有相同的长度而且所有元素都相等,那么这两个容器就相等;否则,它们就不相等。
2、如果两个容器的长度不相同,但较短的容器中所有的元素都等于较长容器中对应的元素,则称较短的容器小于另一个容器。
3、如果两个容器都不是对方的初始子序列,则它们的比较结果取决于所比较的第一个不相等的元素。
使用元素提供的关系操作符实现容器的关系运算:
C++语言只允许两个容器做其元素类型定义的关系运算。
所有容器都通过比较其元素对来实现关系运算。
3.5 容器大小的操作
所有容器类型都提供四种与容器大小相关的操作。
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。 |
3.6 访问元素
如果容器非空,那么容器类型的front和back成员将返回容器内第一个或最后一个元素的引用。