9 顺序容器

目录

0. 容器概述

  • 容器是一种容纳特定类型对象的集合。

  • C++的容器可以分为两类:顺序容器和关联容器。

  • 顺序容器的元素排列和元素值大小无关,而是由元素添加到容器中的次序决定的。

  • 标准库定义了三种顺序容器的类型:vector、list和deque(双端队列)

  • 此外,标准库还提供了三种容器适配器:stack、queue和prioroty_queue类型适配器是根据原始的容器类型所提供的操作,通过定义新的操作接口,来适应基础的容器类型。

  • 注意:类型别名就是在该容器内中对int、iterator等类型重新命名成,就像你的名字在不同国家是不同的。虽然我们在实际运用时用auto

1. 顺序容器概述

  • 一个容器就是一些特定类型对象的集合。

  • 顺序容器(sequential container):为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。

  • 顺序容器类型:

  • 顺序容器 vs 关联容器

    • 顺序容器:元素在顺序容器中的顺序与其加入容器时的位置相对应
    • 关联容器:元素在关联容器中的位置由元素相关联的关键字值决定。关联容器根据关键字的值来存储元素。
  • 下表列出了标准库中的顺序容器,所以顺序容器都提供了快速顺序访问元素的能力。但是,这些容器在以下方面都有不同的性能折中:

    • 添加或删除元素的代价
    • 非顺序访问元素的代价
  • 除了固定大小的 array 外,其他容器都提供高效、灵活的内存管理。例如:

  • (1)string 和 vector

    • string 和 vector 将元素保存在连续的内存空间中。由于元素是连续存储的,由元素的下标来计算其地址是非常快速的。
    • 但是,在这两种容器的中间位置添加或删除元素就会非常耗时:在一次插入或删除操作后,需要移动插入/删除位置之后的所有元素,来保持连续存储
    • 而且添加一个元素有时可能还需要分配额外的存储空间。在这种情况下,每个元素都必须移动到新的存储空间中。
  • (2)list 和 forward_list

    • list 和 forward_list 两个容器的设计目的是令容器任何位置的添加和删除操作都很快速
    • 作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。
    • 而且与 vector 、deque 和array 相比, 这两个容器的额外内存开销也很大。
  • (3)deque

    • deque 是一个更为复杂的数据结构。
    • 与 string 和 vector 类似,deque 支持快速的随机访问
    • 与 strìng 和 vector 一样,在 deque 的中间位置添加或删除元素的代价(可能)很高
    • 但是,在 deque 的两端添加或删除元素都是很快的,与 list 或 forward_list 添加删除元素的速度相当。
  • forward_list 和 array 是C++11新增类型。与内置数组相比,array 更安全易用。

  • 与内置数组类似,array 对象的大小是固定的。不支持添加/删除元素和改变容器大小

  • forward_list 的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此forward_list 没有 size 操作(节省开销)。对其他容器而言,size保证是一个快速的常量时间的操作。

  • 新标准库容器的性能几乎肯定与最精心优化过的同类数据结构一样好(通常会更好)。现代C++程序应该使用标准库容器,而不是更原始的数据结构,如内置数组

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

  • 容器选择原则:

    • 除非有合适的理由选择其他容器,否则应该使用 vector。
    • 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用 list 或 forward_list。
    • 如果程序要求随机访问容器元素,则应该使用 vector 或 deque
    • 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用 deque
    • 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则先确定是否真的需要在容器中间位置插入元素:
      • 当处理输入数据时,可以先向 vector 追加数据,再调用标准库的 sort 函数重排元素,从而避免在中间位置添加元素。
      • 如果必须在中间位置插入元素,可以在输入阶段使用 list。输入完成后将 list 中的内容拷贝到 vector 中。
    • 如果程序既需要随机访问元素,又需要在容器中间位置插入元素——取决于list/forward_list 中访问元素与vector/deque中插入/删除元素的相对性能。一般应用中占主导地位的操作(访问更多还是插入/删除更多)决定了容器类型的选择。在此情况下,对两种容器分别测试应用的性能可能就是必要了
    • 不确定应该使用哪种容器时,可以先只使用 vector 和 list 的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择 vector 或 list 都很方便

2. 容器库概览:对所有容器都适用的操作

  • 所有容器公共操作:

  • 容器类型的操作上形成了一种层次:
    -某些操作是所有容器类型都提供的(表9.2 P295)
    • 另外一些操作仅针对顺序容器(表9.3 P299)、关联容器(表11.7 P388)或无序容器(表11.8 P395)
    • 还有一些操作只适用于一小部分容器
  • 定义一个容器类型的对象之前,必须包含相关的头文件
    • 每个容器都定义在一个头文件中,文件名与类型名相同。即deque定义在头文件deque中,list定义在头文件list中,以此类推。
    • #include<list>
    • #include<vector>
    • #include<deque>
  • 容器均为模板类型——必须提供额外信息来生成特定的容器类型。对大多数,但不是所有容器,我们还需要额外提供元素类型信息:
// 顺序容器几乎可以保存任意类型的元素
list<Sales_data>  //保存Sales_data对象的list
deque<double>
vector<vector<string>> lines;  //vector的vector
// 此处lines是一个vector,其元素类型是string的vector

2.1 迭代器

  • (1)获取迭代器

    • 以c开头的版本是C++11新标准引入的;当不需要写访问时,应该使用cbegin和cend。
  • 容器迭代器支持的所有运算符:表3.6 P96

    • 例外:forward_list 类型不支持递减运算符 --。
  • (2)迭代器支持的算术运算

    • 这些运算只能应用于string、vector、deque和array的迭代器。我们不能将它们用于其他任何容器类型的迭代器。
  • (3)迭代器的范围:[begin, end)

    • 一个 迭代器范围 由一对迭代器表示。这两个迭代器通常被称为 begin 和 end,分别指向同一个容器中的元素或尾后地址。
    • 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 = val;   // 正确:范围非空,因此begin指向一个元素
    ++begin;    	// 移动迭代器,获取下一个元素
}

