《C++Primer 第五版》——第九章 顺序容器

《C++Primer 第五版》——第九章 顺序容器

  • 9.1 顺序容器概述
      • 如何确定使用哪种容器
  • 9.2 容器库概览
      • 对容器可以保存的元素类型的限制
    • 9.2.1 迭代器
      • 使用左闭合范围蕴含的编程假定
    • 9.2.2 容器类型成员
    • 9.2.3 begin 和 end 成员
    • 9.2.4 容器定义和初始化
      • 将一个容器初始化为另一个容器的拷贝
      • 容器的列表初始化
      • 与顺序容器大小相关的构造函数
      • 标准库 array 具有固定大小
    • 9.2.5 赋值和swap
      • 使用 assign(仅顺序容器)
      • 使用 swap
    • 9.2.6 容器大小操作
    • 9.2.7 关系运算符
      • 容器的关系运算符使用元素的关系运算符完成比较
  • 9.3 顺序容器特有的操作
    • 9.3.1 向顺序容器添加元素
      • 使用 push_back
      • 使用 push_front
      • 在容器中的特定位置添加元素
      • 插入范围内元素
      • 使用 insert 的返回值
      • 使用 emplace 操作
    • 9.3.2 访问元素
      • 访问顺序容器成员的函数返回的是引用
      • 下标操作和安全的随机访问
    • 9.3.3 删除元素
      • pop_front 和 pop_back 成员函数
      • 从容器内部删除一个元素
      • 删除多个元素
    • 9.3.4 特殊的 forwar_list 操作
    • 9.3.5 改变容器大小
    • 9.3.6 某些容器操作可能使迭代器、引用、指针失效
      • 编写改变容器的循环程序
      • 不要保存成员函数 end 返回的迭代器
  • 9.4 vector 对象是如何增长的
      • 管理容量的成员函数
      • 容器的 capacity 和 size
  • 9.5 额外的 string 操作
    • 9.5.1 构造 string 的其他方法
      • substr操作
    • 9.5.2 改变 string 的其他方法
      • 成员函数 append 和 replace
      • 改变 string 的多种重载函数
    • 9.5.3 string 搜索操作
      • 指定在哪里开始搜索
      • 逆向搜索
    • 9.5.4 compare 函数
    • 9.5.5 数值转换
  • 9.6 容器适配器
      • 定义一个适配器
      • 栈适配器
      • 队列适配器


一个容器就是一些特定类型对象的集合。 顺序容器(sequentialcontainer) 指实现能按顺序访问的数据结构,它为程序员提供了控制元素存储和访问顺序的能力。 这种访问顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。与之相对的,将在第11章介绍的有序和无序关联容器,则根据关键字的值来存储元素

标准库还提供了三种容器适配器,分别为容器操作定义了不同的接口,来与容器类型适配,将在本章末尾介绍适配器。

所有容器类都共享公共的接口,不同容器按不同方式对其进行扩展。 这个公共接口使容器的学习更加容易——基于某种容器所学习的内容也都适用于其他容器。每种容器都提供了不同的性能和功能的权衡。


9.1 顺序容器概述

下表列出了标准库中定义的顺序容器,所有顺序容器都提供了快速顺序访问元素的能力。但是,这些容器在以下方面都有不同的性能折中:

  • 向容器添加或从容器中删除元素的代价
  • 非顺序访问容器中元素的代价
顺序容器类型 介绍
vector 可变大小数组支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢
deque 双端队列支持快速随机访问。在头尾位置插入/删除速度很快
list 双向链表只支持双向顺序访问。在 list 中任何位置进行插入/删除操作速度都很快
forward 单向链表只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快
array 固定大小数组支持快速随机访问。不能添加或删除元素
string 与 vector 相似的容器,但专门用于保存字符随机访问快。在尾部插入/删除速度快,其它位置慢

除了固定大小的array外,其他容器都提供高效、灵活的内存管理。 即可以添加和删除元素,扩张和收缩容器的大小。容器保存元素的策略对容器操作的效率有着固定的,有时是重大的影响。在某些情况下,存储策略还会影响特定容器是否支持特定操作。

总的来说:

stringvector将元素保存在连续的内存空间中,由元素的下标来计算其地址是非常快速的,但是在两种容器的中间位置添加或删除元素非常耗时。而且,添加一个元素有时可能还需要分配额外的存储空间,此时每个元素都必须移动到新的存储空间中。
listforward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问:即为了访问一个元素,我们只能遍历整个容器。与vectordequearray相比,这两个容器的额外内存开销也很大
deque是一个更为复杂的数据结构,元素可以从两端弹出。与stringvector类似,deque支持快速随机访问stringvector一样,在deque的中间位置添加或删除元素的代价(可能)很高 。但是deque的两端添加或删除元素很快,与list或forward_list添加/删除元素的速度相当
forward_listarray是新C++标准增加的类型。与内置的数组类型相比,array更加安全、更容易使用。forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此,forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size操作保证是一个快速的常量时间(即与任何变量无关)的操作。

Note: 新标准库的容器比旧版本的快很多,原因将在13.6节解释。新标准库容器的性能几乎肯定与最精心优化过的同类数据结构一样好(通常会更好)。现代C++程序应该使用标准库容器,而不是更原始的数据结构,比如内置的数组类型。

如何确定使用哪种容器

Tip: 通常,使用vector是最好的选择,除非你有很好的理由选择其他容器。

一些选择容器的基本原则:

  • 除非有很好的理由选择其他容器,否则使用vector是最好的选择;
  • 如果程序有很多小元素且空间的额外开销很重要,不要使用listforward_list
  • 要求随机访问元素,应该使用vectordeque
  • 要求中间插入或删除元素,应该使用listforward_list
  • 需要在头尾插入或删除元素,且中间不进行插入或删除,应该使用deque
  • 如果只在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素。首先可以考虑在读取输入时使用vector,再调用sort函数重排容器中的元素,从而避免在中间位置添加元素。如果必须在中间位置插入元素,考虑在输入阶段使用list,输入完成将list拷贝到vector

如果程序即需要随机访问元素,又需要在容器中间位置插入元素,怎么办?

取决于listforward_list中访问元素与vectordeque中插入/删除元素的相对性能。 即哪边的缺点相对更好。
一般来说,应用中占主导地位的操作(执行的访问操作更多还是插入/删除的更多)决定了容器类型的选择,即性能优先。
在此情况下,对两种容器分别测试应用的性能可能就是必要的了。

Note: 如果实在不确定使用哪种容器,可以在程序中只使用vector和list的公共操作迭代器而非下标,避免随机访问。这样可以在必要时选择使用vector或list。


9.2 容器库概览

容器类型上的操作形成了一种层次:

  • 某些操作是所有容器类型都提供的
  • 另外一些操作仅针对顺序容器、关联容器或无序容器。
  • 还有一些操作只适用于一小部分容器。

在9.2节中,将介绍对所有容器都适用的操作本章剩余部分将聚焦于仅适用于顺序容器的操作。关联容器特有的操作将在第11章介绍。

一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。即deque定义在头文件deque中,list定义在头文件list中,以此类推。标准库的容器均定义为模板类(class template)(参见3.3节)。例如对vector,我们必须提供额外信息来生成特定的容器类型。对大多数,但不是所有容器,我们必须额外提供元素类型信息

list<Sales_data>// 保存Sales_data对象的list
deque<double>	// 保存double的deque

对容器可以保存的元素类型的限制

顺序容器几乎可以保存任意类型的元素。 特别的是 可以定义一个容器,其元素的类型是另一个容器。这种容器的定义方式与任何其他容器类型完全一样 :在尖括号中指定元素类型:

vector<vector<string>> lines;	//存储vector的vector,此处lines是一个vector,其元素类型是vector

虽然可以在容器中保存几乎任何类型,但是某些容器操作对元素类型有其自己的特殊要求。所以可以为不支持特定操作需求的类型定义容器,但这种情况下就只能使用那些没有特殊要求的容器操作了。

例如,顺序容器构造函数的一个版本接受容器大小参数(参见3.3.1节),它使用了元素类型的默认构造函数。但某些类没有默认构造函数。可以定义一个保存这种类型对象的容器,但在构造这种容器时不能只传递给它一个元素数目参数

// 假定noDefault是一个没有默认构造函数的类型
// init是一个已经创建了的noDefault类型的对象
//  注意下面初始化方法只适用于顺序容器,不支持关联容器。
vector<noDefault> vl (10, init) ; 	// 正确:提供了元素初始化值 element initializer
vector<noDefault> v2 (10); 			// 错误:必须提供一个元素初始化值 element initializer

当后面介绍其他容器的通用操作时,还会注意到每个容器操作对元素类型的其他限制。

下面介绍一部分容器操作:

类型别名
iterator 此容器类型的迭代器类型
const_iterator 可以读取元素,但不能修改元素的迭代器类型
size_type 无符号整数类型,足够保存此种容器类型最大可能容器的大小
different_type 带符号整数类型,足够保存两个迭代器之间的距离
value_type 元素类型
reference 元素的左值类型,与 value_type& 含义相同
const_reference 元素的 const 左值类型,即 const value_type&
构造函数
C c; 默认构造函数,构造空容器
C c1(c2); 构造 c2 的拷贝 c1
C c(b, e) 构造 c, 将迭代器b 和 e 指定的范围内的元素拷贝到 c(array不支持)
C c{a, b, c...} 列表初始化 c
赋值与swap
c1 = c2; 将c1中的元素替换为c2中元素
c1 = {a, b, c...} 将c1中的元素替换为列表中元素(不适用于array)
a.swap(b) 交换a和b的元素
swap(a, b) 与a.swap(b)等价
大小
c.size( ) c中元素的数目(不支持forward_list)
c.max_size() c可保存的最大元素数目
c.empty() 若c中存储了元素,返回false,否则返回true
添加与删除元素(不适用于array)
注意:在不同容器中,这些操作的接口都不同
c.insert(args) 将args中的元素拷贝进c
c.emplace(inits) 使用inits构造c中的一个元素
c.erase(args) 删除args指定的元素
c.clear() 删除c中所有的元素,返回void
关系运算符
==,!= 所有的容器都支持相等运算符
<,<=,>,>= 关系运算符
获取迭代器
c.begin(),c.end() 返回指向c的首元素和尾元素之后位置的迭代器
c.cbegin(),c.cend() 返回 const_iterator
反向容器的额外成员(不支持forward_list)
reverse_iterator 按逆序寻址元素的迭代器
const_reverse_iterator 能修改元素的逆序迭代器
c.rbegin(),c.rend() 返回指向c的尾元素和首元素之前位置的迭代器
c.crbegin(),c.crend()r 返回const_reverse_iterato

9.2.1 迭代器

与容器一样,迭代器有着公共的接口如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。例如,标准容器类型上的所有迭代器都允许我们访问容器中的元素,而所有迭代器都是通过解引用运算符来实现这个操作的。类似的,标准库容器的所有迭代器都定义了递增运算符,从当前元素移动到下一个元素。

