当前位置:   article > 正文

C++|C++ primer 第 9 章 顺序容器_定义一个list对象,其元素类型是int的deque

定义一个list对象,其元素类型是int的deque

        一个容器就是一些特定类型对象的集合。顺序容器(sequential container)为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。

9.1 顺序容器概述

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

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

        除了固定大小的 array 外,其他容器都提供高效、灵活的内存管理。

         现代 C++程序应该使用标准库容器,而不是更原始的数据结构,如内置数组。

 确定使用哪种顺序容器

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

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

  • 除非你有很好的理由选择其他容器,否则应使用 vector。
  • 如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用 list 或 forward_list。
  • 如果程序要求随机访问元素,应使用 vector 或 deque。
  • 如果程序要求在容器的中间插入或删除元素,应使用 list 或 forward_list。
  • 如果程序需要早头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用 deque。
  • 如果程序只有在读取输入时才需要在容器中间插入元素,随后需要随机访问元素,则

        首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地

向 vector 追加数据,然后再调用标准库的 sort 函数来重排容器中的元素,从而避免在中间位置添加元素。

        如果必须在中间位置插入元素,考虑在输入阶段使用 list,一旦输入完成,将 list 中的内容拷贝到一个 vector 中。

        如果程序既需要随机访问元素,又需要在容器中间位置插入元素,那该怎么办?答案取决于在 list 或 forward_list 中访问元素与 vector 或 deque 中插入/删除元素的相对性能。一般来说,应用中占主导地位的操作决定了容器类型的选择。

        如果你不确定应该使用哪种容器,那么你可以在程序中只使用 vector 和 list 公共的操作:使用迭代器,不使用下标操作,避免随机访问。这样,在必要时选择使用 vector 或 list 都很方便。

 9.1 节练习

 (a)“按字典序插入到容器中”意味着进行插入和排序操作,从而需要在容器内部频繁进行插入操作,vector 在尾部之外的位置插入和删除元素很慢,deque 在头尾之外的位置插入和删除元素很慢。而 list 在任何位置插入、删除速度都很快。因此,这个任务选择 list 更为何时。当然,如果不是必须边读取单词边插入到容器中,可以使用 vector ,将读入的单词一次追加到尾部,读取完毕后,调用标准库到排序算法将单词重排为词序。

(b)由于需要在头、为分别进行插入、删除操作,因此将 vector 排除在外,deque 和 list 都可以达到很好的性能。如果还需要频繁进行随机访问,则 deque 更好。

(c)由于整数占用空间很小,且快速的排序算法序频繁随机访问元素,将 list 排除在外。由于无须在头部进行插入、删除操作,因此使用 vector 即可,无须使用 deque。

 9.2 容器库概览

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

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

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

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

    vector<vector<string>> lines;    //vector 的 vector

此处 lines 是一个  vector,其元素类型是 string 的 vector。

        较旧的编译器可能需要在两个尖括号之间键入空格,例如

          vector<vector<string> > lines。

容器操作
类型别名
iterator此容器类型的迭代器类型
const_iterator可以读取元素,但不能修改元素的迭代器类型
size_type无符号整数类型,足够保存此中容器类型最大可能容器的大小
difference_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
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.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()返回 const_reverse_iterator

9.2 节练习

练习 9.2:定义一个 list 对象,其元素类型是 int 的 deque。

list<deque<int>> li;

9.2.1 迭代器

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

        其中,forward_list 不支持递减运算符。算术运算只能应用于 string、vector、deque 和 array的迭代器。

迭代器范围

        一个迭代器范围(iterator range)由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置(one past the last element)。这两个迭代器分别被称为 begin 和 end,或者是 first 和 last(可能有些误导),它们标记了容器中元素的一个范围。

        虽然第二个迭代器常常被称为 last ,但这种叫法有些误导,因为第二个迭代器从来都不会指向范围中的最后一个元素,而是指向尾元素之后的位置。迭代器范围中的元素包含 first 所表示的元素以及从 irst 开始直至 last(但不包含 last)之间的所有元素。

        这种元素范围被称为左闭合区间(left-inclusive interval),其标准数学描述为:

(begin, end)

 表示范围自 begin 开始,于 end 之前结束。迭代器 begin 和 end 必须指向相同的容器。end 可以与 begin 还想相同的位置,但不能指向 begin 之前的位置。

使用左闭合范围蕴含的变成假定

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

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

9.2.1 节练习

练习 9.3:构成迭代器范围的迭代器有何限制?

        两个迭代器 begin 和 end 必须指向同一个容器中的元素,或者是容器最后一个元素之后的位置;而且,对 begin 反复进行递增操作,可保证达到 end,即 end 不在 begin 之前。

  1. //练习9.4:编写函数,接受一对指向 vector<int> 的迭代器和一个 int 值。
  2. //在两个迭代器指定的范围中查找给定的值,返回一个布尔值来指出是否找到。
  3. bool findTarget(vector<int>::iterator begin, vector<int>::iterator end, int target)
  4. {
  5. while (begin != end)
  6. {
  7. if (*begin == target)
  8. {
  9. return true;
  10. }
  11. ++begin;
  12. }
  13. return false;
  14. }
  1. //练习 9.5:重写上一题的函数,返回一个迭代器指向找到的元素。
  2. vector<int>::iterator findTarget1(vector<int>::iterator begin, vector<int>::iterator end, int target)
  3. {
  4. while (begin != end)
  5. {
  6. if (*begin == target)
  7. {
  8. return begin;
  9. }
  10. ++begin;
  11. }
  12. return end;
  13. }

练习 9.6:下面程序有何错误?你应该如何修改它?

  1. list<int> lst1;
  2. list<int>::iterator iter1 = lst1.begin(), iter2 = lst1.end();
  3. while(iter1 < iter2) /*...*/

        list 不支持 < 运算,只支持递增、递减、== 以及 != 运算

 9.2.2 容器类型成员

        每个容器都定义了多个类型。我们已经使用过其中三种:size_type、iterator 和 const_iterator。

        除了已经使用过的迭代器类型,大多数容器还提供反向迭代器。对一个反向迭代器执行++操作,会得到上一个元素。

        剩下的就是类型别名了,通过类型别名,我们可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的 value_type。如果需要元素类型的一个引用,可以使用 reference 或 const_reference。

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

  1. list<string>::iterator iter;
  2. vector<int>::difference_type count;

9.2.2 节练习

练习 9.7:为了索引 int 的 vector 中的元素,应该使用什么类型?

vector<int>::iterator

 练习 9.8:为了读取 string 的 list 中的元素,应该使用什么类型?如果写入 list,又该使用什么类型?

list<string>::value_type        元素类型

list<string>::reference         引用类型

 9.2.3 begin 和 end 成员

        begin 和 end 有多个版本:带 r 的版本返回反向迭代器;带 c 开头的版本则返回 const 迭代器。

        当 auto 与 begin 或 end 结合使用时,获得的迭代器类型依赖于容器类型。但以 c 开头的版本还是可以获得 const_iterator 的,而不管容器的类型是什么。

9.2.3 节练习

练习 9.9:begin 和 cbegin 两个函数有什么不同?

        cbegin 是 C++新标准引入的,用来与 auto 结合使用。它返回指向容器第一个元素的 const 迭代器,可以用来只读地访问容器元素,但不能对容器元素进行修改。因此,当不需要写访问时,应该使用 cbegin。

        begin 则是被重载过的,有两个版本:其中一个是const 成员函数,也返回 const 迭代器;另一个则返回普通迭代器,可以对容器元素进行修改。

 练习 9.10:下面四个对象分别是什么类型?

  1. vector<int> v1;
  2. const vector<int> v2;
  3. auto it1 = v1.begin(), it2 = v2.begin();
  4. auto it3 = v1.cbegin(), it4 = v2.cbegin();

 

 9.2.4 容器定义与初始化

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