2.2 容器类型成员

  • 类型别名:

  • 通过类型别名,可以在不了解容器元素类型的情况下使用元素

    • 如果需要元素类型,可以使用容器的 value_type。
    • 如果需要元素类型的引用,可以使用 reference 或 const_reference。
  • 显式使用类型别名

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

2.3 begin和end成员

  • 容器调用begin和end成员返回迭代器

  • 获取迭代器

  • 获取反向迭代器

  • begin 和 end 操作生成指向容器中第一个元素和尾后地址的迭代器。其常见用途是形成一个包含容器中所有元素的迭代器范围

  • begin 和 end 操作有多个版本:

    • r 的版本返回反向迭代器
    • c 开头的版本(C++11新增)返回 const 迭代器
    • 不以 c 开头的版本都是重载的,当对非常量对象调用这些成员时,返回普通迭代器,对 const 对象调用时,返回 const 迭代器。以begin为例,实际上有两个名为begin的成员:
      • 一个是const成员,返回容器的const_iterator类型:常量对象.begin()
      • 一个是非常量成员,返回容器的iterator类型:非常量对象.begin()
list<string> a = {"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin();    // list<string>::iterator
auto it2 = a.rbegin();   // list<string>::reverse_iterator
auto it3 = a.cbegin();   // list<string>::const_iterator
auto it4 = a.crbegin();  // list<string>::const_reverse_iterator
  • 以 c 开头的版本是C++新标准引入的,用以支持 auto 与 begin 和 end 函数结合使用
// 显式指定类型
list<string>::iterator it5 = a.begin();
list<string>::const_iterator it6 = a.begin();
// 是iterator还是const_iterator依赖于a的类型
auto it7 = a.begin();  //仅当a是const时,it7是const_iterator
auto it8 = a.cbigin();  //it8是const_iterator
  • 当 auto 与 begin 或 end 结合使用时,返回的迭代器类型依赖于容器类型。但调用以 c 开头的版本仍然可以获得 const 迭代器,与容器是否是常量无关。

  • 当程序不需要"写"访问时,应该使用 cbegin 和 cend

// 例题:下面4个对象分别是什么类型?
vector<int> v1;  //v1非常量
const vector<int> v2;  //v2常量
auto it1 = v1.begin(), it2 = v2.begin();
auto it3 = v1.cbegin(), it4 = v2.cbegin();
/*
(1)v1 是 int 的 vector 类型,我们可以修改 v1 的内容,包括添加、删除元素及修改元素值等操作。
(2)v2 是 int 的常量 vector 类型,其内容不能修改,添加、删除元素及修改元素值等均不允许。
(3)begin 与 auto 结合使用时,会根据调用对象的类型来决定迭代器的类型,因此 it1 是普通迭代器,可对容器元素进行读写访问,而 it2 是 const 迭代器,不能对容器元素进行写访问。
(4)而 cbegin 则不管调用对象是什么类型,始终返回 const 迭代器,因此 it3 和 it4 都是 const 迭代器
*/

2.4 容器定义和初始化

  • 容器定义和初始化方式:

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

  • 将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。传递迭代器参数来拷贝一个范围时,不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。

// 每个容器有三个元素,用给定的初始化器进行初始化
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());
  • 当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元余类型都必须相同。

  • C++11允许对容器进行列表初始化。

// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
  • 只有顺序容器的构造函数才接受大小参数,关联容器并不支持。

  • 容器初始化方法总结

  • (1)将一个容器初始化为另一个容器的拷贝,两种:

    • 1)直接拷贝整个容器:两个容器的容器类型和元素类型都必须相同。
    • 2)(array 除外)传递迭代器参数来拷贝一个范围:不要求容器类型相同,而且新容器和原容器中的元素类型也可以不同,但是要能进行类型转换。
// 1)
// 每个容器有三个元素,用给定的初始化器进行初始化
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());

// 2)
//拷贝元素,直到(但不包括)it指向的元素
deque<string> authList(authors.begin(), it)
  • (2)列表初始化
    • C++11允许对容器进行列表初始化:
      • 显式地指定了容器中每个元素的值;
      • 对于除了array之外的容器类型,还隐含地指定容器的大小:容器将包含与初始值一样多的元素
// 每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};
  • (3)与顺序容器大小相关的构造函数(array除外)
    • 只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
    • 接受一个容器大小和一个(可选的)元素初始值(没有则标准库会创建一个值初始化器P88)
    • 如果元素类型是内置类型或者是具有默认构造函数的类类型:可以只提供容器大小参数
    • 如果元素类型没有默认构造函数:除了大小参数,必须指定一个显式的元素初始值
vector<int> ivec(10, -1);   //10个int元素,每个都初始化为-1
list<string> svec(10, "hi!");   //10个strings;每个都初始化为"hi!"
forward_list<int> ivec(10);  //10个元素,每个都初始化为0
deque<string> svec(10);   //10个元素,每个都是空string
  • (4)具有固定大小的标准库array初始化:与内置数组类似
    • 定义和使用 array 类型时,需要同时指定元素类型和容器大小:array<元素类型,容器大小>
    • 2)对 array 进行列表初始化时,初始值的数量不能大于 array 的大小。如果初始值的数量小于 array 的大小,则只初始化靠前的元素,剩余元素会被值初始化。如果元素类型是类类型,则该类需要一个默认构造函数。
    • 3)可以对 array 进行拷贝或赋值操作,但要求二者的元素类型和大小都相同。
// 1)
array<int, 42>      // 类型为:保存42个int的数组
array<string, 10>   // 类型为:保存10个string的数组
array<int, 10>::size_type i;   // 数组类型包括元素类型和大小
array<int>::size_type j;       // 错误:array<int>不是一个类型

// 2)
array<int, 10> ia1;   
array<int, 10> ia2 = {0,1,2,3,4,5,6,7,8,9};  
array<int, 10> ia3 = {42};   // ia3[0]为42,剩余元素为0

