《C++Primer》第九章-顺序容器-学习笔记(1)

《C++Primer》第九章-顺序容器-学习笔记(1)

日志:
1,2020-03-11 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
《C++Primer》第十二章-类-学习笔记(1)

摘要

C++ 提供了使用抽象进行高效率编程的方式。标准库就是一个很好的例子:标准库定义了许多容器类以及一系列泛型算法,使程序员可以更简洁、抽象和有效地编写程序。这样可以让标准库操心那些繁琐的细节,特别是内存管理,我们的程序只需要关注要解决的实际问题就行了。

容器类共享公共的接口,这使标准库更容易学习,只要学会其中一种类型就能运用另一种类型。每种容器类型提供一组不同的时间和功能折衷方案。通常不需要修改代码,只需改变类型声明,用一种容器类型替代另一种容器类型,就可以优化程序的性能。

容器(container)容纳特定类型对象的集合。顺序容器(sequential container):它将单一类型元素聚集起来成为容器,然后根据位置来存储和访问这些元素。顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。

标准库定义了三种顺序容器类型:vectorlistdeque(是双端队列“double-ended queue”的简写,发音为“deck”)。它们的差别在于访问元素的方式,以及添加或删除元素相关操作的运行代价。标准库还提供了三种容器适配器(adaptors)。实际上,适配器是根据原始的容器类型所提供的操作,通过定义新的操作接口,来适应基础的容器类型。顺序容器适配器包括 stack、queue 和 priority_queue 类型

容器只定义了少量操作。大多数额外操作则由算法库提供。标准库为由容器类型定义的操作强加了公共的接口。这些容器类型的差别在于它们提供哪些操作,但是如果两个容器提供了相同的操作,则它们的接口(函数名字和参数个数)应该相同。容器类型的操作集合形成了以下层次结构:

  • 一些操作适用于所有容器类型。
  • 另外一些操作则只适用于顺序或关联容器类型。
  • 还有一些操作只适用于顺序或关联容器类型的一个子集。

表1. 顺序容器类型:

顺序容器 用途 顺序容器适配器 用途
vector 支持快速随机访问 stack 后进先出(LIFO)堆栈
list 支持快速插入/删除 queue 先进先出(FIFO)队列
deque 双端队列 priority_queue 有优先级管理的队列

顺序容器的定义

为了定义一个容器类型的对象,必须先包含相关的头文件,即下列头文件之一:

#include <vector>
#include <list>
#include <deque>

所有的容器都是类模板。要定义某种特殊的容器,必须在容器名后加一对尖括号,尖括号里面提供容器中存放的元素的类型:

vector<string> svec; // empty vector that can hold strings
list<int> ilist; // empty list that can hold ints
deque<Sales_item> items; // empty deque that holds Sales_items

所有容器类型都定义了默认构造函数,用于创建指定类型的空容器对象容器默认构造函数不带参数

为了使程序更清晰、简短,容器类型最常用的构造函数是默认构造函数。在大多数的程序中,使用默认构造函数能达到最佳运行时性能,并且使容器更容易使用。

容器元素的初始化

除了默认构造函数,容器类型还提供其他的构造函数,使程序员可以指定元素初值:

容器构造函数 作用
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 个值初始化(value-initialized)元素的容器 c。只适用于顺序容器
表2. 容器构造函数

将一个容器初始化为另一个容器的副本

当不使用默认构造函数,而是用其他构造函数初始化顺序容器时,必须指出该容器有多少个元素,并提供这些元素的初值。同时指定元素个数和初值的一个方法是将新创建的容器初始化为一个同类型的已存在容器的副本:

vector<int> ivec;
vector<int> ivec2(ivec); // ok: ivec is vector<int>
list<int> ilist(ivec); // error: ivec is not list<int>
vector<double> dvec(ivec); // error: ivec holds int not double

将一个容器复制给另一个容器时,类型必须匹配:容器类型和元素类型都必须相同。

初始化为一段元素的副本

尽管不能直接将一种容器内的元素复制给另一种容器,但系统允许通过传递一对迭代器间接实现该实现该功能。使用迭代器时,不要求容器类型相同。容器内的元素类型也可以不相同,只要它们相互兼容,能够将要复制的元素转换为所构建的新容器的元素类型,即可实现复制。
迭代器标记了要复制的元素范围,这些元素用于初始化新容器的元素。迭代器标记出要复制的第一个元素和最后一个元素。采用这种初始化形式可复制不能直接复制的容器。更重要的是,可以实现复制其他容器的一个子序列:

// initialize slist with copy of each element of svec
list<string> slist(svec.begin(), svec.end());  //svc是string的vector
// find midpoint in the vector
vector<string>::iterator mid = svec.begin() + svec.size()/2;
// initialize front with first half of svec: The elements up to but not including *mid
deque<string> front(svec.begin(), mid); //复制不包括*mid
// initialize back with second half of svec: The elements *mid through end of svec
deque<string> back(mid, svec.end());  //复制包括mid

回顾一下指针,我们知道指针就是迭代器,因此允许通过使用内置数组中的一对指针初始化容器也就不奇怪了:

char *words[] = {"stately", "plump", "buck", "mulligan"};
// calculate how many elements in words
size_t words_size = sizeof(words)/sizeof(char *);
// use entire array to initialize words2
list<string> words2(words, words + words_size); //第二个指针提供停止复制的条件

这里,使用 sizeof计算数组的长度。将数组长度加到指向第一个元素的指针上就可以得到指向超出数组末端的下一位置的指针。通过指向第一个元素的指针 words 和指向数组中最后一个元素的下一位置的指针,实现了words2 的初始化。其中第二个指针提供停止复制的条件,其所指向的位置上存放的元素并没有复制。

分配和初始化指定数目的元素

创建顺序容器时,可显式指定容器大小和一个(可选的)元素初始化式容器大小可以是常量或非常量表达式,元素初始化则必须是可用于初始化其元素类型的对象的值:

const list<int>::size_type list_size = 64;
list<string> slist(list_size, "eh?"); // 64 strings, each is eh?

这段代码表示 slist 含有 64 个元素,每个元素都被初始化为“eh?”字符串。创建容器时,除了指定元素个数,还可选择是否提供元素初始化式。我们也可以只指定容器大小:

list<int> ilist(list_size); // 64 elements, each initialized to 0
//不提供元素初始化式时,标准库将为该容器实现值初始化
// svec has as many elements as the return value from get_word_count
extern unsigned get_word_count(const string &file_name);
vector<string> svec(get_word_count("Chimera"));

不提供元素初始化式时,标准库将为该容器实现值初始化(第3章)。采用这种类型的初始化,元素类型必须是内置或复合类型,或者是提供了默认构造函数的类类型如果元素类型没有默认构造函数,则必须显式指定其元素初始化式。

接受容器大小做形参的构造函数只适用于顺序容器,而关联容器不支持这种初始化。

容器内元素的类型约束

C++ 语言中,大多数类型都可用作容器的元素类型。容器元素类型必须满足以下两个约束:

  • 元素类型必须支持赋值运算。
  • 元素类型的对象必须可以复制。

此外,关联容器的键类型还需满足其他的约束,我们将在第十章介绍相关内容。
大多数类型满足上述最低限度的元素类型要求。除了引用类型外,所有内置或复合类型都可用做元素类型。引用不支持一般意义的赋值运算,因此没有元素是引用类型的容器。
除输入输出(IO)标准库类型(以及第 17章介绍的 auto_ptr 类型)之外,所有其他标准库类型都是有效的容器元素类型。特别地,容器本身也满足上述要求,因此,可以定义元素本身就是容器类型的容器。Sales_item 类型也满足上述要求。IO 库类型不支持复制或赋值。因此,不能创建存放 IO 类型对象的容器。

容器操作的特殊要求

支持复制和赋值功能是容器元素类型的最低要求此外,一些容器操作对元素类型还有特殊要求。如果元素类型不支持这些特殊要求,则相关的容器操作就不能执行(我们可以定义该类型的容器,但不能使用某些特定的操作。)
其中一种需外加类型要求的容器操作是指定容器大小并提供单个初始化式的构造函数。如果容器存储类类型的对象,那么只有当其元素类型提供默认构造函数时,容器才能使用这种构造函数。尽管有一些类没有提供默认构造函数,但大多数类类型都会有。例如,假设类 Foo 没有默认构造函数,但提供了需要一个 int 型形参的构造函数。现在,考虑下面的声明:

vector<Foo> empty; // ok: no need for element default constructor
vector<Foo> bad(10); // error: no default constructor for Foo
vector<Foo> ok(10, 1); // ok: each element initialized to 1

我们定义一个存放 Foo 类型对象的空容器,但是,只有在同时指定每个元素的初始化式时,才能使用给定容器大小的构造函数来创建同类型的容器对象。在描述容器操作时,我们应该留意(如果有的话)每个操作对元素类型的约束。

容器的容器

因为容器受容器元素类型的约束,所以可定义元素是容器类型的容器。例如,可以定义 vector 类型的容器 lines,其元素为 string 类型的 vector 对象:

// note spacing: use ">>" not ">>" when specifying a container element type
vector< vector<string> > lines; // vector of vectors

注意,在指定容器元素为容器类型时,必须如下使用空格:

vector< vector<string> > lines; // ok: space required between close
vector< vector<string>> lines; // error: >> treated as shift operator

必须用空格隔开两个相邻的 > 符号,以示这是两个分开的符号,否则,系统会认为 >> 是单个符号,为右移操作符,并导致编译时错误。

迭代器和迭代器范围

在整个标准库中,经常使用形参为一对迭代器的构造函数。在深入探讨容器操作之前,先来了解一下迭代器和迭代器范围。
第 3章首次介绍了 vector 类型的迭代器。每种容器类型都提供若干共同工作的迭代器类型。与容器类型一样,所有迭代器具有相同的接口:如果某种迭代器支持某种操作,那么支持这种操作的其他迭代器也会以相同的方式支持这种操作。例如,所有容器迭代器都支持以解引用运算从容器中读入一个元素。类似地,容器都提供自增和自减操作符来支持从一个元素到下一个元素的访问。表9.3 列出迭代器为所有标准库容器类型所提供的运算。