容器定义和初始化
C c;默认构造函数。如果 C 是一个 array,则 c 中元素按默认方式初始化;否则 c 为空

C c1(c2)

C c1 = c2

c1初始化为 c2 的拷贝。c1 和 c2 必须是相同类型(即,它们必须是相同的容器类型,且保存的是相同的元素类型;对于 array 类型,两者还必须具有相同大小)

C c{a,b,c...}

C c={a,b,c...}

c 初始化为初始化列表中元素的拷贝。列表中元素的类型必须与 C 的元素类型相容。对于 array 类型,列表中元素数目必须等于或小于 array 的大小,任何遗漏的元素都进行值初始化(3.3.1节)
C c(b, e)c 初始化为迭代器 b 和 e 指定范围中的元素的拷贝。范围中元素的类型必须与 C 的元素类型相容(array 不适用)
只有顺序容器(不包括 array )的构造函数才能接受大小参数
C seq(n)seq 包含 n 个元素,这些元素进行了值初始化;此构造函数是 explicit 的(string)不适用
C seq(n, t)seq 包含 n 个初始化为值 t 的元素

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

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

 列表初始化

        在新标准中,我们可以对一个容器进行列表初始化

  1. //每个容器有三个元素,用给定的初始化器进行初始化
  2. list<string> authors = {"hhh", "Tom", "Anna"};
  3. vector<const char*> articles = {"a", "b", "the"};

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

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

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

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

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

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

标准库 array 具有固定大小

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

  1. array<int, 42> //类型为:保存 42个 int 的数组
  2. array<string, 10> //类型为:保存 10 个 string 的数组

        值得注意的是,虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但 array 并无此限制。与其他容器一样,array 也要求初始值的类型必须与要创建的容器类型相同。此外,array 还要求元素类型的大小也都一样,因为大小是 array 类型的一部分。

9.2.4 节练习

  1. //练习 9.11:对 6 种创建和初始化 vector 对象 的方法,每一种都给出一个实例。
  2. //解释每个 vector 包含什么值
  3. vector<int> v1;
  4. vector<int> v2 = v1;
  5. vector<int> v2(v1);
  6. vector<string> svec = { "hi","hhh","oo" };
  7. vector<string> svce2{ "ggg" };
  8. vector<string> svec3(svec.begin(), svec.end());
  9. vector<int> v3(10);
  10. vector<int> v4(19, 2);

练习 9.12:对于接受一个容器创建其拷贝的构造函数,和接受两个迭代器创建拷贝的构造函数,解释它们的不同。

        接受一个已有容器的构造函数会拷贝此容器中的所有元素,这样,初始化完后才能后,我们得到此容器的一个一模一样的拷贝。当我们确实需要一个容器的完整拷贝时,这种初始化方式非常方便。

        但当我们不需要已有容器中的全部元素,而只是想拷贝其中一部分元素时,可以使用接受两个迭代器的构造函数。传递给它要拷贝的范围的起始和尾后位置的迭代器,即可令新容器对象包含所需范围中元素的拷贝。

 练习 9.13:如何从一个 list<int>初始化一个vector<double>?从一个 vector<int>又该如何创建?编写代码验证你的答案。

  1. list<int> ilist = { 1,2,3,4,5,6,7 };
  2. vector<int> ivec = { 6,5,4,3,3,2,1 };
  3. //容器类型不同,不能使用拷贝初始化
  4. //vector<double> v(ilist);
  5. //元素类型相容,因此可采用范围初始化
  6. vector<double> v1(ilist.begin(), ilist.end());
  7. //容器类型不同,不能使用拷贝初始化
  8. //vector<double> dvec(ivec);
  9. //元素类型相容,因此可采用范围初始化
  10. vector<double> dvec1(ivec.begin(), ivec.end())

9.2.5 赋值和 swap

        下表中列出的与赋值相关的运算符可用于所有容器。

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

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

 使用 assign(仅顺序容器)

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

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

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

        由于其旧元素被替换,incident传递给assign 的迭代器不能指向调用 assign 的容器。

 使用 swap

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

  1. vector<string> svec1(10); //10 个元素的 vector
  2. vector<string> svec2(24); //24 个元素的 vector
  3. swap(svec1, svec2);

调用 swap 后,svec1 将包含24 个 string 元素, svec2 将包含10个 string 元素。除 array 外。交换两个容器内容的操作保证会很快——元素本身并未交换,swap 只是交换了两个容器的内部数据结构。

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

        元素不会移动的事实意味着,除 string 外,指向容器的迭代器、引用和指针在 swap 操作之后都不会失效。它们仍指向 swap 操作之前所指向的那些元素。但是,在 swap 之后,这些元素以及属于不同的容器了。例如,假定 iter 在 swap 之前指向 svec1[3] 的 string ,那么在 swap 之后它指向 svec2[3] 的元素。与其他容器不同,对一个 string 调用 swap 会导致迭代器、引用和指针失效。

        与其他容器不同,交换两个 array 会真正交换它们的元素。因此,交换两个 array 所需的时间与 array 中元素的数目成正比。

        因此,对于 array,在 swap 操作之后,引用、指针和迭代器所绑定的元素保持不变,但元素值以及与另一个 array 中对应元素的值进行了交换。

9.2.5 节练习

  1. //练习9.14:编写程序,将一个 list 中的 char* 指针(指向 C 风格字符串)元素赋值给一个 vector 中的 string
  2. void f()
  3. {
  4. list<const char *> clist = {"hi","world","!"};//加上 const 才正确
  5. vector<string> svec;
  6. svec.assign(clist.begin(), clist.end());//使用范围赋值
  7. }

9.2.6 容器大小操作

        除了一个例外,每个容器都有三个与大小相关的操作。成员 size 返回容器中元素的数目;empty 当 size 为 0 时返回布尔值 true,否则返回 false ;size_max 返回一个大于或等于该类型容器所能容纳的最大元素数的值。forward_list 支持 max_size 和 empty ,但不支持 size。

9.2.7 关系运算符

        每个容器类型都支持相等运算符(== 和 !=);除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。即,我们只能将一个 vector<int> 与另一个 vector<int> 进行比较,而不能将一个 vector<int> 与一个 list<int> 或一个 vector<double> 进行比较。

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

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

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

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

        如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。

9.2.7 节练习

  1. //练习 9.15:编写程序,判断两个 vector<int> 是否相等
  2. bool isEqual(const vector<int> &v1,const vector<int> &v2)
  3. {
  4. if (v1 == v2)
  5. {
  6. return true;
  7. }
  8. return false;
  9. }
  1. //练习 9.16:重写上一题的程序比较一个 list<int> 中的元素和一个 vector<int> 中的元素。
  2. bool l_v_equal(const list<int> &ilist, const vector<int> &ivec)
  3. {
  4. if (ilist.size() != ivec.size())
  5. {
  6. return false;
  7. }
  8. auto lb = ilist.cbegin();
  9. auto le = ilist.cend();
  10. auto vb = ivec.cbegin();
  11. for (; lb != le; ++lb,++vb)
  12. {
  13. if (*lb != *vb)
  14. {
  15. return false;
  16. }
  17. }
  18. return true;
  19. }