// 3)
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;  //正确

2.5 赋值和swap

  • 容器赋值操作:

  • 使用非成员版本的swap是一个好习惯

  • assign操作不适用于关联容器和array

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

  • 容器赋值方式

  • (1)直接使用赋值运算符(两侧的运算对象必须类型相同)

    • 方法:用另一容器赋值;花括号列表赋值(array除外)
    • 赋值运算符要求左边和右边的运算对象具有相同的类型。它将右边运算对象中所有元素拷贝到左边运算对象中
    • 结果:赋值后左边容器大小改变,与右边容器同
c1 = c2;   // 将c1的内容替换为c2中元素的拷贝,容器大小变为同c2
c1 = {a, b, c};   // 赋值后,c1大小为3
  • (2)使用assign(仅顺序容器——array除外)
  • 1)版本1:assign(迭代器1, 迭代器2)
    • 赋值运算符两侧的运算对象必须类型相同。assign 允许用不同但相容的类型赋值,或者用容器的子序列赋值。
    • 由于其旧元素被替换,因此传递给 assign 的迭代器不能指向调用 assign 的容器本身。
list<string> names;
vector<const char*> oldstyle;
names = oldstyle;   // 错误: 容器类型不匹配
// 正确:可以将const char*转换为string
names.assign(oldstyle.cbegin(), oldstyle.cend());
  • 2)版本2:assign(整型值,元素值)
    • assign的第二个版本接受一个整型值和一个元素值。它用指定数目且具有相同给定值的元素替换容器中原有的元素:
//等价于slist.clear()
//后跟slist.insert(slist.begin(),10,"Hiya!");
list<string> slist(1);  //1个元素,为空string
slist.assign(10,"Hiya!");  //10个元素,每个都是"Hiya!"
  • (3)使用swap——交换两个相同类型容器的内容
    • 元素本身并未移动,swap只是交换了两个容器的内部数据结构
    • 除 array 外,swap 不对任何元素进行拷贝、删除或插入操作,只交换两个容器的内部数据结构,因此可以保证快速完成。
    • 元素不会被移动的事实意味着,对于其他容器类型(除string外),指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍指向swap操作之前所指向的那些元素。但是,在swap之后,这些元素已经属于不同的容器了。例如下面的例子中,假定iter在swap之前指向svec1[3]的string,那么在swap之后它指向svec2[3]的元素。
    • 两个例外:
      • 与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效。
      • 对于 array,swap 会真正交换它们的元素。因此在 swap 操作后,指针、引用和迭代器所绑定的元素不变,但元素值已经被交换(即元素值已经与另一个array中对应元素的值进行了互换)——交换两个array所需的时间与array中元素的数目成正比。
vector<string> svec1(10);   // 10个元素的vector
vector<string> svec2(24);   // 24个元素的vector
swap(svec1, svec2);
  • 新标准库同时提供了成员和非成员函数版本的 swap。非成员版本的 swap 在泛型编程中非常重要,建议统一使用非成员版本的 swap

2.6 容器大小操作

  • size 成员返回容器中元素的数量;
  • empty 当 size 为0时返回 true,否则返回 false;
  • max_size 返回一个大于或等于该类型容器所能容纳的最大元素数量的值。
  • forward_list 支持 max_size 和 empty,但不支持 size。

2.7 关系运算符

  • 每个容器类型都支持相等运算符(==、!=)

  • 除无序关联容器外,其他容器都支持关系运算符(>、>=、<、<=)。

  • 关系运算符两侧的容器类型和保存元素类型都必须相同

    • 如vector只能与vector比较,不能与list、vector作比较
  • 两个容器的比较实际上是元素的逐对比较,其工作方式与string的关系运算符类似:

    • 如果两个容器大小相同且所有元素对应相等,则这两个容器相等。
    • 如果两个容器大小不同,但较小容器中的每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
    • 如果两个容器都不是对方的前缀子序列,则两个容器的比较结果取决于第一个不等元素的比较结果。
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 = { 1, 3, 5, 7, 9, 12 };
v1 < v2     // true;v1和v2在元素[2]处不同:v1[2]小于等于v2[2]
v1 < v3     // false;所有元素都相等,但v3中元素数目更少
v1 == v4    // true;每个元素都相等,且v1和v4大小相同
v1 == v2    // false;v2元素数目比v1少
  • 容器的相等运算符实际上是使用元素的 == 运算符实现的,而其他关系运算符则是使用元素的 < 运算符。如果元素类型不支持所需运算符,则保存该元素的容器就不能使用相应的关系运算。

3. 顺序容器(特有)操作:存储、访问、添加以及删除相关

  • 顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系的到了元素如何存储、访问、添加以及删除。

3.1 向顺序容器添加元素:push_back、push_front、insert与emplace_back、emplace_front、emplace

  • 除 array 外,所有标准库容器都提供灵活的内存管理,在运行时可以动态添加或删除元素。
  • 当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能
    • 在一个vector或string的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。
    • 而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中
  • (1)拷贝元素方式添加:调用 push或insert 时,参数即为元素对象被拷贝到容器中
  • 1)使用push_back:顺序容器尾部插入单个元素
    • push_back 将一个元素追加到容器尾部。除 array 和 forward_list 之外, 每个顺序容器(包括 string 类型〉都支持 push_back。
// 从标准输入读取数据,将每个单词放到容器末尾
string word;
while (cin >> word)
    container.push_back(word);
/*
对push_back的调用在container尾部创建了一个新的元素,将container的size增大了1。
该元素的值为word的一个拷贝,container的类型可以是list、vector或deque。
*/
  • 关键概念:容器元素是拷贝
    • 当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。就像我们将一个对象传递给非引用参数一样,容器中的元素与提供值的对象之间没有任何关联。随后对容器中元素的任何改变都不会影响到原始对象,反之亦然。
  • 2)使用push_front:顺序容器头部插入单个元素
    • 除了push_back, list、forward_list和deque容器还支持名为push_front的类似操作。此操作将元素插入到容器头部:
    • 注意:deque像vector一样提供了随机访问元素的能力,但它提供了vector所不支持的push_front。deque保证在容器首部进行插入和删除元素的操作都只花费常数时间。与vector一样,在deque首尾之外的位置插入元素会很耗时。