在3.4.1节中列出了容器迭代器支持的所有操作,其中有一个例外不符合公共接口特点—— forward_list 的迭代器不支持递减运算符(--。在3.4.2节中列出了某些迭代器支持的算术运算,这些算术运算只能应用于 stringvectordequearray 的迭代器,不能将它们用于其他任何容器类型的迭代器。

一个 迭代器范围(iteratorrange) 由一对迭代器表示,两个迭代器分别指向同一个容器中的某个元素或者是 尾元素之后的位置(one past the last element) 。这种元素范围被称为左闭合区间(left-inclusive interval)。其标准数学描述为 [begin,end) 。表示范围自begin开始,于end之前结束。迭代器begin和end必须指向相同的容器。end可以与begin指向相同的位置,但不能指向begin之前的位置。

对构成范围的迭代器的要求:

如果满足以下条件,两个迭代器begin和end构成一个迭代器范围:

  • 它们分别指向同一个容器中的元素,或者是容器最后一个元素之后的位置;
  • 且可以通过反复递增begin来到达end。换句话说,并且end不在begin之前。

编译器不强制要求上面两点要求,但程序员应该满足。

使用左闭合范围蕴含的编程假定

标准库使用左闭合范围是因为这种范围有三种方便的性质。假定迭代器begin和end构成一个合法的迭代器范围,则:

  • 如果begin与end相等,则范围为空
  • 如果begin与end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
  • 可以对begin递增若干次,使得begin=end

这些性质意味着可以像下面的代码一样用一个循环来处理一个元素范围,而这是安全的:

while (begin != end) {	// begin和end都是迭代器类型
    *begin = val;       // 正确:范围非空,因此begin指向一个元素
    ++begin;            // 移动迭代器,获取下一个元素
}

给定构成一个合法范围的迭代器 begin 和 end ,若 begin 和 end 相等,则范围为空。在此情况下,应该退出循环。如果范围不为空,begin指向此非空范围的一个元素。因此,在while循环体中,可以安全地解引用begin(*begin),因为begin必然指向容器里的一个元素。最后,由于每次循环对begin递增一次,可以保证循环最终会结束。

9.2.2 容器类型成员

每个容器都定义了多个类型,如上面所示。之前已经使用过其中三种:size_type (参见3.2.2节)、iteratorconst_iterator(参见3.4.1节)。

除了已经使用过的迭代器类型,大多数容器还提供反向迭代器。简单地说,反向迭代器就是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义也都发生了颠倒。例如,对一个反向迭代器执行++操作,会得到上一个元素。

剩下的就是类型别名了,通过类型别名,可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type。如果需要元素类型的一个引用,可以使用referenceconst_reference。这些元素相关的类型别名在泛型编程中非常有用。

为了使用这些类型,我们必须显式使用其类名:

list<string>::iterator iter;       	// iter 是通过list定义的一个迭代器类型
vector<int>::difference_type count;	// count 是通过vector定义的一个difference_type类型

这些声明语句使用了作用域运算符::来访问某个作用域,以便使用list类的iterator成员及vector类定义的difference_type

9.2.3 begin 和 end 成员

beginend成员函数分别(参见341节)生成指向容器中第一个元素和尾元素之后位置的迭代器。这两个迭代器最常见的用途是形成一个包含容器中所有元素的迭代器范围。

如之前所示,成员函数beginend有多个版本:带 r 版本的返回反向迭代器(后面将在10.4.3节中介绍相关内容),以 c 开头的版本则返回 const 迭代器(即const_iterator

ist<string> a = {"Milton","Shakespeare","Austen"};
auto itl = a.begin();	// list::iterator
auto it2 = a.rbegin();	// list::reverse_iterator
auto it3 = a.cbegin();	// list::const_iterator
auto it4 = a.crbegin();	// list::const_reverse_iterator

实际上有两个名为begin的成员函数。一个是const成员函数,返回容器的const_iterator类型;一个是非const成员,返回容器的iterator类型。rbeginendrend的情况类似。
当对一个非常量对象调用这些成员时,得到的是返回iterator的版本。只有在对一个const对象调用这些函数时,才会得到一个const_iterator版本。
与 const 指针和引用类似,可以将一个普通的iterator转换为对应的const_iterator,但反之不行。

以 c 开头的版本是C++新标准引入的,用以支持auto(参见2.5.2节),可以与beginend(包括r或c等版本)成员函数结合使用。以前没有其他选择,只能显式声明希望使用哪种类型的迭代器:

// 显式指定类型
list<string>::iterator it5 = a.begin();
list<string>::const_iterator it6 = a.begin();	// 是iterator还是const_iterator依赖于a的类型
// 新标准C++之后可以使用 auto 
auto it7 = a.begin();	// 仅当a是const时,it7是const_iterator
auto it8 = a.cbegin();	// it8是const_iterator

autobeginend成员函数结合使用时,获得的迭代器类型依赖于容器类型,与想要如何使用迭代器毫不相干。但以 c 开头的版本只会返回const_iterator对象,而不管容器的类型是不是常量。

Note: 当不需要对容器写访问时,应使用 cbegincend。这不对容器元素进行更改,仅仅是访问。

9.2.4 容器定义和初始化

标准库中的每个容器类型都定义了一个默认构造函数(参见7.1.4节)。除了array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。

容器定义和初始化(C指代顺序容器类型)
C c; 默认构造函数。如果C是一个array,则c中元素将默认初始化,否则c为空容器
C c1(c2) c1初始化为c2的拷贝。c1和c2必须是相同类型(即它们必须是相同的容器类型,且保存的是相同的元素类型对于array类型,两者还必须具有相同的大小
C c1=c2 相同的容器类型,且保存的是相同的元素类型对于array类型,两者还必须具有相同大小
C c{a,b,c...} c初始化为初始化列表中的元素的拷贝列表中元素的类型必须与C的元素类型相容对于array类型,列表中元素数目必须等于或小于array的大小,任何遗漏的元素都进行值初始化
C c = {a,b,c...}
C c(b,e) (array不适用)c初始化为迭代器b和e指定范围中的元素的拷贝范围中元素的类型必须与C的元素类型相容
只有顺序容器(不包括array)的构造函数才能接受大小参数
C seq(n) (string不适用) seq 包含n个元素,这些元素进行了值初始化;此构造函数是explicit的
C seq(n,t) seq 包含n个初始化为值t的元素

将一个容器初始化为另一个容器的拷贝

将一个新容器创建为另一个容器的拷贝的方法有两种:①可以直接拷贝整个容器;②或(array除外的容器)拷贝由一个迭代器对指定的元素范围。

如果通过直接拷贝整个容器,来将一个新容器创建为另一个容器的拷贝,则两个容器的类型及其元素类型必须匹配。 不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且,新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换(参见4.11节)为要初始化的容器的元素类型即可。

// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors={"Milton","Shakespeare","Austen"};
vector<const char *> articles={"a","an","the"};

list<string> list2(authors);	// 正确:类型匹配
deque<string> authList(authors);// 错误:容器类型不匹配
vector<string> words(articles);	// 错误:元素类型必须匹配
// 正确:可以将const char*元素转换为string
forward_list<string> words(articles.begin(),articles.end());

Note: 当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。

接受两个迭代器的构造函数用这两个迭代器表示程序员想要拷贝的一个元素范围。与以往一样,两个迭代器分别标记想要拷贝的第一个元素和尾元素之后的位置。新容器的大小与范围中元素的数目相同新容器中的每个元素都用范围中对应元素的值进行初始化

由于两个迭代器表示一个范围,因此可以使用这种构造函数来拷贝一个容器中的子序列。例如,假定迭代器it表示容器authors中的一个元素,我们可以编写如下代码:

// 拷贝元素,直到(但不包括)it指向的元素
deque<string> authList(authors.begin(),it);

容器的列表初始化

在新标准中可以对一个容器进行列表初始化(参见3.3.1节)。

// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"nMiltonn","Shakespeare","Austen"};
vector<const char*> articles = {"a","an","the"};

当这样做时,就显式指定了容器中每个元素的值。对于除array之外的容器类型,初始化列表还隐含地指定了容器的大小:容器将包含与初始值一样多的元素。

与顺序容器大小相关的构造函数

除了与关联容器相同的构造函数外,顺序容器(array除外)还提供另一个构造函数,它接受一个容器大小和一个(可选的)元素初始值。如果不提供元素初始值,则标准库会创建一个值初始化器(参见3.3.1节)

vector<int> ivec(10,-1);  	// 10个int元素,每个都初始化为-1
list<string> svec(10,"hi!");// 10个string:每个都初始化为"hi!”
forward_list<int> ivec(10);	// 10个元素,每个都初始化为0
deque<string> svec(10);    	// 10个元素,每个都是空string

如果元素的类型内置类型或者是具有默认构造函数的类类型则可以只为构造函数提供一个容器大小参数。反之,如果元素类型没有默认构造函数且还不是内置类型,除了大小参数外,还必须额外指定一显式的元素初始值

Note: 有顺序容器的构造函数才接受大小参数,关联容器并不支持接受大小参数。

标准库 array 具有固定大小

与C++的内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:

array<int,42>		// 类型为:保存42个int的array容器
array<string,10>	// 类型为:保存10个string的array容器

为了使用array类型,必须同时指定元素类型和大小:

array<int,10>::size_type i;	// 数组类型包括元素类型和大小
array<int>::size_type j;	// 错误:array不是一个类型

由于容器大小也是是array类型的一部分,array不支持普通的容器构造函数。这些构造函数都会确定容器的大小,要么隐式地或显式地。同时允许用户向一个array构造函数传递大小参数,最好情况下也是多余的,而且容易出错。

array大小固定的特性也影响了它所定义的构造函数的行为。与其他容器不同,一个默认构造的array是非空的它包含了与其大小一样多的元素这些元素都被默认初始化(参见2.2.1节),就像一个内置数组(参见3.5.1节)中的元素那样。如果对array进行列表初始化,初始值的数目必须等于或小于array的大小。如果初始值数目小于array的大小,则它们被用来初始化array中靠前的元素,所有剩余元素都会进行值初始化(参见3.3.1节)在这两种情况下,如果元素类型是一个类类型,那么该类必须有一个默认构造函数,以使值初始化能够进行:

array<int,10> ial;	// 10个默认初始化的int
array<int,10> ia2 = {0,1,2,3,4,5,6,7,8,9;// 列表初始化
array<int,10> ia3 = {42};	// ia3[0]为42,剩余元素为0

值得注意的是,虽然不能对内置数组类型进行拷贝或对象赋值操作(参见3.5.1节),但array并无此限制

int digs[10] = {0,1,2,3,4,5,6,7,8,9};
int cpy[10] = digs;	//错误:内置数组不支持拷贝或赋值

array<int,10> digits = {0,1,2,3,4,5,6,7,8,9};
array<int,10> copy = digits;	// 正确:只要array的元素类型匹配即合法
array<int,11> copy2 = digits;	// 错误:容器大小不一样

与其他容器一样,array也要求初始值的类型必须与要创建的容器类型相同。此外,array还要求元素类型和容器大小也都一样,因为大小是array类型的一部分

9.2.5 赋值和swap

下面表格中列出的与赋值相关的运算符可用于所有容器赋值运算符将其左边容器中的全部元素替换为右边容器中元素的拷贝:

cl = c2;		// 将cl的内容替换为c2中元素的拷贝
cl = (a,b,c);	// 赋值后,cl大小为3

第一个赋值运算后,左边容器将与右边容器相等。如果两个容器原来大小不同,赋值运算后两者的大小都与右边容器的原大小相同。 第二个赋值运算后,cl的size变为3,即花括号列表中值的数目

与内置数组不同,标准库array类型允许赋值操作。但是 赋值号左右两边的运算对象必须具有相同的类型(对于array,容器大小也是类型的一部分,所以相同类型的array的大小也相同):

array<int, 10>al = {0,1,2,3,4,5,6,7,8,9};
array<int, 10>a2 = {0};	// 所有元素值均为0
al = a2;	// 替换al中的元素
a2 = {0};	// 错误:不能将一个花括号列表赋值给array数组

由于两侧的运算对象的容器大小可能不同因此 array 类型不支持 assign ,也不允许用{}包围的值列表进行赋值。

容器赋值运算
c1=c2 将 c1 中元素替换为 c2 中元素的拷贝。 c1 和 c2 必须具有相同的类型,array也适用
c={a,b,c...} 将 c1 中元素替换为列表中元素的拷贝(不适用于array类型)
swap(c1,c2) 交换 c1 和 c2 中的元素, c1 和 c2 必须具有相同的类型, swap 操作通常比拷贝操作快很多
c1.swap(c2)
assign操作不适用于关联容器和 array
seq.assign(b,e) 将 seq 中的元素替换为迭代器 b 和 e 所表示的左包含范围中的元素。迭代器 b 和 e 不能指向 seq 中的元素
seq.assign( il ) 将 seq 中的元素替换为 初始化列表 il 中的元素(il 是{}包围起来的列表)
seq.assign(n,t) 将 seq 中的元素替换为 n 个值为 t 的元素

注意:

1)赋值相关运算导致指向左边容器内部的迭代器、引用和指针失效
2)而 swap 操作 只是将容器内容交换不会导致指向容器的迭代器、引用和指针失效 (容器类型为arraystring的情况例外)

指针、引用和迭代器的失效:

一个失效的指针、引用或迭代器将不再表示任何元素。

使用 assign(仅顺序容器)

前面小节提到了,赋值运算符要求左边和右边的运算对象具有相同的类型。它将右边运算对象中所有元素拷贝到左边运算对象中。

顺序容器(array除外)还定义了一个名为assign的成员函数,它允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。 成员函数assign用参数所指定的元素(的拷贝)替换左边容器中的所有元素。例如,可以用assgin实现将一个vector中的一段const char*值赋予一个list中的string

list<string> names;
vector<const char*> oldstyle;
names = oldstyle;//错误:容器类型不匹配
names.assign(oldstyle.cbegin(),oldstyle.cend());  //正确:可以将const char*转换为string

上述这段代码中对 assign 的调用将 names 中的元素替换为迭代器指定的范围中的元素的拷贝。 assign 的参数决定了容器中将有多少个元素以及它们的值都是什么。

注意:

由于其旧元素被替换,因此传递给 assign 成员函数的迭代器,不能指向调用该assign成员函数所属的容器对象(在上述代码中是 names )。

assign 成员函数的第二个版本接受一个整型值和一个元素值。它用指定数目但具有相同给定值的元素替换它所属容器中原有的元素

// 等价于 slist1.clear ();
// 后跟 slist1.insert(slist1.begin(),10,"Hiya" );
list<string> slist1(1); // 容器大小为 1 个元素,为空值
slist1.assign(10, "Hiya"); // 容器大小为 10 个元素,每个都是"Hiya”

使用 swap

swap 操作交换两个相同类型容器的内容。 调用swap函数之后,两个容器中的元素将会交换;

vector<string> svecl(10);	// 10个元素的vector
vector<string> svec2(24);	// 24个元素的vector
swap(svecl,svec2);

调用swap后,svec1将包含24个string元素,svec2将包含10个string

array外,交换两个容器内容的操作保证会很快——因为元素本身并未交换,swap函数只是交换了两个容器的内部数据结构。假设内部数据结构是房子,容器名是门牌号, swap 只交换了它们的门牌号,两个房子实质上并没有改变。

Note:

array外, swap 不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。

容器内元素不会被移动的事实意味着:string外,指向容器的迭代器、引用和指针在 swap 操作之后都不会失效。它们仍指向 swap 操作之前所指向的那些元素。 但是,在 swap 操作之后,这些元素已经属于不同的容器了。 例如,假定iter在swap之前指向svecl[3]的string,那么在swap之后它指向svec2[3]的元素。

1)与其他容器不同,对一个 string 对象调用swap会导致迭代器、引用和指针失效
2)与其他容器不同,swap 两个array会真正交换它们的元素。因此,交换两个array所需的时间与array中元素的数目成线性关系
3)所以对于array,在 swap 操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array中对应元素的值进行了交换。