练习 9.17:假定 c1 和 c2 是两个容器,下面的比较操作有何限制(如果有的话)?

if(c1 < c2)

        首先,容器类型必须相同,元素类型也必须相同。

        其次,元素类型必须支持 < 运算符。

 9.3 顺序容器操作

        顺序容器和关联容器的不同之处在于两者组织元素的方式。

9.3.1 向顺序容器添加元素

        除 array 外,所有标准容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小。

向顺序容器添加元素的操作

这些操作会改变容器的大小;array 不支持这些操作。

forward_list 有自己专有版本的 insert 和 emplace;

forward_list 不支持 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 插入元素会使所有指向容器的迭代器、引用和指针失效。

使用 push_back

        

 使用 push_front

        

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

        

 插入范围内元素

        

 使用 insert 的返回值

        

 使用 emplace 操作

        新标准引入了三个新成员——emplace_front、emplace 和 emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应 push_front、insert 和 push_back,允许我们将元素防止在容器头部、一个指定位置之前或容器尾部。

        当调用 push 或 insert 成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个 emplace 成员函数时,则是将参数传递给元素类型的构造函数。emplace 成员使用这些参数在容器管理的内存空间中直接构造元素。例如,假定 c 保存 Sales_data 元素:

  1. //在 c 的末尾构造一个 Sales_data 对象
  2. //使用三个参数的 Sales_data 构造函数
  3. c.emplace_back("98776-6565", 25, 15.19);
  4. //错误:没有接受三个参数的 push_back 版本
  5. c.push_back("98776-6565", 25, 15.19);
  6. //正确:创建一个临时的Sales_data对象传递给 push_back
  7. c.push_back(Sales_data("98776-6565", 25, 15.19));

        其中对 emplace_back 的调用和第二个 push_back 调用都会创建新的 Sales_data 对象。在调用 emplace_back 时,会在容器管理的内存空间中直接创建对象。而调用 push_back 则会创建一个局部临时对象,并将其压入容器中。

9.3.1 节练习

  1. //练习 9.18:编写程序,从标准输入读取 string 序列,存入一个 deque 中。
  2. //编写一个循环,用迭代器打印 deque 中的元素。
  3. void print()
  4. {
  5. string word;
  6. deque<string> d;
  7. while (cin >> word)
  8. {
  9. d.push_back(word);
  10. }
  11. for (auto it = d.begin(); it != d.end(); ++it)
  12. {
  13. cout << *it << endl;
  14. }
  15. }
  1. //练习 9.19:重写上一题的程序,用list 代替deque
  2. void printList()
  3. {
  4. string word;
  5. list<string> slist;
  6. while (cin >> word)
  7. {
  8. slist.push_back(word);
  9. }
  10. for (auto it = slist.begin(); it != slist.end(); ++it)
  11. {
  12. cout << *it << endl;
  13. }
  14. }
  1. //练习9.20:编写程序,从一个 list<int> 拷贝元素到两个 deque 中。
  2. //值为偶数的所有元素都拷贝到另一个 deque 中,而奇数值元素都拷贝到另一个 deque 中
  3. void copyList()
  4. {
  5. list<int> ilist = { 1,2,3,4,5,6,7,8,9,5 };
  6. deque<int> even_d;
  7. deque<int> odd_d;
  8. for (auto it = ilist.cbegin(); it != ilist.cend(); ++it)
  9. {
  10. if (*it % 2 == 0)
  11. {
  12. even_d.push_back(*it);
  13. }
  14. odd_d.push_back(*it);
  15. }
  16. }
  1. //练习 9.21:如果我们将 308 页中使用 insert 返回值将元素添加到 list 中的循环程序改写为将元素插入到 vector 中,分析循环将如何工作
  2. void f()
  3. {
  4. vector<string> svec;
  5. string word;
  6. auto it = svec.begin();
  7. while (cin >> word)
  8. {
  9. it = svec.insert(it, word);
  10. }
  11. //用 cbegin() 获取 vector 首元素迭代器,遍历 vector 中所有元素
  12. for (auto it = svec.cbegin(); it != svec.cend(); ++it)
  13. {
  14. cout << *it << endl;
  15. }
  16. }

练习 9.22:假定 iv 是一个 int 的 vector ,下面的程序存在什么错误?你将如何修改?

 

 9.3.2 访问元素

        包括 array 在内的顺序容器都有一个 front 成员函数,而除 forward_list 之外的所有顺序容器都有一个 back 成员函数。这两个操作分别返回首元素和尾元素的引用。

在顺序容器中访问元素的操作

at 和 下标操作只适用于 string、vector、deque、和 array。

back 不适用于 forward_list。

c.back()返回 c 中尾元素的引用。若 c 为空,函数行为未定义
c.front()返回 c 中首元素的引用。若 c 为空,函数行为未定义
c[n]返回 c z中下标为 n  的元素的引用, n 是一个无符号整数。若 n>= c.size(),则函数行为未定义
c.cat[n]返回下标为 n 的元素的引用。如果下标越界,则抛出一 out_of_range 异常
对一个空容器调用 front 和 back,就像使用一个越界的下标一样,是一种严重的程序设计错误

访问成员函数返回的是引用

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

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

与往常一样,如果我们使用 auto 变量来保存这些函数的返回值,并且希望使用此变量来改变元素的值,必须记得将变量定义为引用类型。

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

        提供快速随机访问的容器(string、vector、deque 和 array)也都提供下标运算符。下标运算符接受一个下标参数,返回容器中该位置的元素的引用。给定下标必须在“范围内”(即,大于等于 0,且小于容器的大小)。保证下标有效是程序员的责任,下标运算符并不检查下标是否在合法范围内。使用越界的下标是一种严重的程序设计错误,而且编译器并不检查这种错误

        如果我们希望确保下标是合法的,可以使用 at 成员函数。at 成员函数类似下标运算符,但如果下标越界,at 会抛出一个 out_of_range 异常。

9.3.2 节练习

  1. //练习9.24:编写程序,分别使用 at、下标运算符、front 和 begin 提取一个 vector 中的第一个元素。
  2. //在一个空 vector 上测试你的程序
  3. void fun()
  4. {
  5. vector<int> ivec;
  6. cout << ivec.at(0)<< endl;
  7. cout << ivec[0] << endl;
  8. cout << ivec.front() << endl;
  9. cout << *(ivec.begin()) << endl;
  10. }

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指向尾后元素,则返回尾后(off-the-end)迭代器。若 p 是尾后迭代器,则函数行为未定义
c.erase(b, e)删除迭代器 b 和 e 所指定范围内的元素。返回一个指向最后一个被删元素之后元素的迭代器,若 e 本身就是尾后迭代器,则函数也返回尾后迭代器
c.clear()删除 c 中所有元素,返回 void
删除 deque 中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向 vector 和 string 中删除点之后位置的迭代器、引用和指针都会失效。

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

 pop_front 和 pop_back 成员函数

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

        这些操作返回 void 。如果你需要弹出的元素的值,就必须在执行弹出操作之前保存它。

从容器内部删除一个元素

        成员 erase 从容器中指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的 erase 都返回指向删除的(最后一个)元素之后位置的迭代器。即,若 j 是 i 之后的元素,那么 erase(i) 将返回指向 j 的迭代器。

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

  1. //循环删除 list 中的奇数元素
  2. void eraseOddNumber()
  3. {
  4. list<int > ilist = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  5. auto it = ilist.begin();
  6. while (it != ilist.end())
  7. {
  8. if (*it % 2) //若元素是奇数
  9. {
  10. it = ilist.erase(it); //删除此元素,并将 it 设置为所删除元素之后的元素
  11. }
  12. else
  13. {
  14. ++it;
  15. }
  16. }
  17. }