list<int> ilist;
//将元素添加到Ilist开头
for(size_t ix=0;ix!=4;++ix)
    ilist.push_front(ix);
/*
此循环将元素0、1、2、3添加到ilist头部。每个元素都插入到list的新的开始位置。
即,当我们插入1时,它会被放置在0之前,2被放置在1之前,依次类推。
因此,在循环中以这种方式将元素添加到容器中,最终会形成逆序
*/
  • 3)使用insert:在容器中的特定位置添加元素——将n个元素插入到迭代器所指定的位置之前
  • 第一个参数都为目标容器的一个迭代器p
    • 迭代器指出了在容器中什么位置放置新元素。它可以指向容器中任何位置,包括容器尾部之后的下一个位置。由于迭代器可能指向容器尾部之后不存在的元素的位置,而且在容器开始位置插入元素是很有用的功能,所以insert函数将元素插入到迭代器所指定的位置之前
//(a)c.insert(p,t)    指定元素
slist.insert(iter,"Hello!");  //将hello添加到iter之前的位置

//(b)c.insert(p,n,t) 指定元素的数量和初始值
svec.insert(svec.end(),10,"Anna");  //将10个元素插入到svec的末尾,并将所有元素都初始化为string“Anna”

//(c)c.insert(p,b,e) 接受一对迭代器(不能指向添加元素的目标容器)
vector<string> v={"quasi","simba","frollo","scar"};
//将v的最后两个元素添加到slist的开始位置
slist.insert(slist.begin(),v.end()-2,v.end());
//运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器				slist.insert(slist.begin(),slist.begin(),slist.end())
						
//(d)c.insert(p,il)    接受一个初始化列表
slist.insert(slist.end(),{"these","words","will","go","at","the","end"});
  • vector、deque、list和string都支持insert成员。forward_list提供了特殊版本的insert成员
  • 一些不支持 push_front 的容器可以使用 insert 将元素插入开始位置
  • 将元素插入到 vector、deque 或 string 的任何位置都是合法的,但可能会很耗时
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!");
  • 4)使用insert的返回值:返回指向第一个新增元素的迭代器;若范围为空,则返回p(可以在容器中一个特定位置反复插入元素)
    • 在新标准库中,接受元素个数或范围的 insert 版本返回指向第一个新增元素的迭代器,而旧版本中这些操作返回 void。
    • 如果范围为空,不插入任何元素,insert 会返回第一个参数p。
list<string> 1st;
auto iter = 1st.begin();
while (cin >> word)
    iter = 1st.insert(iter, word);  // 等价于调用push_front
  • (2)构造元素方式添加(特别是容器存储类实例时):调用 emplace 操作
  • 使用emplace操作。新标准库增加了三个直接构造而不是拷贝元素的操作:
    • 1)emplace_front——对应push_front
    • 2)emplace_back——对应push_back
    • 3)emplace——对应insert
  • 当调用 push或insert 时,元素对象被拷贝到容器中。而调用 emplace 时,则是将参数传递给元素类型的构造函数,直接在容器的内存空间中构造元素
// 例如,假定c保存Sales_data元素:
// 在c的末尾构造一个Sales_data对象
// 使用三个参数的Sales_data的构造函数
c.emplace_back("978-0590353403",25,15.99);
// 错误:没有接受三个参数的push_back版本
c.push_back("978-0590353403",25,15.99);
// 正确:创建一个临时的Sales_data对象传递给push_back
c.push_back(Sales_data(("978-0590353403",25,15.99));
  • 区别:其中对emplace_back的调用和第二个push_back调用都会创建新的Sales_data对象。在调用emplace_back时,会在容器管理的内存空间中直接创建对象。而调用push_back则会创建一个局部临时对象,并将其压入容器中

  • emplace 函数在容器中直接构造元素。传递给 emplace 函数的参数必须与元素类型的构造函数相匹配:元素类型的构造函数可能是多个,根据参数的不同选择不同的函数

// emplace函数的参数根据元素类型而变化
//iter指向c中一个元素,其中保存了Sales_data元素
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);
  • forward_list 有特殊版本的 insert 和 emplace 操作,且不支持 push_back 和 emplace_back。vector 和 string不支持 push_front 和 emplace_front