迭代器 作用
*iter 返回迭代器 iter 所指向的元素的引用
iter->mem 对 iter 进行解引用,获取指定元素中名为 mem 的成员。等效于(*iter).mem
++iter 与 iter++ 给 iter 加 1,使其指向容器里的下一个元素
- -iter 与 iter- - 给 iter 减 1,使其指向容器里的前一个元素
iter1 == iter2 与 iter1 != iter2 比较两个迭代器是否相等(或不等)。当两个迭代器指向同一个容器中的同一个元素,或者当它们都指向同一个容器的超出末端的下一位置时,两个迭代器相等
表3. 常用迭代器运算

vector 和 deque 容器的迭代器提供额外的运算

C++ 定义的容器类型中,只有 vector 和 deque 容器提供下面两种重要的运算集合迭代器算术运算(第 3.4.1 节),以及使用除了 == 和 != 之外的关系操作符来比较两个迭代器(== 和 != 这两种关系运算适用于所有容器)。表 4 总结了这些相关的操作符。

迭代器运算集合 作用
iter + n与iter - n 在迭代器上加(减)整数值 n,将产生指向容器中前面(后面)第 n个元素的迭代器。新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一位置
iter1 += iter2与iter1 -=iter2 这里迭代器加减法的复合赋值运算:将 iter1 加上或减去 iter2 的运算结果赋给 iter1
iter1 - iter2 两个迭代器的减法,其运算结果加上右边的迭代器即得左边的迭代器。这两个迭代器必须指向同一个容器中的元素或超出容器末端的下一位置(只适用于 vector 和 deque 容器)
iter + n与iter - n 在迭代器上加(减)整数值 n,将产生指向容器中前面(后面)第 n个元素的迭代器。新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一位置
>, >=,<, <= 迭代器的关系操作符。当一个迭代器指向的元素在容器中位于另一个迭代器指向的元素之前,则前一个迭代器小于后一个迭代器。关系操作符的两个迭代器必须指向同一个容器中的元素或超出容器末端的下一位置(只适用于 vector 和 deque 容器)
表 4. vector 和 deque 类型迭代器支持的操作

迭代器范围

迭代器范围这个概念是标准库的基础。 C++ 语言使用一对迭代器标记迭代器范围(iterator range),这两个迭代器分别指向同一个容器中的两个元素或超出末端的下一位置,通常将它们命名为
first 和 last,或 beg 和 end,用于标记容器中的一段元素范围。

尽管 last 和 end 这两个名字很常见,但是它们却容易引起误解。其实第二个迭代器从来都不是指向元素范围的最后一个元素,而是指向最后一个元素的下一位置。该范围内的元素包括迭代器 first 指向的元素,以及从 first 开始一直到迭代器 last 指向的位置之前的所有元素。如果两个迭代器相等,则迭代
器范围为空。

此类元素范围称为左闭合区间(left-inclusive interval),其标准表示方式为:

// to be read as: includes first and each element up to but not including last
[ first, last )

表示范围从 first 开始,到 last 结束,但不包括 last。迭代器 last 可以等于 first,或者指向 first 标记的元素后面的某个元素,但绝对不能指向first 标记的元素前面的元素。

使用左闭合区间的编程意义

因为左闭合区间有两个方便使用的性质,所以标准库使用此烦区间。假设first 和 last 标记了一个有效的迭代器范围,于是:

  1. 当 first 与 last 相等时,迭代器范围为空;
  2. 当 first 与不相等时,迭代器范围内至少有一个元素,而且 first 指向该区间中的第一元素。此外,通过若干次自增运算可以使 first 的值不断增大,直到 first == last 为止。

这两个性质意味着程序员可以安全地编写如下的循环,通过测试迭代器处理一段元素:

while (first != last) {
// safe to use *first because we know there is at least one element
	++first;
}

假设 first 和 last 标记了一段有效的迭代器范围,于是我们知道要么first == last,这是退出循环的情况;要么该区间非空,first 指向其第一个元素。因为 while 循环条件处理了空区间情况,所以对此无须再特别处理。当迭代器范围非空时,循环至少执行一次。由于循环体每次循环就给 first 加 1,
因此循环必定会终止。而且在循环内可确保 *first 是安全的:它必然指向first 和 last 之间非空区间内的某个特定元素。

使迭代器失效的容器操作

后面几章会介绍一些容器操作会修改容器的内在状态或移动容器内的元素这样的操作使所有指向被移动的元素的迭代器失效,也可能同时使其他迭代器失效。使用无效迭代器是没有定义的,可能会导致与悬垂指针相同的问题。
例如,每种容器都定义了一个或多个 erase 函数。这些函数提供了删除容器元素的功能。任何指向已删除元素的迭代器都具有无效值,毕竟,该迭代器指向了容器中不再存在的元素

使用迭代器编写程序时,必须留意哪些操作会使迭代器失效。使用无效迭代器将会导致严重的运行时错误。

无法检查迭代器是否有效,也无法通过测试来发现迭代器是否已经失效。任何无效迭代器的使用都可能导致运行时错误,但程序不一定会崩溃,否则检查这种错误也许会容易些。