在新标准库中,容器既提供成员函数版本的 swap ,也提供非成员版本的 swap 。而早期标准库版本只提供成员函数版本的 swap 。非成员版本的 swap 在泛型编程中是非常重要的。统一使用非成员版本的 swap 是一个好习惯。

9.2.6 容器大小操作

除了一个例外,每个容器类型都有三个与大小相关的操作:

  • 成员函数 size 返回容器中元素的数目;
  • 成员函数 empty 当容器元素个数为0时返回布尔值 true ,否则返回 false ;
  • 成员函数 max_size 返回一个大于或等于该类型容器所能容纳的最大元素数的值。

注意: forward_list 支持成员函数 max_sizeempty ,但不支持 size ,原因将在第9.3节解释。

9.2.7 关系运算符

标准库中的每个容器类型都支持相等运算符==!=);除了无序关联容器外的所有容器都支持关系运算符>>=<<=)。

关系运算符两侧的运算对象必须是相同类型的容器,且必须保存相同类型的元素

比较两个容器实际上是进行元素的逐对比较。这些运算符的工作方式与string的关系运算符(参见3.2.2节)类似:

  • 如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等,否则两个容器不等;
  • 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器,否则相反;
  • 如果两个容器都不是另一个容器的前缀子序列(即两个容器对应元素并不全相同),则它们的比较结果取决于第一个不相等的元素的比较结果。

下面的例子展示了这些关系运算符是如何工作的:

vector<int> v1 = { 1, 3, 5, 7, 9, 12 };
vector<int> v2 = { 1, 3, 9 };
vector<int> v3 = { 1, 3, 5, 7 };
vector<int> v4 = { l, 3, 5, 7, 9, 12 };
vl < v2	// true:	v1和v2在元素[2]处不同:v1[2]小于等于v2[2]
vl < v3	// false:	所有元素都相等,但v3中元素数目更少
vl == v4// true:	每个元素都相等,且v1和v4大小相同
v1 == v2// false: 	v2元素数目比v1少
v2 > v3 // true:	v2和v3都不是另外一个容器的前缀子序列,比较结果取决于第一个不相等的元素的比较结果即9和5

容器的关系运算符使用元素的关系运算符完成比较

Note:

只有当容器的元素类型也定义了相应的比较运算符时,才可以使用相应的关系运算符来比较两个容器。

容器的相等运算符实际上是使用元素类型定义的==运算符实现比较的,而容器的其他关系运算符使用元素类型定义的<运算符。 如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。

例如,在第7章中定义的Sales_data类型并未定义==<运算。因此,就不能比较两个保存Sales_data类型元素的容器:

vector<Sales_data> stroeA, storeB;
if (stroeA < storeB) // 错误: Sales_data 不支持 < 运算符

9.3 顺序容器特有的操作

顺序容器和关联容器的不同之处在于它们组织数据的方式不同。而这些不同之处将直接关系到元素的存储、访问、添加及删除。

9.3.1 向顺序容器添加元素

除了array类型之外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小。下表列出了向顺序容器中添加元素的操作。

向顺序容器添加元素的操作
下面这些操作会改变容器的大小,而 array 不支持这些操作
forward_list 有自己专有版本的成员函数 insert 和 emplace ,并且不支持成员函数 push_back 和 emplace_back
vector 和 string 不支持成员函数 push_front 和 emplace_front
c.push_back(t) 在 c 的尾部创建一个值为 t 或由 args 创建的元素返回 void
c.emplace_back(args)
c.push_front(t) 在 c 的头部创建一个值为 t 或由 args 创建的元素返回 void
c.emplace_front(args)
c.insert(p,t) 在迭代器 p 指向的元素之前创建一个值为 t 或由 args 创建的元素返回指向新添加的元素的迭代器
c.emplace(p,args)
c.insert(p,n,t) 在迭代器 p 指向的元素之前插入 n 个值为 t 的元素返回指向新添加的第一个元素的迭代器;若 n 为0,则返回 p
c.insert(p,b,e) 将迭代器 b 和 e 指向的范围内的元素插入到迭代器 p 指向的元素之前b 和 e 不能指向c中的元素返回指向新添加的第一个元素的迭代器;若范围为空,则返回 p
c.insert(p,il) il 是一个花括号包围的值列表,将这些给定值插入到迭代器 p 指向的元素之前返回指向新添加的第一个元素的迭代器:若列表为空,则返回 p
向一个 vector 、 string 或 deque 插入元素,会使之前所有指向该容器的迭代器、引用和指针失效。

当程序员使用这些操作时,必须记得 标准库的不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能 。在一个vectorstring的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vectorstring添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。

使用 push_back

在3.3.2节中,可以看到成员函数 push_back 将一个元素追加到一个vector的尾部。除 arrayforward_list 之外,每个顺序容器(包括 string 类型)都支持 push_back

例如,下面的循环每次读取一个数据到 word 中,然后追加到容器尾部:

// 从标准输入读取数据,将每个单词放到容器末尾
string word;
vector<string> container;
while ( cin >> word )
	container.push_back(word);

push_back 的调用在 container 尾部创建了一个新的元素,并将 container 的 size 增大了。该元素的值为 word 的一个拷贝。除了 string 外,container 的类型可以是 listvectordeque

由于 string 是一个字符容器,也可以使用 push_backstring 末尾添加字符(char),但不能添加 string 对象

void pluralize(size_t cnt, string &word)
{
	if (cnt > 1)
		word.push_back('s');	// 等价于 word += 's'
	string cr;
	cr.push_back(word);			// error:没有添加 string 到 string 的成员函数push_back
}

关键概念:容器元素是拷贝的副本

当程序员用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像将一个对象传递给非引用参数(参见3.2.2节)一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。

使用 push_front