3.2 访问元素:front、back、下标和at(访问元素前确保容器非空)

  • 顺序容器的元素访问操作:

    • 访问成员函数返回的是引用。
    • at和下标操作只适用于string、vector、deque、array。
    • back不适用于forward_list。
    • 如果希望下标是合法的,可以使用at函数
    • 如果容器中没有元素,访问操作的结果是未定义的.一个空容器调用front和back,就像使用一个越界的下标一样
  • 包括array在内的每个顺序容器都有一个 front 成员函数,而除了 forward_list 之外的顺序容器还有一个 back 成员函数。这两个操作分别返回首元素和尾元素的引用。

  • 有两种不同方式可以获取容器中的首元素和尾元素的引用

  • (1)间接方式:调用begin和end

    • 通过解引用begin返回的迭代器来获得首元素的引用,以及通过递减然后解引用end返回的迭代器来获取尾元素的引用
    • 迭代器 end 指向的是容器尾元素之后的(不存在的)元素。为了获取尾元素,必须首先递减此迭代器;
  • (2)直接方式:调用front和back

    • 在调用front和back之前(或解引用 begin 和 end 返回的迭代器)之前,要确保容器非空。
      ···C++
      //在解引用一个迭代器或调用front或back之前检查是否有元素
      if(!c.empty()){
      //val和val2是c中第一个元素值的拷贝
      auto val=c.begin(),val2=c.front();
      //val3和val4是c中最后一个元素值的拷贝
      auto last=c.end();
      auto val3=
      (--last);  //不能递减forward_list迭代器
      auto val4=c.back();  //forward_list不支持
- **在容器中访问元素的成员函数(即,front、back、下标和at)都返回引用类型**。
    - 如果容器是 const 对象,则返回值是 const 引用。
    - 如果容器不是const的,则返回值是普通引用,我们可以用来改变元素的。
```C++
if(!c.empty()){
    c.front()=42;  //将42赋予c中的第一个元素
    auto &v=c.back();   //获得指向最后一个元素的引用v=1024;    //改变c中的元素
    auto v2=c.back();    //v2不是一个引用,它是c.back()的一个拷贝
    v2=0;      //未改变c中的元素
}
  • 注意:如果我们使用auto变量来保存这些函数的返回值,并且希望使用此变量来改变元素的值,必须记得将变量定义为引用类型

  • 下标操作[]和安全的随机访问at

    • 可以快速随机访问的容器(string、vector、deque 和 array)都提供下标运算符。保证下标有效是程序员的责任。
    • 下标运算符接受一个下标参数,返回容器中该位置的元素的引用
    • 如果希望确保下标合法,可以使用 at 成员函数。at 类似下标运算,但如果下标越界,at 会抛出 out_of_range 异常
vector<string> svec;  // 空vector
cout << svec[0];      // 运行时错误:svec中没有元素!
cout << svec.at(0);   // 抛出一个out_of_range异常

3.3 删除元素(除array):pop_back、pop_front、erase、clear。删除元素前,程序员必须确保目标元素存在

  • 顺序容器的元素删除操作:
    • 删除操作会改变容器大小,不适用于array。
    • forward_list有特殊版本的erase
    • forward_list不支持pop_back
    • vector和string不支持pop_front
    • 删除deque中除首位元素之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。
    • 删除元素的成员函数并不检查其参数。删除元素前,程序员必须确保目标元素存在
  • (1)pop_front 和 pop_back 函数分别删除首元素和尾元素。
    • vector 和 string 类型不支持 pop_front,forward_list 类型不支持 pop_back。
while (!ilist.empty()){
	process(ilist.front()); // 对ilist的首元素进行一些处理
	ilist.pop_front(); 		// 完成处理后删除首元素
}
  • (2)erase 函数删除指定位置的元素。
    • 1)可以删除由一个迭代器指定的单个元素
    • 2)也可以删除**由一对迭代器指定的范围[…)内&&的所有元素。
    • 为了删除一个容器中的所有元素,我们既可以调用clear,也可以用begin和end获得的迭代器作为参数调用erase
    • 两种形式的 erase 都返回指向(最后一个)删除元素之后位置的迭代器。即,若j是i之后的元素,那么erase(i)将返回指向j的迭代器
// 1)
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);  //删除此元素
    else
        ++it;
}
// 每个循环步中,首先检查当前元素是否是奇数,
// 如果是,就删除该元素,并将it设置为我们所删除的元素之后的元素。
// 如果*it为偶数,我们将it递增,从而在下一步循环检查下一个元素。


// 2)
// 删除两个迭代器表示的范围内的元素
// 返回指向最后一个被删元素之后位置的迭代器
elem1 = slist.erase(elem1, elem2);  // 调用后,elem1 == e1em2
  • (3)clear 函数删除容器内的所有元素。
slist.clear(); 		                  // 删除容器中所有元素
slist.erase(slist.begin(), slist.end());  // 等价调用

3.4 特殊的forward_list操作:insert_after、emplace_after和erase_after作用元素后——对应其他容器的insert、emplace和erase作用元素前

  • 理解forward_list为什么有特殊版本的添加和删除操作

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

  • 由于这些操作与其他容器上的操作的实现方式不同,forward_list 并未定义 insert、emplace 和 erase,而是定义了名为 insert_after、emplace_after和 erase after 的操作。

  • forward_list 的插入和删除操作:

    • 链表在删除元素时需要修改前置节点的内容,双向链表会前驱的指针,但是单向链表没有保存,因此需要增加获取前置节点的方法。
    • forward_list定义了before_begin,即首前(off-the-begining)迭代器,这个迭代器允许我们在链表首元素之前并不存在的元素“之后”添加或删除元素(亦即在链表首元素之前添加删除元素)
  • 当在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(*curr%2)
        curr=flst.erase_after(prev);// 删除它并移动curr
    else
    {
        prev=curr;  //移动迭代器curr,指向下一个元素,prev指向curr之前的元素
        ++curr;
    }
}
/*
此例中,curr表示我们要处理的元素,prev表示curr的前驱。
调用begin来初始化curr,这样第一步循环就会检查第一个元素是否是奇数。
我们用before_begin来初始化prev,它返回指向curr之前不存在的元素的迭代器。
当找到奇数元素后,我们将prev传递给erase_after,此调用将prev之后的元素删除,即,删除curr指向的元素。
然后我们将curr置为erase_after的返回值,使得curr指向序列中下一个元素,prev保持不变,仍指向(新)curr之前的元素。
如果curr指向的元素不是奇数,在else中我们将两个迭代器都向前移动。
*/

3.5 改变容器大小(除array外):resize——增加或缩小容器

  • 顺序容器的大小操作:

  • 我们可以使用resize来增加或缩小容器,与往常一样,array不支持resize。如果当前大小大于所要求的大小n,容器后面的元素会被删除;如果当前大小小于新大小n,会将新元素添加到容器后部:

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 向容器添加新元素,则必须提供初始值,或元素类型提供默认构造函数。

3.6 容器操作可能使迭代器失效

  • 向容器中添加或删除元素可能会使指向容器元素的指针、引用或迭代器失效。失效的指针、引用或迭代器不再表示任何元素,使用它们是一种严重的程序设计错误。

  • 向容器中添加元素后:

    • 如果容器是 vector 或 string 类型:
      • 如果存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。
      • 如果存储空间未重新分配,指向插入位置之前元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用都会失效。
    • 如果容器是 deque 类型:
      • 添加到除首尾之外的任何位置都会使迭代器、指针和引用失效。
      • 如果添加到首尾位置,则迭代器会失效,而指针和引用不会失效。
    • 如果容器是 list 或 forward_list 类型:
      • 指向容器的迭代器、指针和引用仍然有效。
  • 从容器中删除元素后,指向被删除元素的迭代器、指针和引用失效。当我们删除一个元素后:

    • 如果容器是 list 或 forward_list 类型:
      • 指向容器其他位置的迭代器、指针和引用仍然有效。
    • 如果容器是 deque 类型:
      • 删除除首尾之外的任何元素都会使迭代器、指针和引用失效。
      • 如果删除尾元素,则尾后迭代器失效,其他迭代器、指针和引用不受影响。
      • 如果删除首元素,这些也不会受影响。
    • 如果容器是 vector 或 string 类型:
      • 指向删除位置之前元素的迭代器、指针和引用仍然有效。但尾后迭代器总会失效。
  • 使用失效的迭代器、指针或引用是严重的运行时错误。

  • 编写改变容器的循环程序

    • 添加/删除vector、string或deque元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用和指针。如果循环中调用的是insert或erase,那么更新迭代器很容易。这些操作都返回迭代器,我们可以用来更新:
//傻瓜循环,删除偶数元素,复制每个奇数元素
vector<int> vi={0,1,2,3,4,5,6,7,8,9};
auto iter=vi.begin();  //调用begin而不是cbegin,因为我们要改变vi
while(iter!=vi.end()) 
{
    if(*iter%2){
        iter=vi.insert(iter,*iter);//复制当前元素,旧iter失效
        iter+=2;   //向前移动迭代器,跳过当前元素以及插入到它之前的元素
    }
    else
        iter=vi.erase(iter);  //删除偶数元素,旧iter失效
        //不应向前移动迭代器,iter指向我们删除的元素之后的元素
}
  • 建议:管理迭代器

    • 当你使用迭代器(或指向容器元素的引用或指针)时,最小化要求迭代器必须保持有效的程序片段是一个好的方法。
    • 由于向迭代器添加元素和从迭代器删除元素的代码可能会使选代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对 vector、string 和 deque 尤为重要。
  • 注意:不要保存end返回的迭代器

    • 当我们添加/删除vector或string的元素后,或在deque中首元素之外任何位置添加/删除元素后,用来end返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器,然后一直当作容器末尾使用
    • 如果在一个循环中插入/删除 deque、string 或 vector 中的元素,不要缓存 end 返回的迭代器,更安全的方法:
// 更安全的方法:在每个循环步添加/删除元素后都重新计算end
while (begin != v.end())
{
    // 做一些处理
    ++begin;    // 向前移动begin,因为我们想在此元素之后插入元素
    begin = v.insert(begin, 42);    // 插入新位
    ++begin;    // 向前移动begin,跳过我们刚刚加入的元素
}

4. vector对象是如何增长的

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

  • 假定容器中元素是连续存储的,其容器的大小是可变的,考虑向vector和string中添加元素会发生什么;如果没有空间容纳新元素,容器不可能简单地将它添加到内存中其他位置——因为元素必须连续存储

    • 容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放就存储空间。
    • 如果我们每添加一个新元素,vector就执行一次这样的内存分配和释放操作,性能会慢到不可接受。
  • vector 和 string 的实现通常会分配比新空间需求更大的内存空间,容器预留这些空间作为备用,可用来保存更多新元素。

  • 容器大小管理操作:

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

  • capacity函数 返回容器在不扩充内存空间的情况下最多可以容纳的元素数量。

  • reserve函数 告知容器应该准备保存多少元素,它并不改变容器中元素的数量,仅影响容器预先分配的内存空间大小

  • capacity VS size

    • 容器size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素
// (1)空vector上添加:
vector<int> ivec;
// 空vector的size为0;capacity的值依赖于具体的实现
cout << "ivec: size: " << ivec.size()
    << "capacity: " << ivec.capacity() << endl;
// 向ivec添加24个元素
for (vector<int>::size_type ix=0; ix = 0; ix !=24; ++ix)
    ivec.push_back(ix)
//size应该为24;capacity应该大于等于24,具体值依赖于标准库具体实现
cout << "ivec: size: " << ivec.size()
    << "capacity: " << ivec.capacity() << endl;
/*
输出:
ivec: size: 0 capacity:0
ivec: size: 24 capacity:32
*/

// (2)(重新)预分配一些额外空间:
// 使用reverse按照我们的需求大小分配新的空间
ivec.reserve(50);  // 将capacity至少设定为50,可能会更大
// size应该为24;capacity应该大于等于50,具体值依赖于标准库实现
cout << "ivec: size: " << ivec.size()
    << "capacity: " << ivec.capacity() << endl;
/*
输出:
ivec: size: 24 capacity:50
*/

// (3)用光这些预留空间:
//添加元素用光多余容量
while (ivec.size != ivec.capacity())
ivec.push_back(0);

cout << "ivec: size: " << ivec.size()
    << "capacity: " << ivec.capacity() << endl;
/*
输出:
ivec: size: 50 capacity:50
*/

//(4)只要需求大小没有超出vector的容量,vector就不能重新分配内存空间。
// 如果我们现在再添加一个新元素,vector就不得不重新分配内存空间:
// size应该为51;capacity应该大于等于51,具体值依赖于标准库实现
cout << "ivec: size: " << ivec.size()
    << "capacity: " << ivec.capacity() << endl;
/*
输出:
ivec: size: 51 capacity:100
*/
// 这表明vector的实现采用的策略似乎是在每次需要分配内存空间时将当前容量翻倍

//(5)可以调用shrink_to_fit来要求vector将超出当前大小的多余内存退回给系统
ivec.shrink_to_fit();  //要求归还内存
// size应该未改变;capacity具体值依赖于标准库实现
cout << "ivec: size: " << ivec.size()
    << "capacity: " << ivec.capacity() << endl;
// 调用shrink_to_fit只是一个请求,标准库并不保证退还内存
  • 只有在执行insert操作时size与capacity相等,或者调用resize或reserve时给定的大小超过当前capacity,vector才可能重新分配内存空间。额外分配多少空间取决于具体实现

  • 只有当需要的内存空间超过当前容量时,reserve 才会真正改变容器容量,分配不小于需求大小的内存空间。

    • 如果需求大小 > 当前容量,reserve至少分配与要求一样大的内存空间(可能更大)。
    • 如果需求大小 ≤ 当前容量,reserve什么也不做。特别是当需求大小 < 当前容量时,reserve 并不会退回内存空间。因此在调用 reserve 之后,capacity 会大于或等于传递给 reserve 的参数。
  • 在C++11中可以使用 shrink_to_fit 函数来要求 deque、vector 和 string 退回不需要的内存空间(并不保证退回)。

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

5. 额外的string操作(除了顺序容器的共同操作外,string的特有)

  • string大小写敏感
  • 额外操作中的大部分要么是提供string类和C风格字符数组之间的相互转换,要么是增加了允许我们用下标代替迭代器的版本

5.1 构造string的其他方法

  • 复习1:3.2.1节P76介绍的构造函数

  • 复习2:与其他顺序容器相同的构造函数表9.3 P299——容器定义和初始化方式

  • 构造 string 的其他方法:

    • 这些构造函数接受一个string或const char*参数,还可以接受(可选的)指定拷贝多少个字符的参数
  • 从另一个 string 对象拷贝字符构造 string 时,如果提供的拷贝开始位置(可选)大于给定 string 的大小,则构造函数会抛出 out_of_range 异常。

  • 通常当我们从一个const char*创建string时,指针指向的数组必须以空字符结尾,拷贝操作遇到空字符时停止。如果我们还传递给构造函数一个计数值,数组就不必以空字符结尾。如果我们未传递计数值且数组也未以空字符结尾,或者给定计数值大于数组大小,则构造函数的行为是未定义的。

  • 当从一个string拷贝字符时,我们可以提供一个可选的开始位置和一个计数值。开始位置必须小于或等于给定的string的大小。如果位置大于size,则构造函数抛出一个out_of_range异常。如果我们传递了一个计数值,则从给定位置开始拷贝这么多个字符。不管我们要求拷贝多少个字符,标准库最多拷贝到string结尾,不会更多。

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

5.2 子字符串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异常
  • 如果传递给 substr 函数的开始位置超过 string 的大小,则函数会抛出 out_of_range 异常。
  • 如果开始位置加上计数值大于string的大小,则substr会调整计数值,只拷贝到string的末尾

5.3 改变string的其他方法

  • 修改 string 的操作:

  • 修改string的方法:

  • (1)顺序容器的赋值运算符以及assign、接受迭代器的insert和erase版本

  • (2)string额外的接受下标的insert和erase版本

    • 下标指出了开始删除的位置,或是insert到给定值之前的位置:
s.insert(s.size(),5,'!'); //在s末尾插入5个感叹号
s.erase(s.size()-5,5);  //从s删除最后5个字符
  • (3)接受C风格字符数组的insert和assign版本
// 例如,我们可以将以空字符结尾的字符数组insert到或assign给一个string:
const char *cp="Stately,plump Buck";
				
// 赋予s的是从cp指向的地址开始的7个字符。要求赋值的字符数必须小于或等于cp指向的数组中的字符数(不包括结尾的空字符)
s.assign(cp,7);  //s="Stately"
				
// 将字符插入到s[size()]处(不存在的)元素之前的位置,并将cp开始的7个字符(至多到结尾空字符之前)拷贝到s中。
s.insert(s.size(),cp+7); //s=="Stately,plump Buck"
				
// 也可以指定将来自其它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());
  • (4)string类定义了两个额外的成员函数:append和replace,这两个函数可以改变string的内容
  • 1)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.");     // 等价方法:将" 4th Ed."追加到s2; s == s2
  • 2)replace 函数是调用 erase 和 insert 函数的简写形式。
// 将"4th"替换为"5th"的等价方法
s.erase(11, 3);         // s == "C++ Primer Ed."
s.insert(11, "5th");    // s == "C++ Primer 5th Ed."
// 从位置11开始,删除3个字符并插入"5th"
s2.replace(11, 3, "5th");   // 等价方法: s == s2

// 上面调用replace时,插入的文本恰好与删除的文本一样长。这不是必须的,可以插入一个更长或更短的string:
s.replace(11,3,"Fifth");   //删除了3个字符,但在其位置插入了5个新字符。

5.4 string搜索操作:6个不同的搜索函数,每个函数都有4个重载版本

  • string 的每个搜索操作都返回一个 string::size_type 值,表示匹配位置的下标。如果搜索失败,则返回一个名为 string::npos 的 static 成员。标准库将 npos 定义为 const string::size_type 类型,并初始化为-1。

  • 不建议用 int 或其他带符号类型来保存 string 搜索函数的返回值。

  • string 搜索操作:

正向搜索(从左至右搜索)

  • (1)完成最简单的搜索。它查找参数指定的字符串,若找到,则返回第一个匹配位置的下标,否则返回npos:
string name("AnnaBelle");
auto pos1=name.find("Anna");  //pos1==0,即子字符串"Anna"在"AnnaBelle"中第一次出现的下标		
  • (2)查找与给定字符串中任何一个字符匹配的位置:s.find_first_of(args) 
    • 常见场景:args表示所有可能出现情况(如所有可能数字"123456789"),然后在字符串中查找第一次出现args中任意一种情形的位置(如查找s中第一个数值出现的位置)
// 例如,下面代码定位name中的第一个数字:
string numbers("0123456789"),name("r2d2");
//返回1,即,name中第一个数字的下标
auto pos=name.find_first_of(numbers);
//调用了find_first_of来获得name中第一个可能是数值的字符的位置
  • (3)如果是要搜索第一个不在参数中的字符,我们应该调用find_first_not_of
例如,为了搜索一个string中第一个非数字字符,可以这样做:
string dept("03714p3");
//返回5,字符'p'的下标
auto pos=dept.find_first_not_of(numbers);

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

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

逆向搜索(从右至左搜索)

  • (1)rfind成员函数:搜索最后一个匹配,即子字符串最靠右的出现位置:
string river("Mississippi");			
auto first_pos=river.find("is");  //返回1				
auto last_pos=river.rfind("is");  //返回4			
// find返回下标1,表示第一个"is"的位置,而rfind返回下标4.表示最后一个"is"的位置。
  • (2)find_last函数:功能与find_first函数相似,只是它们返回最后一个而不是第一个匹配:
    • find_last_of搜索与给定string中任何一个字符匹配的最后一个字符
    • find_last_not_of搜索最后一个不出现在给定string中的字符
    • 每个操作都接受一个可选的第二参数,可用来指出从什么位置开始搜索

5.5 compare函数(string的比较:关系运算符或compare)

  • 除了关系运算符外,string 类型提供了一组 compare 函数进行字符串比较操作,类似C标准库的 strcmp 函数。
  • 类似strcmp,根据s是等于、大于还是小于参数指定的字符串,s.compare返回0,正数和负数。
  • compare有6个版本,根据我们是要比较两个string还是一个string与一个字符数组,参数各不相同。在这两种情况下,都可以比较整个或一部分字符串。
  • compare 函数的几种参数形式:

5.6 数值转换(int <=> string)

  • 字符串中常常包含表示数值的字符。例如,我们用两个字符的string表示数值15——字符'1'后跟字符'5'。一般情况,一个数的字符表示不同于其数值。
  • C++11增加了 string 和数值之间的转换函数:
int i=42;
string s=to_string(i);  //将整数i转换为字符表示形式
double d=stod(d);     //将字符串s转换为浮点数
  • string转换为数值的条件:string中第一非空白符必须是数值中可能出现的字符:
string s2="pi=3.14";
			
//转换s中以数字开始的第一个子串,结果d=3.14	
d=stod(s2.substr(s2.find_first_of("+-.0123456789)));
//调用了find_first_of来获得s2中第一个可能是数值的一部分的字符的位置
  • 进行数值转换时,string 参数的第一个非空白字符必须是符号(+ 或 -)或数字。它可以以 0x 或 0X 开头来表示十六进制数。对于转换目标是浮点值的函数,string 参数也可以以小数点开头,并可以包含 e 或 E 来表示指数部分。

  • 如果给定的 string 不能转换为一个数值,则转换函数会抛出 invalid_argument 异常。如果转换得到的数值无法用任何类型表示,则抛出 out_of_range 异常。

6. 容器适配器

  • 标准库定义了 stack、queue 和 priority_queue 三种容器适配器。容器适配器可以改变已有容器的工作机制。
  • 适配器是根据原始的容器类型所提供的操作,通过定义新的操作接口,来适应基础的容器类型
  • 适配器是标准库中的一个通用概念,容器、迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。例如stack适配器接受一种已有的容器类型(除array或forward_list外),并使其操作起来像一个stack一样
  • 所有容器适配器都支持的操作和类型:

6.1 定义一个适配器

  • 每个适配器都定义两个构造函数:
    • 默认的构造函数创建一个空的对象。
    • 接受一个容器的构造函数拷贝该容器来初始化适配器。**
// 假定deq是一个deque<int>,我们可以用deq来初始化一个新的stack
stack<int> stk(deq);  // 从deq拷贝元素到stk
  • 默认情况下,stack 和 queue 是基于 deque 实现的,priority_queue 是基于 vector 实现的
  • 可以在创建适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型
// 在vector上实现的空栈
stack<string, vector<string>> str_stk;
// strstk2在vector上实现,初始化时保存svec的拷贝
stack<string, vector<string>> str_stk2(svec);
  • 但每个容器适配器只可以使用自己的适配器操作,而不能使用底层容器类型的操作。
  • 对于一个给定的适配器,可以使用哪些容器是有限制的
    • 所有适配器都要求容器具有添加和删除元素的能力,因此适配器不能构造在 array 上。适配器还要求容器具有添加、删除和访问尾元素的能力,因此也不能用 forward_list 构造适配器。
    • stack 只要求 push_back、pop_back 和 back 操作,因此可以使用除 array 和 forward_list 之外的任何容器类型来构造。
    • queue 要求 back、push_back、front、push_front,因此它可以构建在 list 和 deque 之上,但不能基于 vector 构造。
    • priority_queue 除了 front、push_back、pop_back 操作之外还要求随机访问能力,因此可以构造于vector 或 queue 之上,但不能基于 list 构造。
  • 每个容器适配器都基于底层容器类型的操作定义自己的特殊操作,使用时只可以使用适配器操作,而不能使用底层容器类型的操作。
intStack.push(ix);    
/*
此语句试图在intStack的底层deque对象上调用push_back。
虽然stack是基于deque实现的,但我们不能直接使用deque操作。
不能在一个stack上调用push_back,而必须使用stack自己的操作:push
*/

6.2 栈适配器stack

  • 栈适配器stack定义在头文件stack中,其支持的操作如下:
stack<int> intStack;  //空栈:定义了一个保存整型元素的栈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();  //弹出栈顶元素
}

6.3 队列适配器queue

  • 队列适配器queue和priority_queue定义在头文件queue中,其支持的操作如下:

  • queue 使用先进先出(first-in,first-out,FIFO)的存储和访问策略。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除

6.4 优先权队列适配器priority_queue

  • priority_queue 允许我们为队列中的元素建立优先级。新加入的元素会安排在所有优先级比它低的已有元素之前。

  • 优先队列例子:饭店按照客人预订时间而不是到来时间来为他们安排座位。默认情况下,标准库在元素类型上使用 < 运算符来确定相对优先级。

posted @ 2021-05-31 13:13  夏目的猫咪老师  阅读(115)  评论(0编辑  收藏  举报