使用迭代器时,通常可以编写程序使得要求迭代器有效的代码范围相对较短。然后,在该范围内,严格检查每一条语句,判断是否有元素添加或删除,从而相应地调整迭代器的值。

每种顺序容器都提供了一组有用的类型定义以及以下操作

每种顺序容器都提供了一组有用的类型定义以及以下操作:

  • 在容器中添加元素。
  • 在容器中删除元素。
  • 设置容器大小。
  • (如果有的话)获取容器内的第一个和最后一个元素。

容器定义的类型别名

在前面的章节里,我们已经使用过三种由容器定义的类型size_typeiteratorconst_iterator。所有容器都提供这三种类型以及表 9.5 所列出的其他类型。

类型别名 作用
size_type 无符号整型,足以存储此容器类型的最大可能容器长度
iterator 此容器类型的迭代器类型
const_iterator 元素的只读迭代器类型
reverse_iterator 按逆序寻址元素的迭代器
const_reverse_iterator 元素的只读(不能写)逆序迭代器
difference_type 足够存储两个迭代器差值的有符号整型,可为负数
value_type 元素类型
reference 元素的左值类型,是 value_type& 的同义词
const_reference 元素的常量左值类型,等效于 const value_type&
表5. 容器定义的类型别名

书将在第 11.3.3 节中详细介绍逆序迭代器。简单地说,逆序迭代器从后向前遍历容器,并反转了某些相关的迭代器操作:例如,在逆序迭代器上做 ++ 运算将指向容器中的前一个元素。
表 5 的最后三种类型使程序员无须直接知道容器元素的真正类型,就能使用它。需要使用元素类型时,只要用 value_type即可。如果要引用该类型,则通过 referenceconst_reference 类型实现。在程序员编写自己的泛型程序(第十六章)时,这些元素相关类型的定义非常有用。
使用容器定义类型的表达式看上去非常复杂:

// iter is the iterator type defined by list<string>
list<string>::iterator iter;
// cnt is the difference_type type defined by vector<int>
vector<int>::difference_type cnt;

iter 所声明使用了作用域操作符,以表明此时所使用的符号 :: 右边的类型名字是在符号 iter 左边指定容器的作用域内定义的。其效果是将 iter 声明为 iterator 类型,而 iterator 是存放 string 类型元素的 list 类的成员。

begin 和 end 成员

begin 和 end 操作产生指向容器内第一个元素和最后一个元素的下一位置的迭代器,如表 6 所示。这两个迭代器通常用于标记包含容器中所有元素的迭代器范围。

迭代器 作用
c.begin() 返回一个迭代器,它指向容器 c 的第一个元素
c.end() 返回一个迭代器,它指向容器 c 的最后一个元素的下一位置
c.rbegin() 返回一个逆序迭代器,它指向容器 c 的最后一个元素
c.rend() 返回一个逆序迭代器,它指向容器 c 的第一个元素前面的位置

上述每个操作都有两个不同版本:一个是 const 成员(第 7.7.1 节),另一个是非 const 成员。这些操作返回什么类型取决于容器是否为 const。如果容器不是 const,则这些操作返回 iterator 或 reverse_iterator 类型。如果容器是 const,则其返回类型要加上 const_ 前缀,也就是 const_iterator 和const_reverse_iterator 类型。我们将在第 11.3.3 节中详细介绍逆序迭代器。

在顺序容器中添加元素

第 3.3.2 节介绍了添加元素的一种方法:push_back所有顺序容器都支持push_back 操作(表 7),提供在容器尾部插入一个元素的功能。下面的循环每次读入一个 string 类型的值,并存放在 text_word: 对象中:

// read from standard input putting each word onto the end of container string text_word;
while (cin >> text_word)
	container.push_back(text_word);

调用 push_back 函数会在容器 container 尾部创建一个新元素,并使容器的长度加 1。新元素的值为 text_word 对象的副本,而 container 的类型则可能是 list、vector 或 deque。
除了 push_back 运算,list 和 deque 容器类型还提供了类似的操作push_front。这个操作实现在容器首部插入新元素的功能。例如:

list<int> ilist;
// add elements at the end of ilist
for (size_t ix = 0; ix != 4; ++ix)
	ilist.push_back(ix);

使用 push_back 操作在容器 ilist 尾部依次添加元素 0、1、2、3。然后,我们选择用 push_front 操作再次在 ilist 中添加元素:

// add elements to the start of ilist
for (size_t ix = 0; ix != 4; ++ix)
	ilist.push_front(ix);

此时,元素 0、1、2、3 则被依次添加在 ilist 的开始位置。由于每个元素都在 list 的新起点插入,因此它们在容器中以逆序排列,循环结束后,ilist内的元素序列为:3、2、1、0、0、1、2、3。
表 7 在顺序容器中添加元素的操作:

操作 作用
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 类型

在容器中指定位置添加元素

使用 push_back 和 push_front 操作可以非常方便地在顺序容器的尾部或首部添加单个元素。而 insert 操作则提供了一组更通用的插入方法,实现在容器的任意指定位置插入新元素。 insert 操作有三个版本(表 7)。第一个版本需要一个迭代器和一个元素值参数,迭代器指向插入新元素的位置。下面的程序就是使用了这个版本的 insert 函数在容器首部插入新元素:

vector<string> svec;
list<string> slist;
string spouse("Beth");
// equivalent to calling slist.push_front (spouse);
slist.insert(slist.begin(), spouse);
// no push_front on vector but we can insert before begin()
// warning: inserting anywhere but at the end of a vector is an expensive operation
svec.insert(svec.begin(), spouse);

新元素在insert 函数中是插入在迭代器指向的位置之前迭代器可以指向容器的任意位置,包括超出末端的下一位置。由于迭代器可能指向超出容器末端的下一位置,这是一个不存在的元素,因此 insert 函数是在其指向位置之前而非其后插入元素。下面的代码就在 iter 指向的元素前面插入 spouse 的副本。

slist.insert(iter, spouse); // insert spouse just before iter

这个版本的 insert 函数返回指向新插入元素的迭代器。可使用该返回值在容器中的指定位置重复插入元素:

list<string> lst;
list<string>::iterator iter = lst.begin();
while (cin >> word)
iter = lst.insert(iter, word); // same as calling push_front  这里的iter不断的赋值变化

要彻底地理解上述循环是如何执行的,这一点非常重要——特别是要明白我们为什么说上述循环等效于调用 push_front 函数。
循环前,将 iter 初始化为 lst.begin()。此时,由于该 list 对象是空的,因此 lst.begin() 与 lst.end() 相等,于是 iter 指向该(空)容器的超出末端的下一位置。第一次调用 insert 函数时,将刚读入的元素插入到 iter 所指向位置的前面,容器 lst 得到第一个也是唯一的元素。然后 insert 函数返回指向这个新元素的迭代器,并赋给 iter,接着重复 while 循环,读入下一个单词。只要有单词要插入,每次 while 循环都将新元素插入到 iter 前面,然后重置 iter 指向新插入元素。新插入的元素总是容器中的第一个元素,因此,每次迭代器都将元素插入在该 list 对象的第一元素前面。

插入一段元素

insert 函数的第二个版本提供在指定位置插入指定数量的相同元素的功能:

svec.insert(svec.end(), 10, "Anna");

上述代码在容器 svec 的尾部插入 10 个元素,每个新元素都初始化为 “Anna”。
insert 函数的最后一个版本实现在容器中插入由一对迭代器标记的一段范围内的元素。例如,给出以下 string 类型的数组:

string sarray[4] = {"quasi", "simba", "frollo", "scar"};

可将该数组中所有的或其中一部分元素插入到 string 类型的 list 容器中:

// insert all the elements in sarray at end of slist
slist.insert(slist.end(), sarray, sarray+4);
list<string>::iterator slist_iter = slist.begin();
// insert last two elements of sarray before slist_iter
slist.insert(slist_iter, sarray+2, sarray+4);

添加元素可能会使迭代器失效

正如我们在第 9.4 节中了解的一样,在 vector 容器中添加元素可能会导致整个容器的重新加载,这样的话,该容器涉及的所有迭代器都会失效。即使需要重新加载整个容器,指向新插入元素后面的那个元素的迭代器也会失效。
任何 insert 或 push 操作都可能导致迭代器失效。当编写循环将元素插入到 vector 或 deque 容器中时,程序必须确保迭代器在每次循环后都得到更新。

避免存储end 操作返回的迭代器

在 vector 或 deque 容器中添加元素时,可能会导致某些或全部迭代器失效。假设所有迭代器失效是最安全的做法。这个建议特别适用于由 end 操作返回的迭代器。在容器的任何位置插入任何元素都会使该迭代器失效。
例如,考虑一个读取容器中每个元素的循环,对读出元素做完处理后,在原始元素后面插入一个新元素。我们希望该循环可以处理每个原始元素,然后使用insert 函数插入新元素,并返回指向刚插入元素的迭代器。在每次插入操作完成后,给返回的迭代器自增 1,以使循环定位在下一个要处理的原始元素。如果我们尝试通过存储 end() 操作返回的迭代器来“优化”该循环,将导致灾难性错误:

vector<int>::iterator first = v.begin(),
last = v.end(); // cache end iterator
// diaster: behavior of this loop is undefined
while (first != last) {
// do some processing
// insert new value and reassign first, which otherwise would be invalid
	first = v.insert(first, 42);
	++first; // advance first just past the element we added
}

上述代码的行为未定义。在很多实现中,该段代码将导致死循环。问题在于这个程序将 end 操作返回的迭代器值存储在名为 last 的局部变量中。循环体中实现了元素的添加运算,添加元素会使得存储在 last 中的迭代器失效(该迭代器既没有指向容器 v 的元素,也不再指向 v 的超出末端的下一位置。)。

不要存储 end 操作返回的迭代器。添加或删除 deque 或vector 容器内的元素都会导致存储的迭代器失效。

为了避免存储 end 迭代器,可以在每次做完插入运算后重新计算 end 迭代器值:

// safer: recalculate end on each trip whenever the loop adds/erases elements
while (first != v.end()) {  //注意这里的v.end
	first = v.insert(first, 42); // insert new value
	++first; // advance first just past the element we added
}

关系操作符

所有的容器类型都支持用关系操作符(第 5.2 节)来实现两个容器的比较。
比较的容器必须具有相同的容器类型,而且其元素类型也必须相同。例如,vector 容器只能与 vector 容器比较,而不能与 list 或vector 容器比较,而不能与 list 或 vector 类型的容器比较。
容器的比较是基于容器内元素的比较。容器的比较使用了元素类型定义的同一个关系操作符:两个容器做 != 比较使用了其元素类型定义的 != 操作符。如果容器的元素类型不支持某种操作符,则该容器就不能做这种比较运算。
下面的操作类似于 string 类型的关系运算(第 3.2.3 节):

  • 如果两个容器具有相同的长度而且所有元素都相等,那么这两个容器就相
    等;否则,它们就不相等。
  • 如果两个容器的长度不相同,但较短的容器中所有元素都等于较长容器中
    对应的元素,则称较短的容器小于另一个容器。
  • 如果两个容器都不是对文的初始子序列,则它们的比较结果取决于所比较
    的第一个不相等的元素。

理解上述操作的最简单方法是研究例程:

/*
ivec1: 1 3 5 7 9 12
ivec2: 0 2 4 6 8 10 12
ivec3: 1 3 9
ivec4: 1 3 5 7
ivec5: 1 3 5 7 9 12
//当一个迭代器指向的元素在容器中位于另一个迭代器指向的元素之前,则前一个迭代器小于后一个迭代器。
*/
// ivec1 and ivec2 differ at element[0]: ivec1 greater than ivec2
ivec1 < ivec2 // false
ivec2 < ivec1 // true
// ivec1 and ivec3 differ at element[2]: ivec1 less than ivec3
ivec1 < ivec3 // true
// all elements equal, but ivec4 has fewer elements, so ivec1 is greater than ivec4
ivec1 < ivec4 // false
ivec1 == ivec5 // true; each element equal and same number of elements
ivec1 == ivec4 // false; ivec4 has fewer elements than ivec1
ivec1 != ivec4 // true; ivec4 has fewer elements than ivec1

使用元素提供的关系操作符实现容器的关系运算

C++ 语言只允许两个容器做其元素类型定义的关系运算。
所有容器都通过比较其元素对来实现关系运算:ivec1 < ivec2
假设 ivec1 和 ivec2 都是 vector 类型的容器,则上述比较使用了内置 int 型定义的小于操作符。如果这两个 vector 容器存储的是 strings 对象,则使用 string 类型的小于操作符。
如果上述 vector 容器存储 第 1.5 节定义的 Sales_item 类型的对象,则该比较运算不合法。因为 Sales_item 类型没有定义关系运算,所以不能比较存放 Sales_items 对象的容器:

vector<Sales_item> storeA;
vector<Sales_item> storeB;
if (storeA < storeB) // error: Sales_item has no less-than operator

容器大小的操作

所有容器类型都提供四种与容器大小相关的操作(表 9.8)第 3.2.3 节已经使用了 size 和 empty 函数:size 操作返回容器内元素的个数:empty 操作则返回一个布尔值,当容器的大小为 0 时,返回值为 true,否则为 false。
表8. 顺序容器的大小操作:

操作 作用
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 操作来改变容器所包含的元素个数。如果当前的容器长度大于新的长度值,则该容器后部的元素会被删除;如果当前的容器长度小于新的长度值,则系统会在该容器后部添加新元素:

list<int> ilist(10, 42); // 10 ints: each has value 42
ilist.resize(15); // adds 5 elements of value 0 to back of ilist
ilist.resize(25, -1); // adds 10 elements of value -1 to back of ilist
ilist.resize(5); // erases 20 elements from the back of ilist

resize 操作可带有一个可选的元素值形参。如果在调用该函数时提供了这个参数,则所有新添加的元素都初始化为这个值。如果没有这个参数,则新添加的元素采用值初始化(第 3.3.1 节)。
resize 操作可能会使迭代器失效。在 vector 或 deque 容器上做 resize 操作有可能会使其所有的迭代器都失效。
对于所有的容器类型,如果 resize 操作压缩了容器,则指向已删除的元素迭代器失效。

访问元素

如果容器非空,那么容器类型的 front 和 back 成员(表 9)将返回容器内第一个或最后一个元素的引用

// check that there are elements before dereferencing an iterator
// or calling front or back
	if (!ilist.empty()) {
// val and val2 refer to the same element
	list<int>::reference val = *ilist.begin();
	list<int>::reference val2 = ilist.front();
// last and last2 refer to the same element
	list<int>::reference last = *--ilist.end();
	list<int>::reference last2 = ilist.back(); }

表 9. 访问顺序容器内元素的操作:

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