除了 push_back 外, listforward_listdeque 容器类型还支持名为 push_front 的类似操作。 push_front 操作将元素插入到容器头部

list<int> ilist;	// 将元素添加到ilist开头
for(size_t ix=0; ix != 4; ++ix)
	ilist.push_front(ix);

此循环将元素0、1、2、3添加到 ilist 头部。每个元素都插入到 ilist 的 新的开始位置(new beginning) 。即当程序员插入1时,它会被放置在0之前,2被放置在1之前,依此类推。因此,在循环中以这种方式将元素添加到容器中,最终会形成逆序。在循环执行完毕后, ilist 保存序列3、2、1、0。

注意, dequevector 一样提供了随机访问元素的能力,但它提供了 vector 所不支持的 push_frontdeque 保证在容器首尾进行插入和删除元素的操作都只花费常数时间。与 vector 一样,在 deque 首尾之外的位置插入元素会很耗时。

虽然 vector 不支持 push_front 操作,但还是可以添加元素到容器头部,在下一小节将会解释。

在容器中的特定位置添加元素


insert成员函数它允许我们在容器中任意位置插入0个或多个元素。而 vectordequeliststring 都支持 insert 成员函数。至于 forward_list 则是被提供了特殊版本的 insert 成员函数,将在9.3.4节中介绍。

每个 insert 成员函数都接受一个迭代器作为其第一个参数。这个迭代器指出了在容器中什么位置放置新元素。它可以指向容器中任何位置,包括容器尾部之后的下一个位置。 由于迭代器可能指向容器尾部之后不存在的元素的位置,而且在容器开始位置插入元素是很有用的功能,所以 insert 函数将元素插入到迭代器所指定的位置之前。例如,下面的语句:

slist.insert(iter, "Hello!");	// 将"Hello!"添加到 iter 指向的元素之前的位置

虽然某些容器不支持 push_front 操作,但它们对于 insert 操作并无类似的限制(插入开始位置),比如 vector 。因此可以将元素插入到容器的开始位置,而不必担心容器是否支持 push_front

vector<string> svec;
list<string> slist;

// 等价于调用slist.push_front("Hello!'*);
slist.insert(slist.begin(),"Hello!");

// vector 不支持push_front,但我们可以插入到begin()之前
// 警告:插入到vector末尾之外的任何位置都可能很慢 
svec.insert(svec.begin(),"Hello!");

Note:

将元素插入到 vectordequestring 中的任何位置都是合法的。然而,这样做可能很耗时。

插入范围内元素

除了第一个迭代器参数之外,insert 函数还可以接受更多的参数,这与容器构造函数类似。其中一个版本接受一个元素数目和一个值它将指定数量的元素添加到指定位置之前,这些元素都按给定值初始化

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

这行代码将10个元素插入到 svec 的末尾,并将所有元素都初始化为 string类型,且值为 “Anna” ,因为 svec 的类型是vector

接受 一对迭代器 或 一个初始化列表 的 insert 版本将给定范围中的元素插入到第一个参数的迭代器所指定的位置之前

vector<string> v = {"quasin", "simba", "frollo", "nscarH");
//将v的最后两个元素添加到slist的开始位置
slist.insert(slist.begin(), v.end()-2, v.end());
slist.insert(slist.end(), {"these","words","will","go","at","the","end"});

// 运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end());

如果传递给 insert 一对迭代器,它们不能指向添加元素的目标容器。

在新标准下,接受元素个数或范围的 insert 版本 返回指向第一新加入元素的迭代器在旧版本的标准库中,这些操作 返回 void )。如果范围为空(第二参数和第三参数迭代器)则不插入任何元素,insert 操作会将第一个参数返回

使用 insert 的返回值

无论是哪个版本的成员函数 insert ,都会将值插入到所给迭代器指定位置的前面。

而通过 使用成员函数 insert 的返回值 ,可以 在容器中一个特定位置反复插入元素

list<string> lst; 
auto iter = lst.begin();
while (cin » word) 
	iter = lst.insert(iter, word); // 等价于调用 push_front

在上述代码中,在循环之前,将 iter 用 lst.begin() 初始化,配合使用了 auto 作为类型说明符。在上述代码中,第一次调用成员函数 insert 会将 word 存储的刚刚读入的 string 值插入到 iter 所指向的元素之前的位置。而成员函数 insert 返回的迭代器恰好指向这个新元素。然后将此迭代器赋予 iter 并重复循环,读取下一个单词。只要继续有单词读入,每步while循环就会将一个新元素插入到 iter 之前,并将 iter 改变为新加入元素的位置。此元素为(新的)首元素。因此,每步循环将一个新元素插入到 lst 的首元素之前的位置。

使用 emplace 操作

新标准引入了三个新成员 emplace_frontemplaceemplace_back ,这些 emplace 操作中的成员函数是直接通过参数构造元素而不是拷贝或移动元素。这些操作分别对应 push_frontinsertpush_back ,允许将元素放置在容器头部、一个指定位置之前和容器尾部。

当程序员执行一个 emplace 操作时,则是将参数传递给容器的元素类型的普通构造函数。emplace 操作的成员函数使用这些参数在容器管理的内存空间中直接构造元素。之前的( push_frontinsertpush_back )是调用移动构造函数或拷贝构造函数,会浪费内存和时间。

Note:

push_frontinsertpush_back 调用移动构造函数时,相比 emplace 操作,一定会浪费时间,可能会浪费内存(如果给移动构造函数的右值的类型有非指针成员就会浪费内存,因为还是会拷贝这些成员值到新内存并释放之前存储这些成员值的旧内存)。

例如,假定c保存Sales_data(在7.1.4节创建的案例class类型)元素:

// 在 c 的末尾构造一个Sales_data对象
c.emplace_back("978-0590353403",25,15.99);  		// 使用三个参数的Sales_data构造函数
c.push_back("978-0590353403",25,15.99);				// 错误:没有接受三个参数的push_back版本
c.push_back(Sales_data("978-0590353403",25,15.99));	// 正确:创建一个临时的Sales_data对象传递给push_back

在上述代码中,其中对 emplace_back 的调用和第二个 push_back 调用都会创建新的Sales_data对象。在调用成员函数 emplace_back 时,会在容器管理的内存空间中直接创建对象。而调用 push_back 则会创建一个局部临时对象,并调用一个转移构造函数创建另一个对象,并将其压入容器中,最后释放这个局部临时对象。

emplace 操作中的成员函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配:

// iter 指向 c 中一个元素,其中保存了 Salesdata 元素
c.emplace_back();	// 调用了 Sales_data 的默认构造函数
c.emplace(iter,"999-999999999");	// 使用 Sales_data(string)
// 使用 Sales_data 的接受一个 ISBN 、一个 count和一个 price 的构造函数 
c.emplace_front("978-0590353403",25,15.99);

9.3.2 访问元素

下面的表格列出了可以用来在顺序容器中访问元素的操作。如果容器中没有元素,则访问顺序容器元素操作的结果是未定义的。包括 array 在内的每个顺序容器都有一个 front 成员函数,而除 forward_list 之外的所有顺序容器都有一个 back 成员函数。这两个操作分别返回首元素和尾元素的引用:

// 在解引用一个迭代器或调用 front 或 back 之前检查是否有元素
if(!c.empty()){
	auto val=*c.begin(), val2=c.front();// val和val_2是c中第一个元素值的拷贝
	auto last=c.end();					// val3和va_4是c中最后一个元素值的拷贝
	auto val3=*(--last);				// 不能递减 forward_list 迭代器
	auto val4=c.back();					// forward_list 不支持
}	// front和back两个操作分别返回首元素和尾元素的引用。

上面代码用两种不同方式来获取c中的首元素和尾元素的引用。获取容器中的首元素和尾元素的引用,直接的方法调用顺序容器的成员函数 frontback 。而间接的方法通过解引用成员函数 begin 返回的迭代器来获得首元素的引用,以及通过递减成员函数 end 返回的迭代器,然后对它解引用来获得尾元素的引用

这个程序有两点值得注意:

① 成员函数 end 返回的迭代器指向的是容器尾元素之后的(实际不存在的)元素。所以,为了获取尾元素,必须首先递减此迭代器再对其解引用;
② 另一个重要之处是,在调用 frontback (或解引用 beginend 返回的迭代器)之前,要确保容器非空(如果容器为空,访问顺序容器元素的操作将是未定义的)。

在顺序容器中访问元素的操作
at 和下标操作只适用于 string 、vector 、deque 和 array
back 不适用于 forward_list
c.back() 返回 c 中尾元素的引用。若 c 为空,则函数行为未定义
c.front() 返回 c 中首元素的引用。若 c 为空,则函数行为未定义
c[n] 返回 c 中下标为 n 的元素的引用。 n 是一个无符号整数。若 n>=c.size() ,则函数行为未定义
c.at(n) 返回 c 中下标为 n 的元素的引用。如果下标越界,则抛出一个 out_of_range 异常
对一个空容器调用成员函数 front 和 back ,就像使用一个越界的下标一样,是一个十分严重的错误。

访问顺序容器成员的函数返回的是引用

在顺序容器中访问元素的成员函数(即 frontback 、下标和 at )返回的都是对元素的引用。如果容器是一个 const 对象,则返回值是 const 引用。如果容器不是 const 的,则返回值是普通引用,可以用来改变元素的值:

if(!c.empty()){
	c.front()=42;    // 将42赋予c中的第一个元素
	auto &v=c.back();// 获得指向c最后一个元素的引用
	v=1024;      	 // 改变c中的元素
	auto v2=c.back();// v2不是一个引用,它是 c.back() 的一个拷贝
	v2=0;			 // 未改变c中的元素
}                    

与往常一样,如果使用 auto 变量来保存这些函数的返回值,并且希望使用此变量来改变容器中对应元素的值,必须记得将变量定义为引用类型。 否则得到的只是一个拷贝。

下标操作和安全的随机访问

提供快速随机访问的容器( stringvectordequearray )也都提供下标运算符(参见3.3.3节)。就像现在看到的那样,下标运算符接受一个下标参数,返问容器中该位置的元素的引用且给定下标必须“在范围内”(即,参数值大于等于0,且小于容器的大小)。

注意:

再次强调,保证下标有效是程序员的责任,下标运算符并不检查下标是否在合法范围内。使用越界的下标是一种严重的程序设计错误,而且编译器并不检查这种错误。

如果程序员希望编译器来确保下标是合法的,可以使用 at 成员函数。 at 成员函数类似下标运算符,但如果下标越界, at 会抛出一个 out_of_range 异常(参见5.6节):

vector<string> svec;// 空 vector 
cout << svec[0];	// 运行时错误:svec中没有元素!
cout << svec.at(0);	// 抛出一个out_of_range 异常

9.3.3 删除元素


与添加元素的多种方式类似,顺序容器(除了 array )也有多种删除元素的方式。下面表格列出了这些成员函数:

顺序容器的删除操作
删除元素的操作会改变容器的大小,所以不适用于 array
forward_list 有特殊版本的成员函数 erase
forward_list 不支持 pop_back vector 和 string 不支持 pop_front
c.pop_back() 删除 c 中尾元素,若 c 为空,则函数行为未定义,函数返回 void
c.pop_front() 删除 c 中首元素,若 c 为空,则函数行为未定义,函数返回 void
c.erase(p) 删除迭代器 p 所指的元素,返回一个指向被删除元素之后元素的迭代器,若 p 指向尾元素,则返回尾后迭代器。若 p 是尾后(off-the-end)迭代器,则函数的行为未定义
c.erase(b,e) 删除迭代器 b 和 e 所指定范围内的元素,返回一个指向最后一个被删除元素之后元素的迭代器,若 e 本身就是尾后迭代器,则函数也返回尾后迭代器
c.clear() 删除 c 中的全部元素,返回 void
删除 deque 中除首尾位置外的任何元素,都会使所有之前指向该容器内的迭代器、引用和指针失效(失效相当于它们之间不再有关联了,一个失效的指针、引用或迭代器将不再表示任何元素。,比如无法用之前引用、指针或迭代器改变容器内元素的值)。
之前定义的指向 vector 或 string 中删除点之后位置的迭代器、引用和指针都会失效。

Note:

表格内删除元素的成员函数并不检查其参数执行的元素是否存在。所以在删除元素之前,程序员必须确保它(们)是存在的。

pop_front 和 pop_back 成员函数

pop_frontpop_back 成员函数分别删除首元素和尾元素。与 vectorstring 不支持 push_front 一样,这些类型也不支持 pop_front 。类似的, forward_list 不支持 pop_back 。与访问元素的成员函数类似,不能对一个空容器执行弹出操作。

这两个成员函数都返回 void ,如果需要弹出的元素的值,就必须在执行弹出操作之前保存它:

while(!ilist.empty()){
	process(ilist.front()); // 对 ilist 的首元素进行一些处理
	ilist.pop_front();  	// 完成处理后删除首元素
}

从容器内部删除一个元素

成员函数 erase 从容器中指定位置删除元素。它可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。 两种形式的 erase 都返回指向删除的(最后一个)元素之后位置的迭代器。即,若j是i之后的元素,那么 erase(i) 将返回指向j的迭代器,和 insert 相反,假设在i之前添加j, insert 添加返回的是指向j的迭代器。

例如,下面的循环删除一个 list 中的所有奇数元素:

list <int> lst = {0,1,2,3,4,5,6,7,8,9};
auto it : lst.begin()
while(it != lst.end()){
    if(*it % 2)				// 若干元素为奇数
        it = lst.erase(it);	// 删除迭代器 it 指向的元素
    else
        ++it;
}

每个循环步中,首先检查当前元素是否是奇数。如果是,就删除该元素,并将it设置为我们所删除的元素之后的元素。如果*it为偶数,则将it递增,从而在下一步循环检查下一个元素。

删除多个元素

接受一对迭代器的 erase 版本允许删除其指定的一个范围内的元素:

// 删除两个迭代器表示的范围内的元素
// 返回指向最后一个被删元素之后位置的送代器
elem1 = slist.erase(elem1, elem2);	// 调用后,elem1 == elem2

迭代器elem1指向要删除的第一个元素,elem2指向我们要删除的最后一个元素之后的位置。
为了删除一个容器中的所有元素程序员既可以直接调用成员函数 clear ,也可以用成员函数 beginend 获得的迭代器作为参数调用成员函数 erase

slist.clear();	// 删除容器中所有元素
slist.erase(slist.begin(), slist.end());	// 等价调用

9.3.4 特殊的 forwar_list 操作

为了理解 forward_list 为什么有特殊版本的添加和删除操作,可以考虑当从一个单向链表中删除一个元素时会发生什么。如下图所示,删除一个元素会改变序列中的链接。在此情况下,删除elem3也会改变elem2,elem2原来指向elem3。但删除elem3后,elem2指向了elem4。

《C++Primer 第五版》——第九章 顺序容器_第1张图片

当添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生改变。为了添加或删除一个元素,需要访问这个元素的前驱,以便改变前驱的链接。但是, 由于forward_list 是单向链表,在一个单向链表中,没有简单的方法来获取一个元素的前驱。 出于这个原因,在一个 forward_list 中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。这样,程序员总是可以访问到被添加或删除操作所影响的元素。

由于这些操作与其他容器上的操作的实现方式不同, forward_list 并未定义 insertemplaceerase ,而是定义了名为 insert_afteremplace_aftererase_after 的操作。例如,在上图的例子中,为了删除elem3,应该用指向elem2的迭代器作为参数调用 erase_after

为了支持这些操作,顺序容器 forward_list 也定义了成员函数 before_begin ,它返回一个 首前(off-the-beginning) 迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素“之后”添加或删除元素。

在 forward_list 中插入或删除元素的操作
lst.before_begin() 返回指向链表首元素之前并不存在的元素的迭代器(即首前迭代器)首前迭代器不能解引用。 cbefore_begin() 返回一个 const_iterator
lst.cbefore_begin()
lst.insert_after(p,t) 在迭代器 p 之后的位置插入元素。 t 是一个对象, n 是数量, b 和 e 是表示范围的一对迭代器( b 和 e 不能指向 lst 内), il 是一个花括号列表。
返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回 p 。
若 p 为尾后迭代器,则函数行为未定义
insert_after 的重载不会导致指针、迭代器和引用失效
lst.insert_after(p,n,t)
lst.insert_after(p,b,e)
lst.insert_after(p,il)
emplace_after(p,args) 使用 args ( args 是一个系列参数,用于新元素的初始化)在 p 指定的位置之后创建一个元素,返回一个指向这个新元素的迭代器。若 p 为尾后迭代器,则函数的行为未定义
lst.erase_after(p) 删除 p 指向的位置之后的元素,或删除从 b 之后直到 e (但不包含 e )之间的元素。返回一个指向被删除元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代器,如果 p 指向 lst 的尾元素或者是一个尾后迭代器,则函数行为未定义
erase_after 的重载仅使之前定义的指向已删除元素的指针、迭代器或引用无效
lst.erase_after(b,e)
lst.pop_front() 删除首元素,若容器中无元素,则函数行为未定义。返回值为 void 。指向被删除元素的迭代器、引用或指针会失效。
lst.push_front(value) 添加 value 为新的首元素。返回值为 void 。之前定义的指针、引用和迭代器不会失效。

当在 forward_list 中添加或删除元素时,必须关注两个迭代器——一个指向要处理的元素,另一个指向其前驱。 例如,可以改写以前一个从 list 中删除奇数元素的循环程序,将其改为从 forward_list 中删除元素:

forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9};
auto prev = flst.before_begin();// 表示 flst 的首前元素
auto curr = flst.begin();		// 表示 flst 的第一个元素
while(curr != flst.end()){
    if(*flst % 2)
        curr = flst.erase_after(prev);
    else
        {
            prev = curr; //移动迭代器 curr ,指向下一个元素, prev 指向它之前的元素
            ++curr;
        }
}

此例中,curr表示要处理的元素,prev表示curr的前驱。调用begin来初始化curr,这样第一步循环就会检查第一个元素是否是奇数。这里用 before_begin 来初始化prev,它返回指向curr之前不存在的元素的迭代器。

当找到奇数元素后,将prev传递给 erase_after ,此调用将prev之后的元素删除,即删除curr指向的元素。然后将curr重置为 erase_after 的返回值,使得curr指向序列中下一个元素,prev保持不变,仍指向(新)curr之前的元素。如果curr指向的元素不是奇数,在 else 语句中将两个迭代器都向前移动。

9.3.5 改变容器大小

在 forward_list 中插入或删除元素的操作
resize 不适用于 array
c.resize(n) 调整 c 的大小为n个元素。若 n < c.size() ,则多出的元素被丢弃。若必须添加新元素,则对新元素进行值初始化
c.resize(n,t) 调整 c 的大小为n个元素。任何新添加的元素都初始化为值 t


如上面表格所描述,程序员可以用成员函数 resize 来增大或缩小容器,与往常一样,具有固定大小的容器 array 不支持成员函数 resize调用成员函数 resize 时,如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部:

list<int>ilist(10,42);	// 10个int:每个的值都是42
ilist.resize(15);		// 将5个值为0的元素添加到ilist的末尾
ilist.resize(25,-1);	// 将10个值为-1的元素添加到ilist的末尾
ilist.resize(5);		// 从ilist末尾删除20个元素

resize 操作接受一个可选的元素值参数,用来初始化添加到容器中的元素。如果调用者未提供此参数,新元素进行值初始化。如果容器保存的是类类型元素,且 resize 向容器添加新元素时,就必须提供初始值,或者元素的类型必须提供一个默认构造函数。

9.3.6 某些容器操作可能使迭代器、引用、指针失效

向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。 一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题。

在向容器添加元素后:

  • 如果容器是 vectorstring ,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效 (空间重新分配,比如当前空间太小了,增大空间,但是空间不足,需要将整个容器移到别的地方重新开辟,指向先前的地址的迭代器、指针和引用就会失效)。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效(和前面一样,如果增大的空间不是很大,则在原有的空间后面追加开辟空间,先前的仍然保留)。
  • 对于 deque ,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
  • 对于 listforward_list ,无论插入到什么位置,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。

当程序员从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效。 毕竟,这些元素都已经被销毁了。当从容器中删除一个元素后:

  • 对于 listforward_list ,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效。
  • 对于 deque ,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。 如果是删除 deque 的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。
  • 对于 vectorstring ,指向被删元素之前元素的迭代器、引用和指针仍有效。 注意:当程序员删除容器的元素时,尾后迭代器总是会失效

Note:

因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对 vectorstringdeque 尤为重要。

编写改变容器的循环程序

添加或删除 vectorstringdeque 元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。这种程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是 inserterase ,那么更新迭代器很容易,因为它们都返回新的迭代器,所以可以用来更新旧的指针、引用或迭代器:

//傻瓜循环 删除偶数元素 复制每个奇数元素
vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
auto iter = vi.begin();	// 使用begin,而不是cbegin,因为需要改变元素
while(iter != vi.end()){
    if(*iter % 2){
        iter = vi.insert(iter,*iter);	// 复制当前元素
        iter += 2;		// 跳过当前元素 以及插入到他之前的元素
    } else
        iter = vi.erase(iter);			// 删除偶数元素
        // 不需要移动迭代器 iter删除元素之后 指向删除元素之后的元素
}

此程序删除 vector 中的偶数值元素,并复制每个奇数值元素。程序在调用 inserterase 后都更新迭代器,因为两者都会使迭代器失效。

在调用 erase 后,不必递增迭代器,因为 erase 返回的迭代器已经指向序列中下一 个元素。调用 insert 后,需要递增迭代器两次。记住, insert 在给定位置之前插入新元素,然后返回指向新插入元素的迭代器。因此,在调用 insert 后,iter指向新插入元素,位于正在处理的元素之前。将迭代器递增两次,恰好越过了新添加的元素和正在处理的元素,指向下一个未处理的元素。