删除多个元素

        接受一对迭代器的 erase 版本允许我们删除一个范围内的元素。

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

         elem1 指向我们要删除的第一个元素,elem2 指向我们要删除的最后一个元素之后的位置。 

        为了删除一个容器中的所有元素,我们既可以调用 clear,也可以用 begin 和 end 获得的迭代器作为参数调用 erase。

9.3.3 节练习

练习 9.25:对于上述的程序,如果 elem1 与 elem2 相等会发生什么?如果 elem2 是尾后迭代器,或者 elem1 和 elem2 皆为尾后迭代器,又会发生什么?

        如果 elem1 和 elem2 相等,则什么也不会发生,容器保持不变。哪怕两个迭代器是指向尾后位置(例如 end()+1)也是如此,程序也不会出错。

        因此 elem1 和 elem2 都是尾后迭代器时,容器保持不变。

        如果 elem2 为尾后迭代器,elem1 指向之前的合法位置,则会删除从 elem1 开始直至容器末尾的所有元素。

  1. //练习 9.26:使用下面代码定义的 ia,将 ia 拷贝到一个 vector 和一个 list 中。
  2. //使用单迭代器版本的 erase 从 list 中删除奇数元素,从 vector 中删除偶数元素。
  3. void f1()
  4. {
  5. int ia[] = { 0,1,2,3,8,13,21,55,89 };
  6. list<int> il;
  7. vector<int> iv;
  8. iv.assign(ia, ia + 11);
  9. il.assign(ia, ia + 11);
  10. auto iiv = iv.begin();
  11. while (iiv != iv.end())
  12. {
  13. if (!(*iiv & 1))//偶数
  14. {
  15. iiv = iv.erase(iiv);
  16. }
  17. else
  18. {
  19. ++iiv;
  20. }
  21. }
  22. auto iil = il.begin();
  23. while (iil != il.end())
  24. {
  25. if ((*iil & 1))//奇数
  26. {
  27. iil = il.erase(iil);
  28. }
  29. else
  30. {
  31. ++iil;
  32. }
  33. }
  34. }

9.3.4 特殊的 forward_list 操作

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

        由于这些操作与其他容器上的操作的实现方式不同,forward_list 并未定义 insert、emplace 和 erase,而是定义了名为 insert_after、emplace_after 和 erase_after 的操作。为了支持这些操作,forward_list 也定义了 before_begin,它返回一个首前(off-the-begining)迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素“之后”添加或删除元素(即在链表首元素之前添加删除元素)。

在 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 是尾后迭代器,则函数行为未定义
lst.insert_after(p, n, t)
lst.insert_after(p, b, e)
lst.insert_after(p, il)
emplace_after(p, args)使用 args 在 p 指定的位置之后创建一个元素。返回一个指向这个新元素的迭代器。若 p 为尾后迭代器,则函数行为未定义
lst.erase_after(p)删除 p 指向的位置之后的元素,或删除从 b 之后直到(但不包含)e 之间的元素。返回一个指向被删元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代器。如果 p 指向 lst 的尾元素或者是一个尾后迭代器,则函数行为未定义
lst.erase_after(b, e)

          当在 forward_list 中添加或删除元素时,我们必须关注两个迭代器——一个指向我们要处理的元素,另一个指向其前驱。

  1. //从 forward_list 中删除元素
  2. void f1()
  3. {
  4. forward_list<int> flst = { 0,1,2,3,4,5,6,7,8,9,10 };
  5. auto prev = flst.before_begin(); //表示 flst 的“首前元素”
  6. auto curr = flst.begin(); //表示 flst 中的第一个元素
  7. while (curr != flst.end()) //仍有元素要处理
  8. {
  9. if (*curr % 2)
  10. curr = flst.erase_after(prev); //删除它并移动 prve
  11. else
  12. {
  13. prev = curr; //移动迭代器 curr,指向下一个元素, prve 指向
  14. ++curr; //curr 之前的元素
  15. }
  16. }
  17. }

 9.3.4 节练习

  1. //练习 9.27:编写程序,查找并删除 forward_list<int> 中的奇数元素
  2. void f2()
  3. {
  4. forward_list<int> flst = { 0,1,2,3,4,5,6,7,8,9,10 };
  5. auto prev = flst.before_begin(); //表示 flst 的“首前元素”
  6. auto curr = flst.begin(); //表示 flst 中的第一个元素
  7. while (curr != flst.end()) //仍有元素要处理
  8. {
  9. if (*curr & 1) //奇数
  10. curr = flst.erase_after(prev); //删除它并移动 prve
  11. else
  12. {
  13. prev = curr; //移动迭代器 curr,指向下一个元素, prve 指向
  14. ++curr; //curr 之前的元素
  15. }
  16. }
  17. for (curr = flst.begin(); curr != flst.end(); curr++)
  18. {
  19. cout << *curr << " ";
  20. }
  21. cout << endl;
  22. }
  1. //练习 9.28:编写函数,接受一个 forward_list<string> 和两个 string 共三个参数。
  2. //函数应在链表中查找第一个 string,并将第二个 string 插入到紧接着第一个 string之后的位置。
  3. //若第一个 string 未在链表中,则将第二个 string 插入到链表末尾
  4. void f(forward_list<string> sl, string s1, string s2)
  5. {
  6. auto prev = sl.before_begin();
  7. auto curr = sl.begin();
  8. bool inserted = false;
  9. while (curr != sl.end())
  10. {
  11. if (*curr == s1)
  12. {
  13. curr = sl.insert_after(curr, s2);//插入新字符串,curr 指向它
  14. inserted = true;
  15. }
  16. prev = curr;
  17. ++curr;
  18. }
  19. if (!inserted)
  20. {
  21. sl.insert_after(prev, s2);
  22. }
  23. }

9.3.5 改变容器大小

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

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

如果 resize 缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对 vector、string 或 deque 进行 resize 可能导致迭代器、指针和 引用失效。

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

9.3.5 节练习

练习 9.29:假定 vec 包含 25 个元素,那么 vec.resize(100)会做什么?如果接下来调用 vec.resize(10)会做什么?

        调用 vec.resize(100) 会向 vec 末尾添加 75 个元素,这些元素将进行值初始化。

        接下来调用 vec.resize(10) 会将 vec 末尾的 90 个元素删除。

 练习 9.30:接受单个参数的 resize 版本对元素类型有什么限制(如果有的话)?

        对于元素是类类型,则单参数 resize 版本要求该类型必须提供一个默认构造函数。

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

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

        在向容器添加元素后:

  • 如果容器是 vector 或 string ,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。
  • 对于 deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
  • 对于 list 和 forward_list ,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。

        当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效,这应该不会令人惊讶。毕竟,这些元素都已经被销毁了,当我们删除一个元素后:

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

注意:当我们删除元素时,尾后迭代器总是会失效。使用失效的迭代器、指针或引用是严重的运行时错误。

编写改变容器的循环程序

        添加 / 删除 vector、string 或 deque 元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是 insert 或 erase,那么更新迭代器很容易,这些操作都返回迭代器。

  1. //傻瓜循环,删除偶数元素,复制每个奇数元素
  2. vector<int> vi = { 0,1,2,3,4,5,6,7,8,9 };
  3. auto it = vi.begin();
  4. while (it != vi.end())
  5. {
  6. if (*it % 2)
  7. {
  8. it = vi.insert(it, *it); //复制当前元素
  9. it += 2; //向前移动迭代器,跳过当前元素以及插入到它之前的元素
  10. }
  11. else
  12. {
  13. it = vi.erase(it); //删除偶数元素
  14. //不向前移动迭代器,it 指向我们删除的元素之后的元素
  15. }
  16. }

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

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