这段程序使用了两种不同的方法获取时 ilist 中的第一个和最后一个元素 的引用。直接的方法是调用 front 或 back 函数。间接的方法是,通过对 begin操作返回的迭代器进行解引用,或对 end 操作返回的迭代器的前一个元素位置进行解引用,来获取对同一元素的引用。在这段程序中,有两个地方值得注意:
end 迭代器指向容器的超出末端的下一位置,因此必须先对其减 1 才能获取最后一个元素;另一点是,在调用 front 或 back 函数之前,或者在对 begin 或end 返回的迭代器进行解引用运算之前,必须保证 ilist 容器非空。如果该list 容器为空,则 if 语句内所有的操作都没有定义。
第 3.3.2 节介绍了下标运算,我们注意到程序员必须保证在指定下标位置上的元素确实存在。下标操作符本身不会做相关的检查。使用 front 或 back 运算时,必须注意同样的问题。如果容器为空,那么这些操作将产生未定义的结果。如果容器内只有一个元素,则 front 和 back 操作都返回对该元素的引用。使用越界的下标,或调用空容器的 front 或 back 函数,都会导致程序出现严重的错误。
使用下标运算的另一个可选方案是 at 成员函数(表 9.9)。这个函数的行为和下标运算相似,但是如果给出的下标无效,at 函数将会抛出 out_of_range异常(第 6.13 节):

vector<string> svec; // empty vector
cout << svec[0]; // run-time error: There are no elements
in svec!
cout << svec.at(0); // throws out_of_range exception

删除元素

回顾前面的章节,我们知道容器类型提供了通用的 insert 操作在容器的任何位置插入元素,并支持特定的 push_front 和 push_back 操作在容器首部或尾部插入新元素。类似地,容器类型提供了通用的 erase 操作和特定的pop_front 和 pop_back 操作来删除容器内的元素(表 10)。
表 10. 删除顺序容器内元素的操作:

操作 作用
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 容器)

删除第一个或最后一个元素

pop_front 和 pop_back 函数用于删除容器内的第一个和最后一个元素。但vector 容器类型不支持 pop_front 操作。这些操作删除指定的元素并返回void。
pop_front 操作通常与 front 操作配套使用,实现以栈的方式处理容器:

while (!ilist.empty()) {
	process(ilist.front()); // do something with the current top of ilist
	ilist.pop_front(); // done; remove first element
}

这个循环非常简单:使用 front 操作获取要处理的元素,然后调用pop_front 函数从容器 list 中删除该元素。pop_front 和 pop_back 函数的返回值并不是删除的元素值,而是 void。要获取删除的元素值,则必须在删除元素之前调用notfrontback 函数。

删除容器内的一个元素

删除一个或一段元素更通用的方法是 erase 操作。该操作有两个版本:删除由一个迭代器指向的单个元素,或删除由一对迭代器标记的一段元素。erase的这两种形式都返回一个迭代器,它指向被删除元素或元素段后面的元素。也就是说,如果元素 j 恰好紧跟在元素 i 后面,则将元素 i 从容器中删除后,删除操作返回指向 j 的迭代器。
如同其他操作一样,erase 操作也不会检查它的参数。程序员必须确保用作参数的迭代器或迭代器范围是有效的。
通常,程序员必须在容器中找出要删除的元素后,才使用 erase 操作。寻找一个指定元素的最简单方法是使用标准库的 find 算法。我们将在第 11.1 节中进一步讨论find 算法。为了使用 find 函数或其他泛型算法,在编程时,必须将 algorithm 头文件包含进来。find 函数需要一对标记查找范围的迭代器以及一个在该范围内查找的值作参数。查找完成后,该函数返回一个迭代器,它指向具有指定值的第一个元素,或超出末端的下一位置。

string searchValue("Quasimodo");
list<string>::iterator iter =find(slist.begin(), slist.end(), searchValue);
if (iter != slist.end())
	slist.erase(iter);

注意,在删除元素之前,必须确保迭代器是不是 end 迭代器。使用 erase 操作删除单个必须确保元素确实存在——如果删除指向超出末端的下一位置的迭代器,那么 erase 操作的行为未定义。

删除容器内所有元素

要删除容器内所有的元素,可以调用 clear 函数,或将 begin 和 end 迭代器传递给 erase 函数

slist.clear(); // delete all the elements within the container
slist.erase(slist.begin(), slist.end()); // equivalent

erase 函数的迭代器对版本提供了删除一部分元素的功能:

// delete range of elements between two values
list<string>::iterator elem1, elem2;
// elem1 refers to val1
elem1 = find(slist.begin(), slist.end(), val1);
// elem2 refers to the first occurrence of val2 after val1
elem2 = find(elem1, slist.end(), val2);
// erase range from val1 up to but not including val2
slist.erase(elem1, elem2);

这段代码首先调用了 find 函数两次,以获得指向特定元素的两个迭代器。迭代器 elem1 指向第一个具有 val1 值的元素,如果容器 list 中不存在值为val1 的元素,则该迭代器指向超出末端的下一位置。如果在 val1 元素后面存在值为 val2 的元素,那么迭代器 elem2 就指向这段范围内第一个具有 val2
值的元素,否则,elem2 就是一个超出末端的迭代器。最后,调用 erase 函数删除从迭代器 elem1 开始一直到 elem2 之间的所有元素,但不包括 elem2 指 向的元素。