不要保存成员函数 end 返回的迭代器

当程序员添加或删除 vectorstring 的元素后,或在 deque 中首元素之外任何位置添加或删除元素后,原来 end 返回的迭代器总是会失效。 因此,添加或删除元素的循环程序必须反复调用成员函数 end ,而不能在循环之前保存成员函数 end 返回的迭代器,一直当作容器末尾使用。 通常C++标准库的实现中 end 操作都很快,部分就是因为这个原因。

例如,存在这样一个循环,它处理容器中的每个元素,在其后添加一个新元素。它希望循环能跳过新添加的元素,只处理原有元素。在每步循环之后将定位迭代器,使其指向下一个原有元素。如果试图像这样修改这个循环——在循环之前保存 end() 返回的迭代器,一直用作容器末尾,就会导致一场灾难,如下:

//灾难 此循环的行为是未定义的
auto begin = v.begin();
auto end = v.end();	// 保存尾迭代器的数值是一个坏主意!!!!!!
while(begin != end){
    //做一些处理
    //传入新值,对 begin 进行重新赋值 否则它会失效
    ++begin;	// 向前移动 begin 因为我们想在这个元素之后插入元素
    begin = v.insert(begin,42);
    ++begin;	// 向前移动,跳过新加入的元素
}

此代码的行为是未定义的。在很多标准库实现上,此代码会导致无限循环。问题在于将成员函数 end 返回的迭代器保存在一个名为end的局部变量中。在此循环体中,程序向容器中添加了一个元素,这个操作使保存在变量end中的迭代器失效了。这个迭代器不再指向v中任何元素,或是v 中尾元素之后的位置。所以循环的条件实际上会出问题。

Note:

如果在一个循环中插入或删除 dequestringvector 中的元素,不要保存 end 返回的迭代器。

必须在每次存储空间被重新分配后,获取需要更新的迭代器、引用或指针,而不能在有关操作开始前保存之前获取的迭代器、引用或指针并使用。

如果在一个循环中插入或删除 dequestringvector 中的元素,不要保存 end 返回的迭代器。必须在每次插入操作后重新调用 end ,而不能在循环开始前保存它返回的迭代器:

//更安全的做法是 每个循环添加/删除元素之后 都重新计算尾后迭代器
while(begin != v.end){
    ++begin();	// 向前移动 begin 因为想在此元素之后插入元素
    begin = v.insert(begin,42);//插入新值
    ++begin;	// 向前移动 begin 跳过新加入的元素
}

9.4 vector 对象是如何增长的

为了支持快速随机访问, vector 将元素连续存储在内存空间中,每个元素紧挨着前一个元素存储。 通常情况下不必关心一个标准库类型是如何实现的,而只需关心它如何使用。然而,对于 vectorstring ,其部分实现渗透到了接口中。

假定容器中元素是连续存储的,且容器的大小是可变的,考虑向 vectorstring 中添加元素会发生什么: 如果没有空间容纳新元素,容器不可能简单地将它添加到内存中其他位置——因为元素必须连续存储。容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放旧存储空间。

问题:如果每添加一个新元素, vector 就执行一次这样的内存分配和释放操作,性能会慢到不可接受。
解决方法:

为了避免这种代价,标准库实现者采用了可以减少容器空间重新分配次数的策略:当不得不获取新的内存空间时vectorstring 的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可用来保存更多的新元素(提前分配更大的空间)。 这样,就不需要每次添加新元素都重新分配容器的内存空间了。

这种分配策略比每次添加新元素时都重新分配容器内存空间的策略要高效得多。其实际性能也表现得足够好——虽然 vector 在每次重新分配内存空间时都要移动所有元素,但使用此策略后,其扩张操作通常比 listdeque 还要快。

管理容量的成员函数

如下表所示, vectorstring 类型提供了一些成员函数,允许程序员与它的实现中的内存分配部分互动。成员函数 capacity 的返回值告诉程序员容器在不扩张内存空间的情况下可以容纳多少个元素。 reserve 操作允许程序员通知容器它应该准备保存多少个元素。

容器大小管理操作
成员函数 shrink_to_fit 只适用于 vector 、 string 和 deque
成员函数 capacity 和 reserve 只适用于 vector 和 string
c.shrink_to_fit() 将 capacity 减少为 size 相同大小
c.capacity() 不重新分配内存空间的话,容器 c 可以保存多少元素
c.reserve(n) 分配至少能容纳 n 个元素的内存空间(即改变容器的 capacity )

Note:

reserve 并不改变容器中元素的数量,它仅影响 vector 预先分配多大的内存空间

成员函数 reserve 参数的不同情况:

只有当需要的内存空间超过当前容量时调用成员函数 reserve 才会改变 vector 的容量
如果需求大小大于当前容量 reserve 至少分配与需求一样大的内存空间(可能更大)
如果需求大小小于或等于当前容量 reserve 什么也不做特别是,当需求大小小于当前容量时容器不会退回内存空间
因此,在调用 reserve 之后,容器的 capacity 将会大于或等于传递给 reserve 的参数。

这样,调用 reserve 永远也不会减少容器占用的内存空间。类似的, resize 成员函数(参见9.3.5节)只改变容器中元素的数目,而不是容器的容量。程序员同样不能使用 resize 来减少容器预留的内存空间。

在新标准库中,程序员可以通过调用成员函数 shrink_to_fit 来要求 dequevectorstring 退回不需要的内存空间。 此函数指出不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求。也就是说,调用 shrink_to_fit 也并不保证一定退回内存空间。

容器的 capacity 和 size

理解 capacity 和 size 的区别非常重要。 容器的 size 是指它已经保存的元素的数目;而 capacity 则是在不分配新的内存空间的前提下它最多可以保存多少元素。

下面的代码展示了size和 capacity 之间的相互作用:

vector<int> ivec;
// size 应该为0; capacity 应该依赖于具体实现
cout << "ivec:size: " << ivec.size()
     << "capacity: "  << ivec.capacity() << endl;
// 向 ivec 添加24个元素
for(vector<int>::size_type ix = 0; ix != 24; ix++)
    ivec.push_back(ix);
// size 应该为24; capacity 应该大于等于24,  capacity 的具体值依赖于标准库的实现
cout << "ivec:size: " << ivec.size()
     << "capacity: "  << ivec.capacity() << endl;

当在某个特定的系统上运行时,这段程序得到如下输出:

ivec:size:0 	capacity:0
ivec:size:24 	capacity:32

一个空 vector 的 size 为0,显然在这个特定系统中的标准库实现中一个空 vector 的 capacity 也为0。当向 vector 中添加元素时,容器的 size 与添加的元素数目相等。而 capacity 至少与 size 一样大,具体会分配多少额外空间则视标准库具体实现而定 在上述这个特定系统中的标准库实现中,每次添加1个元素,共添加24个元素,会使 capacity 变为32。

可以想象ivec的当前状态如下图所示:

《C++Primer 第五版》——第九章 顺序容器_第2张图片

现在可以预先分配一些额外的空间:

ivec.reserve(50);	// 将 capacity 的容量设置至少为50,可能会更大
// size 的大小应该为24, capacity 的大小应该大于等于50,具体值依赖于标准库的实现
cout << "ivec:size: " 	<< ivec.size()
     << " capacity: "	<< ivec.capacity() << endl;

程序的输出表明 reserve 严格按照我们需求的大小分配了新的空间:

ivec:size:24 	capacity:50

接下来可以用光这些预留空间

// 添加元素用光预留空间 
while(ivec.size() != ivec.capacity())
	ivec.push_back(0);
// capacity 应该未改变, size 和 capacity 不相等
cout << "ivec:size: " 	<< ivec.size()
     << " capacity: "	<< ivec.capacity() << endl;

程序的输出表明用光了预存空间,此时容器的 size 和 capacity 相等。

ivec:size:50 	capacity:50

由于只使用了预留空间,因此没必要为 vector 容器 ivec 分配新空间。实际上,只要使用的空间没有超过 vector 的容量, vector 就不能重新分配内存空间。

可以调用成员函数 shrink_to_fit 来要求 vector 超出当前大小的多余内存空间退回给系统:

ivec.shrink_to_fit(); //要求归还内存

再次强调:调用成员函数 shrink_to_fit 只是一个请求,是否归还并无保证,这取决于标准库的实现

Note:

每个 vector 实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。

只有在执行 insert 操作时 size 与 capacity 相等,或者调用 resizereserve 时给定的大小超过当前 capacity , vector 才可能重新分配内存空间。会分配多少超过给定容量的额外空间,取决于标准库的具体实现

虽然不同的实现可以采用不同的分配策略,但所有实现都应遵循一个原则:确保用 push_backvector 添加元素的操作有高效率。 从技术角度说,就是通过在一个初始为空的 vector 上调用n次 push_back 来创建一个n个元素的 vector ,所花费的时间不能超过n的常数倍。


9.5 额外的 string 操作

除了顺序容器共同的操作之外, string 类型还提供了一些额外的操作。这些操作中的大部分要么是提供 string 类和C风格字符数组之间的相互转换,要么是增加了允许程序员用下标代替迭代器的版本。

标准库 string 类型定义了大量函数。幸运的是,这些函数使用了重复的模式。当了解 string 支持哪些类型的操作后,就可以在需要使用一个特定操作时回过头来仔细阅读。

9.5.1 构造 string 的其他方法

除了在3.2.1节中已经介绍过的构造函数,以及与其他顺序容器相同的构造函数(参见之前的表)外, string 类型还支持另外三个构造函数,如下表所示。

构造 string 的其他方法
n 、 len2 和 pos2 都是无符号值
string s(cp,n) s 是 cp 指向的数组中的第 n 个字符的拷贝,此数组至少应该包含 n 个字符
string s(s2,pos2) s 是 string s2 从下标 pos2 开始的字符的拷贝。若 pos2>s2.size() ,构造函数的行为未定义
string s(s2,pos2,len2) s 是 string s2 从下标 pos2 开始 len2 个字符的拷贝。若 pos2>s2.size() ,构造函数的行为未定义不管 len2 的值是多少,构造函数至多拷贝 s2.size()-pos2 个字符

这些构造函数的第一个参数接受一个 string 或一个 const char* 参数,还接受(可选的)指定拷贝多少个字符的参数。当传递给它们的是一个 string 时,还可以给定一个下标来指出从哪里开始拷贝:

const char *cp = "Hello World!!!"; // 以空字符串结束的数组
char noNull[] = {'H','i'}; // 不是以空的字符串结束
string s1(cp); 		// 拷贝 cp 中的字符直到遇到空的字符串 ,因此 s1 == "Hello World!!!"
string s2(noNull,2);// 拷贝两个字符串 s2 == "Hi"
string s3(noNull);	// 未定义行为,因为字符数组 noNull 不以空字符结束
string s4(cp+6,5);	// 从cp[6]开始拷贝5个字符 s4 == "Word" 
string s5(sl,6,5);	// 从s1[6]开始拷贝5个字符 s5 == "Word" 
string s6(s1,6);	// 从s1[6]开始拷贝直至末尾 s6 == "Word" 
string s7(s1,6,20);	// 从s1[6]开始拷贝直至末尾 s7 == "Word" 
string s8(s1,16);	// 抛出一个 out_of_range 异常