不要保存 end 返回的迭代器

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

        如果在一个循环中插入 / 删除 deque、string 或 vector 中的元素,不要缓存 end 返回的迭代器。

 9.3.6 节练习

练习 9.31:上面删除偶数值元素并复制奇数值元素的程序不能用于 list 或 forward_list 。为什么?修改程序,使之也能用于这些类型。

        因为 list 和 forward_list 的迭代器不支持加减运算。应多次调用 ++ 来实现与迭代器加法相同的效果。

  1. //list
  2. void f3()
  3. {
  4. list<int> vi = { 0,1,2,3,4,5,6,7,8,9 };
  5. auto it = vi.begin();
  6. while (it != vi.end())
  7. {
  8. if (*it % 2)
  9. {
  10. it = vi.insert(it, *it); //复制当前元素
  11. it++; it++; //向前移动迭代器,跳过当前元素以及插入到它之前的元素
  12. }
  13. else
  14. {
  15. it = vi.erase(it); //删除偶数元素
  16. //不向前移动迭代器,it 指向我们删除的元素之后的元素
  17. }
  18. }
  19. }
  1. //forward_list
  2. void f3()
  3. {
  4. forward_list<int> iflst = { 0,1,2,3,4,5,6,7,8,9 };
  5. auto prev = iflst.begin();
  6. auto curr = iflst.begin();
  7. while (curr != iflst.end())
  8. {
  9. if (*curr % 2)
  10. {
  11. curr = iflst.insert_after(curr, *curr); //复制当前元素
  12. prev = curr; // prev 移动到新插入元素
  13. curr++; //curr 移动到下一元素
  14. }
  15. else
  16. {
  17. curr = iflst.erase_after(prev); //删除偶数元素,curr 指向下一元素
  18. //不向前移动迭代器,it 指向我们删除的元素之后的元素
  19. }
  20. }
  21. }

练习 9.32:在上面的程序中,像下面语句这样调用 insert 是否合法?如果不合法,为什么?

it = vi.insert(it, *it++);

 

 练习 9.34:假定 vi 是一个保存  int 的容器,其中有偶数值也有奇数值,分析下面循环的行为,然后编程验证你的分析是否正确。

  1. vector<int> vi;
  2. auto iter = vi.begin();
  3. while (iter != vi.end())
  4. if (*iter % 2)
  5. iter = vi.insert(iter, *iter);
  6. ++iter;

        这段代码第一个错误是忘记使用花括号,使得 ++iter 编程循环后的第一条语句,而非所期望的最后一条语句。因此,除非容器为空,否则程序会陷入死循环:

        1. 若容器的第一个元素是偶数,布尔表达式为假,if 语句真分支不会被执行,iter 保持不变。循环继续执行,真分支仍然不会执行,iter 继续保持不变,如此陷入死循环。

        2. 若容器的第一个元素是奇数,insert 语句被调用,将该值插入到首元素之前,并将返回的迭代器(指向新插入元素)赋予 iter,因此 iter 指向新首元素。继续执行循环,会继续将首元素复制到容器首位置,并令 iter 指向它,如此陷入死循环。

 9.4 vector 对象是如何增长的

        当不得不获取新的内存空间时,vector 和 string 的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可用来保存更多的新元素。这样,就不需要每次添加新元素都重新分配容器的内存空间了。

管理容量的成员函数

        

容器大小操作

shink_to_fit 只适用于 vector、string 和 deque

capacity  和 reserve 只适用于 vector 和 string

c.shrink_to_fit()请将 capacity() 减少为与 size() 相同大小
c.capacity()不重新分配空间的话,c 可以保存多少元素
c.reserve(n)分配至少能容纳 n 个元素的内存空间

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

         只有当需要的内存空间超过当前容量时,reserve 调用才会改变 vector 的容量。如果需求大小小于或等于当前容量,reserve 什么也不做。特别是,当需求大小小于当前容量时,容器不会退回内存空间。因此,在调用 reserve 之后,capacity 将会大于或等于传递给 reserve 的参数。

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

        在新标准中,我们可以调用 shrink_to_fit 来要求 deque、vector 或 string 退回不需要的内存空间。此函数指出我们不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求。也就是说,调用 shrink_to_fit 也并不保证一定退回内存空间。

capacity 和 size

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

9.4 节练习

练习 9.37:为什么 list 或 array 没有 capacity 成员函数?

        list 是链表,当有新元素加入时,会从内存空间中分配一个新节点保存它;当从链表中删除元素时,该节点占用的内存空间会被立刻释放。因此,一个链表占用的内存空间总是与它当前保存的元素所需空间相等(换句话说,capacity 总是等于 size)。

        而 array 是固定大小数组,内存一次性分配,大小不变,不会变化。

        因此它们均不需要 capacity。

 9.5 额外的 string 操作

        除了顺序容器共同的操作之外,string 类型还提供了一些额外的操作。

9.5.1 构造 string 的其他方法