erase、pop_front 和 pop_back 函数使指向被删除元素的所有迭代器失效。对于 vector 容器,指向删除点后面的元素的迭代器通常也会失效。而对于 deque 容器,如果删除时不包含第一个元素或最后一个元素,那么该 deque 容器相关的所有迭代器都会失效。

赋值与swap

与赋值相关的操作符都作用于整个容器除 swap 操作外,其他操作都可以用 erase 和 insert 操作实现(表 11)。赋值操作符首先 erases 其左操作数容器中的所有元素,然后将右操作数容器的所有元素 inserts 到左边容器中:

c1 = c2; // replace contents of c1 with a copy of elements in c2
// equivalent operation using erase and insert   等效的操作
c1.erase(c1.begin(), c1.end()); // delete all elements in c1
c1.insert(c1.begin(), c2.begin(), c2.end()); // insert c2

赋值后,左右两边的容器相等:尽管赋值前两个容器的长度可能不相等,但赋值后两个容器都具有右操作数的长度。
赋值和 assign 操作使左操作数容器的所有迭代器失效。swap操作则不会使迭代器失效。完成 swap 操作后,尽管被交换的元素已经存放在另一容器中,但迭代器仍然指向相同的元素。

表 11. 顺序容器的赋值操作

操作 作用
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 操作首先删除容器中所有的元素,然后将其参数所指定的新元素插入到该容器中。与复制容器元素的构造函数一样,如果两个容器类型相同,其元素类型也相同,就可以使用赋值操作符(=)将一个容器赋值给另一个容器。如果在不同(或相同)类型的容器内,元素类型不相同但是相互兼容,则其赋值运算必须使用 assign 函数。例如,可通过 assign 操作实现将 vector 容器中一段 char* 类型的元素赋给 string 类型 list 容器。
由于 assign 操作首先删除容器中原来存储的所有元素,因此,传递给 assign 函数的迭代器不能指向调用该函数的容器内的元素。assign 函数的参数决定了要插入多少个元素以及新元素的值是什么。语句

// equivalent to slist1 = slist2
slist1.assign(slist2.begin(), slist2.end());
//c.assign(b,e)	重新设置 c 的元素:将迭代器 b 和 e 标记的范围内所有的元素复制到 c 中。b 和 e 必须不是指向 c 中元素的迭代器

使用了带一对迭代器参数的 assign 函数版本。在删除 slist1 的元素后,该函数将 slist2 容器内一段指定的元素复制到 slist2 中。于是,这段代码行等效于将 slist1 赋给 slist2。
带有一对迭代器参数的 assign 操作允许我们将一个容器的元素赋给另一个不同类型的容器。
assign 运算的第二’;个版本需要一个整型数值和一个元素值做参数,它将容器重置为存储指定数量的元素,并且每个元素的值都为指定值:

// equivalent to: slist1.clear();
// followed by slist1.insert(slist1.begin(), 10, "Hiya!");
slist1.assign(10, "Hiya!"); // 10 elements; each one is Hiya!

执行了上述语句后,容器 slist1 有 10 个元素,每个元素的值都是 Hiya!

使用swap 操作以节省删除元素的成本

swap 操作实现交换两个容器内所有元素的功能。要交换的容器的类型必须匹配:操作数必须是相同类型的容器,而且所存储的元素类型也必须相同。调用了 swap 函数后,右操作数原来存储的元素被存放在左操作数中,反之亦然。

vector<string> svec1(10); // vector with 10 elements
vector<string> svec2(24); // vector with 24 elements
svec1.swap(svec2);

执行 swap 后,容器 svec1 中存储 24 个 string 类型的元素,而 svec2 则存储 10 个元素。
关于 swap 的一个重要问题在于:该操作不会删除或插入任何元素,而且保证在常量时间内实现交换。由于容器内没有移动任何元素,因此迭代器不会失效。
没有移动元素这个事实意味着迭代器不会失效。它们指向同一元素,就像没作 swap 运算之前一样。虽然,在 swap 运算后,这些元素已经被存储在不同的容器之中了。例如,在做 swap 运算之前,有一个迭代器 iter 指向 svec1[3] 字符串;实现 swap 运算后,该迭代器则指向 svec2[3] 字符串(这是同一个字符串,只是存储在不同的容器之中而已)。

关键概念

对形成迭代器范围的迭代器的要求

迭代器 first 和 last 如果满足以下条件,则可形成一个迭代器范围:

  • 它们指向同一个容器中的元素或超出末端的下一位置。
  • 如果这两个迭代器不相等,则对 first 反复做自增运算必须能够到达 last。换句话说,在容器中,last 绝对不能位于 first 之前。

编译器自己不能保证上述要求。编译器无法知道迭代器所关联的是哪个容器,也不知道容器内有多少个元素。若不能满足上述要求,将导致运行时未定义的行为。

容器元素都是副本

在容器中添加元素时,系统是将元素值复制到容器里。类似地,使用一段元素初始化新容器时,新容器存放的是原始元素的副本。被复制的原始值与新容器中的元素各不相关,此后,容器内元素值发生变化时,被复制的原值不会受到影响,反之亦然

参考资料

【1】C++ Primer 中文版(第四版·特别版)

注解

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104779035