注意:

①通常当程序员只传递一个 const char*char* 类型的参数来创建 string 时,该指针指向的数组必须以空字符结尾,而拷贝构造的操作遇到空字符时停止;
②除了一个指向 char 数组的指针参数,如果还传递给构造函数一个小于数组大小的计数值,该数组就不必以空字符结尾;
③如果未传递计数值且该 char 数组数组也未以空字符结尾(即不是C风格字符串),或者给定计数值大于数组大小则构造函数的行为是未定义的

当从一个 string 拷贝字符时,程序员可以提供一个可选的开始位置和一个计数值。

开始位置必须小于或等于给定的 string 的大小。如果开始位置大于容器的 size ,则构造函数抛出一 个 out_of_range 异常。
② 如果还传递了一个计数值,则从开始位置开始拷贝这么多个字符。不管要求拷贝多少个字符,标准库最多拷贝到 string 结尾,不会更多。

substr操作

substr 成员函数(参见下表) 返回一个 string ,它会把原始 string 的一部分或全部给拷贝。可以传递给 substr 一个可选的开始位置和计数值:

string s("hello world");  
string s2 = s.substr(0,5);	// s2=hello
string s3 = s.substr(6); 	// s3=world
string s4 = s.substr(6,11);	// s4=world
string s5 = s.substr(12);	// 抛出一个out_of_range异常

如果开始位置超过了 string 的大小,则成员函数 substr 抛出一个 out_of_range 异常。
如果开始位置加上计数值大于 string 的大小,则 substr 会调整计数值,只拷贝到 string 的末尾。

子字符串操作
s.substr(pos,n) 返回一个 string ,包含 s 中从 pos 开始的 n 个字符的拷贝。 pos 的默认值为0。 n 的默认值的 s.size() - pos ,即默认拷贝从 pos 开始的所有字符

9.5.2 改变 string 的其他方法

string 类型支持顺序容器的赋值运算符 = 以及 assigninserterase 操作。 除此之外,它还定义了额外的 inserterase 版本。除了接受迭代器的 inserterase 版本外, string 还提供了接受下标的版本。下标指出了开始删除的位置,或是 insert 到给定值之前的位置

s.insert(s.size(),5,'!');	// 在 s 末尾插入5个感叹号
s.erase(s.size()-5,5);		// 从 s 删除最后5个字符

标准库 string 类型还提供了接受C风格字符数组的成员函数 insertassign 版本。例如,可以将以空字符结尾的字符数组 insert 到或 assign 给一个 string

const char* cp = "Stately, plump Buck";
s.assign(cp,7);			// s=="Stately"
s.insert(s.size(),cp+7);// s=="Stately, plump Buck"

在这段代码中首先通过调用 assign 替换 s 的内容。在这里赋予 s 的是从 cp 指向的地址开始的7个字符。 assign 要求赋值的字符数必须小于或等于 cp 指向的数组中的字符数(不包括结尾的空字符)。接下来在 s 上调用 insert ,这步的意图是将 cp 开始的7个字符(至多到结尾空字符之前)拷贝到 s 中,插入到 s[size()] 处(不存在的)元素之前的位置。

程序员也可以指定将来自其他 string 或子字符串的字符插入到当前 string 中或赋予当前 string

string s="some string", s2= "some other string";
s.insert(0,s2);	// 在 s[0] 之前插入 s2 的拷贝
// 在 s[0] 之前插入 s2 中从 s2[0] 开始的 s2.size() 个字符
s.insert(0, s2, 0, s2.size());

成员函数 append 和 replace

string 类定义了两个额外的成员函数: appendreplace ,这两个函数可以改变 string 的内容。 下表描述了这两个函数的功能:

修改 string 的操作
s.insert(pos,args) 在 pos 之前插入 args 指定的字符, pos 可以是一个下标或者一个迭代器。接受下标的版本返回一个指向 s 的引用;接受迭代器的版本返回指向第一个插入字符的迭代器
s.erase(pos,len) 删除从位置 pos 开始的 len 个字符。如果 len 被省略,则删除从 pos 开始直至 s 末尾的所有字符。返回一个指向 s 的引用
s.assign(args) 将 s 中的字符替换为 args 指定的字符。返回一个指向 s 的引用
s.append(args) 将 args 追加到 s 。返回以指向 s 的引用
s.replace(range,args) 删除 s 中范围 range 内的字符,替换为 args 指定的字符。 range 或者是一个下标和一个长度,或者是一对指向 s 的迭代器,返回一个指向 s 的引用
args 可以是下列形式之一; append 和 assign 可以使用所有形式:
str 不能与 s 相同,迭代器 b 和 e 不能指向 s
str 字符串 str
str,pos,len str 中从 pos 开始最多 len 个字符
cp,len 从cp指向的字符数组的前(最多)len个字符
cp cp 指向的以空字符串结尾的字符数组
n,c n 个字符 c
b,e 迭代器 b 和 e 指定的范围内的字符
初始化列表 花括号 {} 包围,以逗号分隔的字符列表
replace 和 insert 所允许的 args 形式依赖于 rangepos 是如何指定的
replace(pos,len,args) replace(b,e,args) insert(pos,args) insert(iter,args) args 可以是
str
str,pos,len
cp,len
cp
n,c
b2,e2
初始化列表

其中, append 成员函数是在 string 末尾进行插入操作的一种简写形式:

string s("C++ Primer"), s2 = s;	// 将s和s2初始化为"C++ Primer"
s.insert(s.size(), "4th Ed.");	// s=="C++ Primer 4th Ed."
s2.append("4th Ed.");			// 等价方法:将"4thEd."追加到s2;  s==s2

replace 操作是同时调用 eraseinsert 的一种简写形式:

// 将“4th”替换为"5th"的等价方法
s.erase(11, 3);				// 删除 "4th" , s=="C++ Primer Ed."
s.insert(11, "5th");		// s=="C++ Primer 5th Ed.”
// 从位置11开始,删除3个字符并插入"5th"
s2.replace(11, 3, "5th");	// 等价方法:s==s2

此例调用 replace 时,插入的文本恰好与删除的文本一样长。但这不是必须的,调用 replace 时可以插入一个更长或更短的 string

s.replace(11, 3, 'Fifth');	// s=="C++Primer Fifth Ed."  

在此调用中,删除了3个字符,但在其位置插入了5个新字符。

改变 string 的多种重载函数

上表列出的成员函数 appendassigninsertreplace 有多个重载版本。根据如何指定要添加的字符和 string 中被替换的部分,这些函数有不同的版本。幸运的是,这些成员函数有共同的接口

appendappend 函数无须指定要替换 string 中哪个部分: append 总是替换 string 中的所有内容, append 总是将新字符追加到 string 末尾。

replace 成员函数提供了两种指定替换的元素范围的方式:可以通过一个位置和一个长度来指定范围,也可以通过一个迭代器范围来指定。 insert 成员函数允许用两种方式指定插入点:用一个下标或一个迭代器,在两种情况下,新元素都会插入到给定下标(或迭代器)之前的位置。

可以用好几种方式(即上表中的 args)来指定要添加到 string 中的字符。新字符可以来自于另一个 string ,来自于一个字符指针(指向的字符数组),来自于一个花括号包围的字符列表,或者是一个字符和一个计数值。当字符来自于一个 string 或一个字符指针时,我们可以传递一个额外的参数来控制是拷贝部分还是全部字符。

并不是每个函数都支持所有形式的参数 args (即上表中的 args)。例如, insert 就不支持下标和初始化列表参数。类似的,如果希望用迭代器指定插入点,就不能用字符指针指定新字符的来源。

9.5.3 string 搜索操作

string 类提供了6个不同的搜索函数,每个函数都有4个重载版本。下表中描述了这些搜索成员函数及其参数。每个搜索操作都返回一个 string::size_type 值,表示匹配发生位置的下标。如果搜索失败,则返回一个名为 string::nposstatic 数据成员。 标准库将 string::npos 定义为一个 const string::size_type 类型,并初始化为值-1。由于 string::npos 是一个 const unsigned 类型,此初始值意味着 string::npos 等于任何 string 最大的可能大小 (比如 unsigned char c = -1; ,假设 char 占8bit,则 c 的值为255)。

成员函数 find 完成最简单的搜索。它查找参数指定的字符串,若找到,则返回第一个匹配位置的下标,否则返回 string::npos

string name("AnnaBelle");
auto posl = name.find("Annan");	// posl==0

这段程序返回0,即子字符串"Anna"在"AnnaBelle”中第一次出现的下标。

搜索(以及其他 string 操作)是大小写敏感的。当在 string 中查找子字符串时,要注意大小写:

string lowercase("annabelle");
posl = lowercase.find("Anna");	// posl==npos

这段代码会将 posl 置为 string::npos ,因为"Anna"与"anna"不匹配。

一个更复杂一些的问题是 查找与给定字符串中任何一个字符匹配的位置,可以使用成员函数 find_first_of 。例如,下面代码定位 name 中的第一个数字:

string numbers("0123456789"), name("r2d2"); 
auto pos = name.find_first_of(numbers);	// 返回1,即,name中第一个数字的下标

如果是要 搜索第一个不在参数中的字符,应该调用 find_first_not_of 。例如,为了搜索一个 string 中第一个非数字字符,可以这样做:

string dept("03714p3");
auto pos = dept.find_first_not_of(numbers);	// 返回5——即第一个非数字字符'p'的下标
string 搜索操作
搜索操作返回指定字符出现的下标,如果未找到则返回 string::npos
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,pos 从 s 中位置 pos 开始查找指针 cp 指向的空字符结尾的C风格字符串, pos 默认为0
cp,pos,n 从 s 中位置 pos 开始查找指针 cp 指向的数组的前 n 个字符。 pos 和 n 无默认值

指定在哪里开始搜索

程序员可以传递给 find 操作一个可选的开始位置。这个可选的参数指出从哪个位置开始进行搜索。默认情况下,此位置被置为0。一种常见的程序设计模式是用这个可选参数在字符串中循环地搜索子字符串出现的所有位置:

string::size_type pos = 0;
// 每步循环查找 name 中的一个数
// npos 是指 string 的末尾最后的位置,一般是 std::container::size_type
while( (pos = name.find_first_of(numbers, pos)) != string::npos ){    
	std::cout << "found number at index" << pos << "element is "<< name[pos] << endl;
    ++pos;//移动到下一个字符
} 