构造 string 的其他方法
n、len2 和 pos2 都是无符号数
string(cp, n)s 是 cp 指向的数组中前 n 个字符的拷贝。此数组至少应该包含 n 个字符
string(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 时,还可以给定一个下标来指出从哪里开始拷贝:

  1. const char *cp = "hello world!!!"; //以空字符结束的数组
  2. char noNull[] = { 'h','i' }; //不是以空字符结束
  3. string s1(cp); //拷贝 cp 中的字符直到遇到空字符;s1 == "hello world!!!"
  4. string s2(noNull, 2); //从 noNull 拷贝两个字符;s2 == "hi"
  5. string s3(cp + 6, 5); //从 cp[6] 开始拷贝 5 个字符;s4 == "world"
  6. string s5(s1, 6, 5); //从 s1[6] 开始拷贝 5 个字符;s5 == "world"
  7. string s6(s1, 6); //从 s1[6] 开始拷贝,直至 s1 末尾;s6 == "world!!!"
  8. string s7(s1, 6, 20); //正确,只拷贝到 s1 末尾;s7 == "world!!!"
  9. string s8(s1, 6); //抛出一个 out_of_range 异常

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

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

substr 操作

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

        substr 操作返回一个 string ,它是原始 string 的一部分或全部的拷贝。可以传递给 substr 一个可以选择的位置和计数值:

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

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

9.5.1 节练习

练习 9.41:编写程序,从一个 vector<char> 初始化一个 string。

  1. vector<char> vc = { 'h','e','l','l' };
  2. string s(vc.begin(),vc.end());
  3. string s1(vc.data(), vc.size());
  4. cout << s << endl;
  5. cout << s1 << endl;

练习 9.42:假定你希望每次读取一个字符存入一个 string 中,而且知道最少需要读取 100 个字符,应该如何提高程序的性能?

        先用 reserve 为 string 分配 100 个字符的空间,然后逐个读取字符,用 push_back 添加到 string 末尾。

9.5.2 改变 string 的其他方法

        string 类型支持顺序容器的赋值运算以及 assign、insert 和 erase 操作。除此之外,它还定义了额外的 insert 和 erase 版本。

        除了接受迭代器的 insert 和 erase 版本外,string 还提供了接受下标的版本。下标指出了开始删除的位置,或是 insert 到给定值之前的位置:

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

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

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

此处,我们首先通过调用 assign 替换 s 的内容。我们赋予 s 的是从 cp 指向的地址开始的 7 个字符。要求赋值的字符数必须小于或等于 cp 指向的数组中的字符数(不包括结尾的空字符)。

        接下来在 s 上调用 insert,我们的意图是将字符插入到 s[size()] 处(不存在的)元素之前的位置。在此例中,我们将 cp 开始的 7 个字符(至多到结尾空字符之前)拷贝到 s 中。

        我们也可以指定将来自其他 string 或字符串的字符插入到当前 string 中赋予当前 string:

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

append 和 replace 函数

        string 类定义了两个额外的成员函数:append 和 replace,这两个函数可以改变 string 的内容。

        append 操作是在 string  末尾进行插入操作的一种简写形式。

  1. string s("C++ Primer"), s2 = s;
  2. s.insert(s.size(), " 4th ED."); // s == "C++ Primer 4th ED."
  3. s2.append(" 4th ED."); // s2 == "C++ Primer 4th ED." 等价方法 s == s2

        replace 操作是调用 erase 和 insert 的一种简写形式。

  1. //将 "4th" 替换为 "5th" 的等价方法
  2. s.erase(11, 3); // s = "C++ Primer ED."
  3. s.insert(11, "5th"); // s == "C++ Primer 5th ED."
  4. //从位置 11 开始,删除 3 个字符并插入"5th"
  5. s.replace(11, 3, "5th"); // 等价方法: s == s2
修改 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, lenstr 中从 pos 开始最多 len 个字
cp, len从 cp 指向的字符数组的前(最多)len 个字符
cpcp 指向的以空字符结尾的字符数组
n, c n 个字符 c
b, e迭代器 b 和 e 指定的范围内的字符
初始化列表花括号包围的,以逗号分隔的字符列表
replace 和 insert 所允许的 args 形式依赖于 range 和 pos 是如何指定的。

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
初始化列表

改变 string 的多种重载函数

        上表列出的 append 、assign、insert 和 replace 函数有多个重载版本。

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

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

9.5.2 节练习

  1. //练习 9.43:编写一个函数,接受三个 string 参数 s、oldVal、newVal。使用迭代器及
  2. // insert 和 erase 函数将 s 中所有 oldVal 替换为 newVal。测试你的程序,用它替换
  3. //通用的简写形式,如,将 "tho" 替换为"though",将 "thru" 替换为 "through"
  4. void replace_string(string &s,const string &oldVal,const string &newVal)
  5. {
  6. string:size_t pos = 0;
  7. while (pos < s.length())
  8. {
  9. pos = s.find(oldVal, pos); //从 0 号位置开始查找
  10. if (pos >= s.length()) break; //如果没找到退出循环
  11. s.erase(pos, oldVal.length()); //如果找到,先删除原来的字符
  12. s.insert(pos, newVal); //插入新字符
  13. pos = pos + newVal.length(); //移动 pos 位置
  14. }
  15. cout << s << endl;
  16. }
  17. //使用迭代器
  18. void replace_string1(string &s, const string &oldVal, const string &newVal)
  19. {
  20. auto it = oldVal.size();
  21. if (!it)
  22. {
  23. return; //要查找的字符为空
  24. }
  25. auto iter = s.begin();
  26. while (iter <= s.end() - 1) //末尾少于 oldVal 长度的部分无须检查
  27. {
  28. auto iter1 = iter;
  29. auto iter2 = oldVal.begin();
  30. // s 中 iter 开始的子串必须每个字符都与 oldVal 相同
  31. while (iter2 != oldVal.end() && *iter1 == *iter2)
  32. {
  33. iter1++;
  34. iter2++;
  35. }
  36. if (iter2 == oldVal.end())// oldVal 耗尽——字符串相等
  37. {
  38. iter = s.erase(iter, iter1);//删除 s 中与 oldVal 相等部分
  39. if (newVal.size()) //替换子串是否为空
  40. {
  41. iter2 = newVal.end(); //由后至前逐个插入 newVal 中的字符
  42. do
  43. {
  44. iter2--;
  45. iter = s.insert(iter, *iter2);
  46. } while (iter2 > newVal.begin());
  47. }
  48. iter += newVal.size(); //迭代器移动到新插入内容之后
  49. }
  50. else iter++;
  51. }
  52. }
  53. //调用函数
  54. int main()
  55. {
  56. string s = "tho thru tho!";
  57. replace_string(s, "thru", "through");
  58. return 0;
  59. }

  1. //练习 9.44:重写上一题的函数,这次使用一个下标和 replace
  2. void replace_string2(string &s, const string &oldVal, const string &newVal)
  3. {
  4. int p = 0;
  5. while ((p = s.find(oldVal, p)) != string::npos)
  6. {
  7. s.replace(p, oldVal.size(), newVal);
  8. p += newVal.size();
  9. }
  10. }
  1. //练习 9.45:编写一个函数,接受一个表示名字的 string 参数和两个分别表示前缀(如 "Mr."或"Ms.")
  2. //和后缀(如"Jr"或"III")的字符串。使用迭代器及 insert 和 append 函数将前缀和后缀
  3. //添加到给定的名字中,将生成的新 string 返回。
  4. string name_string(string &name,const string &prefix,const string &suffix)
  5. {
  6. name.insert(name.begin(), 1, ' ');
  7. name.insert(name.begin(), prefix.begin(), prefix.end()); //插入前缀
  8. name.append(" ");
  9. name.append(suffix.begin(), suffix.end()); //插入后缀
  10. }
  1. //练习 9.46:重写上一题的函数,这次使用位置和长度来管理 string,并只使用 insert.
  2. string name_string1(string &name, const string &prefix, const string &suffix)
  3. {
  4. name.insert(0," ");
  5. name.insert(0, prefix); //插入前缀
  6. name.insert(name.size(), " ");
  7. name.insert(name.size(), suffix); //插入后缀
  8. }

9.5.3 string 搜索操作

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

        string 搜索函数返回 string::size_type 值,该类型是一个 unsigned 类型。因此,用一个 int 或其他带符号类型来保存这些函数的返回值不是一个好主意。

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 函数完成最简单的搜索。它查找参数指定的字符串,若找到,则返回第一个匹配位置的下标,否则返回 npos:

  1. string name("AnnaBelle");
  2. auto pos1 = name.find("Anna"); // pos1 == 0

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

  1. string lowercase("annabelle");
  2. pos1 = lowercase.find("Anna"); // pos1 == npos

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

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

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

  1. string dept("5236345p3");
  2. //返回 7——字符'p'的下标
  3. auto pos2 = dept.find_first_not_of(numbers);

指定在哪里开始搜索

        我们可以传递给 find 操作一个可选的开始位置。这个可选的参数指出从哪个位置开始进行搜索。默认情况下,此位置被置为 0。例如,用这个可选参数在字符串中循环地搜索子字符串出现的所有位置:

  1. string::size_type pos = 0;
  2. while((pos = name.find_first_of(numbers, pos))
  3. != string::npos)
  4. {
  5. cout<<"found number at index:"<<pos
  6. <<" element is "<<name[pos]<<endl;
  7. ++pos; //移动到下一个字符
  8. }

while 的循环条件是将 pos 重置为从 pos 开始遇到的第一个数字的下标。只要 find_first_of 返回一个合法下标,我们就打印当前结果并递增 pos。

        如果忽略了递增 pos,循环永远也不会终止。为了搞清楚原因,考虑如果不做递增运算会发生什么。在第二步循环中,我们从 pos 指向的字符开始搜索。这个字符是一个数字,因此 find_first_of 会(重复地)返回 pos!

逆向搜索

        find 是从左至右搜索。类似地,rfind 是从右至左进行搜索。

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

9.5.3 节练习

  1. //练习 9.47:编写程序,首先查找 string "ab2c3d7R4E6"中的每个数字字符,然后查找其中每个字母字符。
  2. //编写两个程序版本,第一个使用 find_first_of,第二个使用 find_first_not_of。
  3. void find_char(string &s,const string &chars)
  4. {
  5. cout << "在" << s << "中查找" << chars << "中字符" << endl;
  6. string::size_type pos = 0;
  7. //每步循环查找 s 中下一个数
  8. while ((pos = s.find_first_of(chars, pos)) != string::npos)//找到字符
  9. {
  10. cout << "pos:" << pos << " ,char:" << s[pos] << endl;
  11. ++pos;
  12. }
  13. }
  14. void find_not_char(string &s, const string &chars)
  15. {
  16. cout << "在" << s << "中查找不在" << chars << "中字符" << endl;
  17. string::size_type pos = 0;
  18. //每步循环查找 s 中下一个数
  19. while ((pos = s.find_first_not_of(chars, pos)) != string::npos)//找到字符
  20. {
  21. cout << "pos:" << pos << " ,char:" << s[pos] << endl;
  22. ++pos;
  23. }
  24. }
  25. int main()
  26. {
  27. string s = "ab2c3d7R4E6";
  28. cout << "查找所有数字" << endl;
  29. find_char(s, "0123456789");
  30. find_not_char(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRETUVWXYZ");
  31. cout << "查找所有字母" << endl;
  32. find_char(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRETUVWXYZ");
  33. find_not_char(s, "0123456789");
  34. return 0;
  35. }
  1. // 练习9.49:如果一个字母延伸到中线之上,如 d 或 f ,则称其有上出头部分(ascender。
  2. //如果一个字母延伸到中线之下,如 p 或 g,则称其有下出头部分(descender))。编写程序,
  3. //读入一个单词文件,输出最长的既不包含上出头部分,也不包含下出头部分的单词
  4. void find_longest_word(ifstream &in)
  5. {
  6. string s, longest_word;
  7. int max_length = 0;
  8. while (in >> s)
  9. {
  10. if (s.find_first_of("bdghjklqpty") != string::npos)
  11. continue;
  12. cout << s << " ";
  13. if (max_length < s.size()) //新单词更长
  14. {
  15. max_length = s.size();
  16. longest_word = s;
  17. }
  18. }
  19. cout << "最长单词:" << longest_word << endl;
  20. }

9.5.4 compare 函数

        compare 有 6 个版本。

s.compare 的几种参数形式
s2比较 s 和 s2
pos1, n1, s2将 s 中从 pos1 开始的 n1 个字符与 s2 进行比较
pos1, n1, s2, pos2, n2将 s 中从 pos1 开始的 n1 个字符与 s2 中从 pos2 开始的 n2 个字符进行比较
cp比较 s 与 cp 指向的以空字符结尾的字符数组
pos1, n1, cp将 s 中从 pos1 开始的 n1 个字符与 cp 指向的以空字符结尾的字符数组进行比较
pos1, n1, cp, n2将 s 中从 pos1 开始的 n1 个字符与指针 cp 指向的地址开始的 n2 个字符进行比较

9.5.5 数值转换

        字符串中常常包含表示数值的字符。例如,我们用两个字符的 string 表示数值 15——字符 '1' 后跟字符 '5'。一般情况下,一个数的字符表示不同于其数值。

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

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

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

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

string 和数值之间的转换
to_string(val)一组重载函数,返回数值 val 的 string 表示。val 可以是任何算术类型(2.1.1节)。对每个浮点数类型和 int 或更大的整型,都有相应版本的 to_string。与往常一样,小整型会被提升(4.11.1节)
stoi(s, p, b)
stol(s, p, b)
stoul(s, p, b)
stoll(s, p, b)
stoull(s, p, b)
返回 s 的其实子串(表示整数内容)的数值,返回值类型分别是 int、long、unsigned long、long long、unsigned long long。b 表示转换所用的基数,默认值为 10。p 是 size_t 指针,用来保存 s 中第一个非数值字符的下标,p 默认为 0,即,函数不保存下标

stof(s, p)

stod(s, p)

stold(s, p)

返回 s 的起始子串(表示浮点数内容)的数值,返回值类型分别是 float、double 或 long double 。参数 p 的作用与整数转换函数中一样

9.5.5 节练习

  1. //练习 9.50:编写程序处理一个 vector<string>,其元素都表示整型值。
  2. //计算 vector 中所有元素之和。修改程序,使之计算表示浮点值的 string 之和
  3. int vector_sum(const vector<string> &vs)
  4. {
  5. int res = 0;
  6. for (auto x : vs)
  7. {
  8. res += stoi(x);
  9. //计算表示浮点值的 string 之和
  10. // res+=stof(x)
  11. }
  12. return res;
  13. }
  1. //练习 9.51:设计一个类,它有三个 unsigned 成员,分别表示年、月和日。
  2. //为其编写构造函数,接受一个表示日期的 string 参数。你的构造函数应该能处理不同数据格式
  3. //如 January 1,1900,1/1/1990、Jan 1 1900 等。
  4. class date
  5. {
  6. public:
  7. friend ostream& operator<<(ostream&, const date&);
  8. //构造函数
  9. date() = default;
  10. date(string &ds);
  11. unsigned y()const { return year; }
  12. unsigned m()const { return month; }
  13. unsigned d()const { return day; }
  14. private:
  15. unsigned int year;
  16. unsigned int month;
  17. unsigned int day;
  18. };
  19. //月份全称
  20. const string month_name[] = { "January","February","March","April","May","June",
  21. "July","August","September","October","November","December"};
  22. //月份简写
  23. const string month_abbr[] = { "Jan","Feb","Mar","Apr","May","Jun",
  24. "Jul","Aug","Sept","Oct","Nov","Dec" };
  25. //每月天数
  26. const int days[] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
  27. inline int get_month(string &ds,int &end_of_month)
  28. {
  29. int i, j;
  30. for (i = 0; i < 12; i++)
  31. {
  32. //检查每个字符是否与月份简写相等
  33. for (j = 0; j < month_abbr[i].size(); ++j)
  34. {
  35. if (ds[j] != month_abbr[i][j]) //不是此月简写
  36. break;
  37. if (j == month_abbr[i].size()) //与简写匹配
  38. break;
  39. }
  40. }
  41. if (i == 12) //与所有月份名称都不相同
  42. throw invalid_argument("不是合法月份名");
  43. if (ds[j] == ' ') //空白符,仅是月份缩写
  44. {
  45. end_of_month = j + 1;
  46. return i + 1;
  47. }
  48. for (; j < month_name[i].size(); j++)
  49. if (ds[j] != month_name[i][j])
  50. break;
  51. if (j == month_name[i].size() && ds[j] == ' ') //月份全称
  52. {
  53. end_of_month = j + 1;
  54. return i + 1;
  55. }
  56. throw invalid_argument("不是合法月份名");
  57. }
  58. inline int get_day(string &ds, int month, int &p)
  59. {
  60. size_t q;
  61. int day = stoi(ds.substr(p), &q); //从 p 开始的部分转换为日期值
  62. if (day<1 || day>days[month])
  63. throw invalid_argument("不是合法日期值");
  64. p += q; //移动到日期值之后
  65. return day;
  66. }
  67. inline int get_year(string&ds, int &p)
  68. {
  69. size_t q;
  70. int year = stoi(ds.substr(p), &q); //从 p 开始的部分转换为年
  71. if (p+q>ds.size())
  72. throw invalid_argument("非法结尾内容");
  73. return year;
  74. }
  75. date::date(string &ds)
  76. {
  77. int p;
  78. size_t q;
  79. if ((p = ds.find_first_of("0123456789")) == string::npos)
  80. throw invalid_argument("没有数字,非法日期");
  81. if (p > 0)
  82. {
  83. month = get_month(ds, p);
  84. day = get_day(ds, month, p);
  85. if (ds[p] != ' ' && ds[p] != ',')
  86. throw invalid_argument("非法间隔符");
  87. p++;
  88. year = get_year(ds, p);
  89. }
  90. else
  91. {
  92. month = stoi(ds, &q);
  93. p = q;
  94. if (month < 1 || month>12)
  95. throw invalid_argument("不是合法月份值");
  96. if (ds[p++] != '/')
  97. throw invalid_argument("非法间隔符");
  98. day = get_day(ds, month, p);
  99. if (ds[p++] != '/')
  100. throw invalid_argument("非法间隔符");
  101. year = get_year(ds, p);
  102. }
  103. }
  104. ostream& operator<<(ostream& out, const date& d)
  105. {
  106. out << d.y() << "年" << d.m() << "月" << d.d() << "日" << endl;
  107. return out;
  108. }

9.6 容器适配器

        除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue 和 priority_queue。

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

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

swap(a, b)

a.swap(b)

交换 a 和 b 的内容,a 和 b 必须有相同类型,包括底层容器类型也必须相同

定义一个适配器

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

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

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

  1. //在 vector 上实现的栈
  2. stack<string, vector<string>> str_stk;
  3. //str_stk2 在 vector 上实现,初始化时保存 svec 的拷贝
  4. stack<string, vector<string>> str_stk2(svec);

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

        stack 只要求 push_back、pop和 back  操作,因此可以使用除 array 和 forward_list 之外的任何容器来构造 stack。

        deque 适配器要求 back、push_back、front 和 push_front ,因此它可以构造于 list 或 deque 之上。但不能基于 vector 构造。

        priority_queue 除了 front 、push_back 和 pop_back 操作之外还要求随机访问能力,因此它可以构造于 vector 或 deque 之上,但不能基于 list 构造。

栈适配器

        stack 类型定义在 stack 头文件中。下面的程序展示了如何使用 stack。

  1. stack<int> intStack; // 空栈
  2. //填满栈
  3. for (size_t ix = 0; ix != 10; ++ix)
  4. {
  5. intStack.push(ix); //intStack 保存 0 到 9 十个数
  6. }
  7. while (!intStack.empty())//intStack有值就继续循环
  8. {
  9. int value = intStack.top();
  10. //使用栈顶值的代码
  11. intStack.pop(); //弹出栈顶元素,继续循环
  12. }
stack 支持的操作
栈默认基于 deque 实现,也可以在 list 或 vector 之上实现
s.pop()删除栈顶元素,但不返回该元素值

s.push(item)

s.emplace(args)

创建一个新元素压入栈顶,该元素通过拷贝或移动 item 而来,或者由 args 构造
s.top()返回栈顶元素,但不将元素弹出栈

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

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

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

队列适配器

        queue 和 priority_queue 适配器定义在 queue 头文件中。

deque 和 priority_queue 支持的操作

queue 默认基于 deque 实现,priority_queue 默认基于 vector 实现;

queue 也可以用 list 或 vector 实现,priority_queue 也可以用 deque 实现。

q.pop()返回 queue 的首元素或 priority_queue 的最高优先级的元素
q.front()返回首元素或尾元素,但不删除此元素
q.back()只适用于 queue
q.top()

返回最高优先级元素,但不删除该元素

只适用于 priority_queue

q.push(item)

q.emplace(args)

在 queue 末尾或 priority_queue 中恰当的位置创建一个元素,其值为 item 或者由 args 构造

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

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

9.6 节练习

  1. #include<iostream>
  2. #include<stack>
  3. #include<string>
  4. #include<vector>
  5. using namespace std;
  6. //练习 9.52:使用 stack 处理括号化的表达式。当你看到一个左括号,将其记录下来。
  7. //当你在一个左括号之后看到一个右括号,从 stack 中 pop 对象,直至遇到左括号,将
  8. //左括号也一起弹出栈。然后将一个值(括号内的运算结果)push 到栈中,表示一个括号
  9. //化的(子)表达式已经被处理完毕,被其运算结果所替代
  10. //这个程序只能计算加减法
  11. enum obj_type { LP, RP, ADD, SUB, VAL };
  12. struct obj
  13. {
  14. obj(obj_type type, double val = 0) { t = type; v = val; }
  15. obj_type t;
  16. double v;
  17. };
  18. inline void skipws(string &exp, size_t &p)
  19. {
  20. p = exp.find_first_not_of(" ", p);
  21. }
  22. inline void new_val(stack<obj> &so, double v)
  23. {
  24. if (so.empty() || so.top().t == LP) //空栈或右括号
  25. {
  26. so.push(obj(VAL, v));
  27. }
  28. else if (so.top().t == ADD || so.top().t == SUB)
  29. {
  30. //之前是运算符
  31. obj_type type = so.top().t;
  32. so.pop();
  33. if (type == ADD)
  34. v += so.top().v;
  35. else v = so.top().v - v;
  36. so.pop();
  37. so.push(obj(VAL, v));
  38. }
  39. else
  40. {
  41. throw invalid_argument("缺少运算符");
  42. }
  43. }
  44. int main()
  45. {
  46. stack<obj> so;
  47. string exp;
  48. size_t p = 0, q;
  49. double v;
  50. cout << "请输入表达式:";
  51. getline(cin, exp);
  52. while (p < exp.size())
  53. {
  54. skipws(exp, p);
  55. if (exp[p] == '(') //左括号直接压栈
  56. {
  57. so.push(obj(LP));
  58. p++;
  59. }
  60. else if(exp[p]=='+' || exp[p]=='-')
  61. {
  62. if (so.empty() || so.top().t != VAL)
  63. //空栈或之前不是运算符
  64. throw invalid_argument("缺少运算数");
  65. if (exp[p] == '+')
  66. so.push(obj(ADD));
  67. else so.push(obj(SUB));
  68. p++;
  69. }
  70. else if (exp[p] == ')') //右括号
  71. {
  72. p++;
  73. if (so.empty())
  74. throw invalid_argument("未匹配右括号");
  75. if (so.top().t == LP)
  76. throw invalid_argument("空括号");
  77. if (so.top().t == VAL)
  78. {
  79. v = so.top().v;
  80. so.pop();
  81. if (so.empty() || so.top().t != LP)
  82. throw invalid_argument("未匹配右括号");
  83. so.pop();
  84. new_val(so, v);
  85. }
  86. else
  87. throw invalid_argument("缺少运算数");
  88. }
  89. else
  90. {
  91. v = stod(exp.substr(p), &q);
  92. p += q;
  93. new_val(so, v);
  94. }
  95. }
  96. if (so.size() != 1 || so.top().t != VAL)
  97. throw invalid_argument("非法表达式");
  98. cout << so.top().v << endl;
  99. return 0;
  100. }

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/178224
推荐阅读
相关标签
  

闽ICP备14008679号