while 语句的循环条件将pos重置为从pos开始遇到的第一个数字的下标。只要 find_first_of 返回一个合法下标,程序就打印当前结果并递增pos。 如果忽略了递增pos,循环就永远也不会终止。为了搞清楚原因,考虑如果不做递增运算会发生什么。在第二步循环中,从pos指向的字符开始搜索。这个字符是—个数字,因此 find_first_of 会(重复地)返回pos。

逆向搜索

到现在为止,已经用过的 find 操作都是由左至右搜索。标准库还提供了类似的, 但由右至左搜索的操作。 成员函数 rfind 搜索最后一个匹配,即子字符串最靠右的出现位置

string river("Mississippi");
auto first_pos = river. find ("is") ; // 返回 1
auto last_pos = river. rfind ("is") ; // 返回 4

成员函数 find 返回下标值1,表示第一个"is"的位置,而 rfind 返回下标4,表示最后一个"is"的位置。

类似的, find_last 函数的功能与 find_first 函数相似,只是它们返回最后一个字符而不是第一个字符的匹配

  • find_last_of 搜索与给定 string 中任何一个字符匹配的最后一个字符。
  • find_last_not_of 搜索最后一个不出现在给定 string 中的字符。

9.5.4 compare 函数

除了关系运算符外,标准库 string 类型还提供了一组重载的成员函数 compare ,这些重载函数与C标准库的 strcmp 函数(在C++中在头文件 cstring 中)很相似。类似 strcmp ,根据s是等于、大于还是小于参数指定的字符串, s.compare 返回0、正数或负数。

如下表所示,成员函数 compare 有6个版本。根据程序员是要比较两个 string 还是一个 string 与一个字符数组,参数各有不同。在这两种情况下,都可以比较整个或一部分字符串。

成员函数 compare 的几种重载形式
s.compare( s2 ) 比较 s 和 s2
s.compare( pos1, n1, s2 ) 将 s 中从 pos1 开始的 n1 个字符与 s2 进行比较
s.compare( pos1, n1, s2, pos2, n2 ) 将 s 中从 pos1 开始的 n1 个字符与 s2 中从 pos2 开始的 n2 个字符进行比较
s.compare( cp ) 比较 s 与 cp 指向的以空字符结尾的字符数组
s.compare( pos1, n1, cp ) 将 s 中从 pos1 开始的 n1 个字符和指针 cp 指向的以空字符结尾的C风格字符串进行比较
s.compare( pos1, n1, cp, n2 ) 将 s 中从 pos 开始的 n1 个字符与指针 cp 指向的地址开始的 n2 个字符进行比较

9.5.5 数值转换

字符串中常常包含表示数值的字符。例如,用两个字符的 string 表示数值15——字符’1’后跟字符’5’。一般情况,一个数的字符表示不同于其数值。数值15如果保存为16位的 short 类型,则其二进制位模式为0000000000001111,而字符串"15"存为两个 Latin-1 编码char ,二进制位模式为0011000100110101。第一个字节表示字符’1’,其八进制值为061,第二个字节表示字符’5’,,其 Latin-1编码为八进制值065。

新标准引入了多个函数,可以实现数值数据与标准库 string 之间的转换:

int i = 42;
string s = to_string(i);// 将整数 i 转换为字符表示形式
double d = stod(s);		// 将字符串 s 转换为浮点数

此例中,程序调用函数 to_string 将42转换为其对应的 string 表示,然后调用函数 stod 将此 string 转换为浮点值。

要转换为数值的 string 中第一个非空白符必须是数值中可能出现的字符:

string s2 = "pi = 3.14";
// 转换s中以数字开始的第一个子串,结果d = 3.14
d = stod(s2.substr(s2.find_first_of("+-.0123456789")));

在这个 stod 调用中,还调用了成员函数 find_first_of 来获得s中第一个可能是数值的一部分的字符的位置。然后调用成员函数 substr 返回s中从此位置开始的子串作为参数传递给 stodstod 函数读取此参数,处理其中的字符,直至遇到不可能是数值的一部分的字符。然后它就将找到的这个数值的字符串表示形式转换为对应的双精度浮点值。

对于这些实现数值数据与标准库 string 之间转换的函数, string 参数中第一个非空白符必须是符号(+或-)或数字。它可以以 0x 或 0X 开头来表示十六进制数。对那些将字符串转换为浮点值的函数, string 参数也可以以小数点(.)开头,并可以包含 e 或 E 来表示指数部分。 对于那些将字符串转换为整型值的函数,根据基数不同, string 参数可以包含字母字符,对应大于数字9的数,比如十六进制。

Note:

如果 string 不能转换为一个数值,这些函数将抛出一个 invalid_argument 异常(参见第5.6节)。如果转换得到的数值无法用任何类型赖表示,则抛出一个 out_of_range 异常。

string 和数值之间的转换
to_string(val) 一组重载函数,返回参数 val 的 string 表示。 val 可以是任何算术类型。对每个浮点类型和 int 或更大的整型,都有相应版本的 to_string 。
stoi(s, p, b) 返回 s 的起始子串(表示整数内容)的数值。返回值类型分别是 int 、 long 、 unsigned long 、 long long 、 unsigned long long 。 b 表示转换所用的基数(即进制),默认值是10。 p 是指向 std::size_t 类型的指针,用来保存 s 中第一个非数值字符的下标, p 默认是0,即,函数不保存下标
stol(s, p, b)
stoul(s, p, b)
stoll(s, p, b)
stoull(s, p, b)
stof(s, p) 返回 s 的起始子串(表示浮点数内容)的数值,返回值类型分别是 float 、 double 和 long double 。参数 p 的作用与整数转换中一样
stod(s, p)
stold(s, p)

9.6 容器适配器

​​  除了顺序容器外,标准库还定义了三个顺序容器适配器: stackqueuepriority_queue

​​  适配器(adaptor) 是标准库中的一个通用概念。 容器、迭代器和函数都有适配器。 本质上一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。比如,一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。

​​  例如, stack 适配器接受一个顺序容器(除 arrayforward_list 外),并使其操作起来像一个 stack 一样(外层包装)。下表列出了所有容器适配器都支持的操作和类型。

所有容器适配器都支持的操作和类型
size_type 一种类型,足以保存当前类型的最大对象的大小
value_type 元素类型
container_type 实现适配器的底层容器类型
A a; 创建一个名为 a 的空适配器
A a(c); 创建一个名为 a 的适配器,带有容器 c 的一个拷贝
关系运算符 每个适配器都支持所有关系运算符:==、!=、<、<=、>和>=,这些运算符返回底层容器的比较结果
a.empty() 若 a 包含任何元素,返回 false ,否则返回 true
a.size() 返回 a 中的元素数目
swap(a, b) 交换 a 和 b 的内容, a 和 b 必须有相同的类型,包括底层容器类型也必须相同
a.swap(a,b)

定义一个适配器

每个STL容器适配器都定义了两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。 例如,假定 deq 是一个 deque ,可以用 deq 来初始化一个新的 stack ,如下所示:

stack<int> stk(deq);	// 从deq拷贝元素到stk

默认情况下, stackqueue 是基于 deque 实现的, priority_queue 是在 vector 之上实现的。 程序员可以在创建一个STL容器适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。

// 在vector_h实现的空栈
stack<string, vector<string>> str_stk;
// str_stk2在vector上实现,初始化时保存svec的拷贝
stack<string, vector<string>> str_stk2(svec);

对于一个给定的STL容器适配器,可以使用哪些容器是有限制的。所有STL容器适配器都要求容器具有添加和删除元素的能力。 因此,STL容器适配器不能构造在 array 之上。类似的,程序员也不能用 forward_list 来构造STL容器适配器,因为所有STL容器适配器都要求容器具有添加、删除以及访问尾元素的能力。

stack 适配器 只要求 push_backpop_backback 操作,因此可以使用除 arrayforward_list 之外的任何容器类型来构造 stack
queue 适配器 要求 backpush_backfrontpush_front ,因此它可以构造于 listdeque 之上,但不能基于 vector 构造;
priority_queue 适配器 除了要求 frontpush_backpop_back 操作之外还要求有随机访问能力,因此它可以构造于 vectordeque 之上,但不能基于 list 构造。

栈适配器

stack 类型定义在 stack 头文件中。 下表列出了 stack 所支持的操作。下面的程序展示了如何使用 stack

stack<int> intStack;	// 空栈
// 填满栈
for (size_t ix = 0; ix != 10; ++ix)
	intStack.push(ix); 	// intStack 保存 0 到 9 十个数
while (!intStack.empty())	{ 	// intStack 中有值就继续循环
		int value = intStack.top();
		// 使用栈顶值的代码
		intStack.pop(); // 弹出栈顶元素,继续循环
}

其中,声明语句

stack<int> intStack; // 空栈

定义了一个保存整型元素的栈intStack,初始时为空。 for 循环将10个元素添加到栈中,这些元素被初始化为从0开始连续的整数。 while 循环遍历整个 stack ,从而获取 top 值,再将其从栈中弹出,直至栈空。

上一张表格未列出的栈( stack )操作
栈(即 stack )默认是基于 deque 实现,也可以在 list 或 vector 上实现
s.pop() 删除栈顶元素,但不返回该元素的值
s.push( item ) 创建一个新元素压人栈顶,该元素通过拷贝或移动 item 而来,或者由 args 构造
s.emplace( args )
s.top() 返回栈顶元素,但不将元素弹出栈

每个容器适配器都基于底层容器类型的操作定义了自己的特殊操作。程序员只可以使用适配器操作,而不能使用底层容器类型的操作。 例如:

intStack.push(ix);	// intStack保存0到9,一共十个数

此语句试图在intStack的底层 deque 对象上调用 push_back 。虽然 stack 默认是基于 deque 实现的,但不能直接使用 deque 的操作。不能在一个 stack 调用 push_back ,而必须使用 stack 自己的操作——即成员函数 push

队列适配器

queuepriority_queue 适配器定义在 queue 头文件中。 下表列出了它们所支持的操作。

上一张表格未列出的队列( queue )和优先队列( priority_queue )的操作
queue 默认基于 deque 实现, priority_queue 默认基于 vector 实现;
queue 也可以由 list 或 vector 实现, priority_queue 也可以用 deque 实现
q.pop() 返回 queue 的首元素或 priority_queue 的最高优先级的元素,但不删除此元素
q.front() 返回首元素或尾元素,但不删除此元素,只适用于 queue
q.back()
q.top() 返回最高优先级元素,但不删除该元素,只适用于 priority_queue
q.push( item ) 在 queue 末尾或 priority_queue 中恰当的位置创建一个元素。其值为 item ,或者由 args 构造
q.emplace( args )

标准库类型 queue 使用一种 先进先出(first-in first-out,FIFO)的存储和访问策略 。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除。即先进入队列的元素比后进入的元素先被删除。

标准库类型 priority_queue 允许 程序员为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。默认情况下,标准库在元素类型上使用 < 运算符来确定相对优先级。 将在11.2.2节学习如何重载这个默认设置。

你可能感兴趣的:(C++,Primer,读书笔记,c++)