第8章 顺序容器

8.1 顺序容器概述

顺序容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素储存和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。标准库提供了三种容器适配器,分别为容器操作定义了不同的接口,来与容器类型适配。所有顺序容器都提供了快速顺序访问元素的能力。但是这些容器在以下方面都有不同的性能折中,不同容器向容器添加或从容器中删除元素的代价不同,不同容器非顺序访问容器中元素的代价不同。

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

除了固定大小的array外,其他容器都提供高效、灵活的内存管理。我们可以添加和删除元素,扩张和收缩容器的大小。容器保存元素的策略对容器操作的效率是有影响的,在某些情况下,储存策略还会影响特定容器是否支持某些特定操作。
stringvector将元素保存在连续的内存空间中。由于元素是连续存储的,由元素的下标来计算其地址非常迅速,但是在这2种容器的中间位置添加或删除元素就会非常耗时。
listforward_list两个容器的设计目的是令容器的任何位置的添加和删除操作都很快速,但作为代价这2个容器不支持元素的随机访问。为了访问一个元素必须遍历整个容器。
deque与string和vector类似,deque支持快速的随机访问,同样在deque的中间位置插入或删除元素的代价很高,但在deque的两端添加或删除元素的操作很快。
forward_listarray是新C++增加的类型。array对象的大小是固定的。array不支持添加和删除元素以及改变容器大小的操作,forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此forward_list没有size操作。
1.确认使用那种顺序容器
除非你有很好的理由,不然一律建议使用vector;
如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list;
如果程序要求随机访问元素,应使用vector或deque;
如果在容器中间插入或删除元素,应使用list或forward_list;
如果在头尾位置插入或删除元素,但不会在中间位置插入或删除元素,建议使用deque;

8.2 容器库概览

容器类型上的操作形成了一种层次:某些操作是所有容器类型都提供的,而另外一些操作仅针对顺序容器、关联容器或无序容器,还有一些操作只适用于一小部分容器。
1.对容器可以保存类型的限制
顺序容器几乎可以保存任意类型的元素。特别的是我们可以定义一个容器,其元素的类型是另外一个容器。

vector<vector<string>>lines;

8.2.1 迭代器

与容器一样,迭代器有着公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的。例如,标准容器类型上的所有迭代器都允许我们访问容器中的元素,而所有迭代器都是通过解引用运算符来实现这个操作。
1.迭代器范围
一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。这2个迭代器通常被称为begin和end,或者是first和last。它们标记了容器中元素的一个范围。虽然第2个迭代器被称为last,但这个迭代器从来不会指向范围中最后一个元素,而是指向尾元素之后的位置。迭代器范围中的元素包含first所表示的元素以及从first开始到last(不包含last)之间的所有元素。这种元素范围被称为左闭合区间。
2.使用左闭合范围蕴含的编程假定
标准库使用左闭合范围是因为这种范围有三种方便的性质。假定begin和end构成一个合法的迭代器范围,如果begin和end相等,则范围为空。如果begin和end不等,则范围内至少包含一个元素,且begin指向该范围中的第一个元素,我们可以使begin递增若干次,使得begin == end。

8.2.2 容器类型成员

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

8.2.3 begin和end成员

begin和end操作生成指向容器中第一个元素和尾元素之后位置的迭代器。这2个迭代器最常见的用途是形成一个包含容器中所有元素的迭代器范围。begin和end有多个版本:带r的版本返回反向迭代器,以c开头的版本返回const迭代器。

auto it1=a.begin();
auto it2=a.rbegin();
auto it3=a.cbegin();

8.2.4 容器定义和初始化

每个容器都定义了一个默认构造函数。除array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。
1.将一个容器初始化为另一个容器的拷贝
将一个新容器创建为另一个容器的拷贝的方法有2种:可以直接拷贝整个容器,或者拷贝由一个迭代器指定的元素范围。为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且新容器和原始容器中的元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。

list<string>authors={"A","B","C"};
vector<const char*>articles={"a","b","c"};
list<string>list2(authors);
forward_list<string>words(articles.begin(),articles.end());

2.列表初始化
在新标准中,我们可以对一个容器进行列表初始化,当这样做时,我们就显式的指定了容器中每个元素的值。对于除array之外的容器类型,初始化列表还隐含地制定了容器的大小,容器将包含与初始值一样多的元素。
3.与顺序容器大小相关的构造函数
除了与关联容器相同的构造函数外,顺序容器(除array)还提供另一个构造函数,它接受一个容器大小和一个(可选的)元素初始值。如果我们不提供元素初始值,则标准库会创建一个值初始化器。

forward_list<int>ivec(10);  //10个元素,初始化为0
deque<string>svec(10);      //10个元素,每个都是空string

如果元素类型是内置类型或者是具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。如果元素类型没有默认构造函数,除了大小参数外还必须指定一个显式的元素初始值。
4.标准库array具有固定大小
与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:

array<int,42>        //保存42个int的数组
array<int,10> ia1;
array<string,10>    //保存10个string的数组

由于大小是array类型的一部分,array不支持普通的容器构造函数。这些构造函数都会确定容器的大小,要么隐式地,要么显式地。array大小固定的特性也影响了它所定义的构造函数的行为。与其他容器不同,一个默认构造的array是非空的;它包含了与其大小一样多的元素。与其他容器一样,array也要求初始值的类型必须与要创建的容器类型相同。此外array还要求元素类型和大小也都一样,因为大小是array类型的一部分。

8.2.5 赋值和swap

c1=c2;
c1={a,b,c};

第一个赋值运算后,左边容器将与右边容器相等。如果两个容器原来大小不同,赋值运算后两者的大小都与右边容器的原大小相同。第二个赋值运算后,c1的size变为3,即花括号列表中值的数目。与内置数组不同,标准库array类型允许赋值。赋值号左右两边的运算对象必须具有相同的类型。

array<int,10>a1={0,1,2,3,4,5,6,7,8,9};
array<int,10>a2={0};
a1=a2;
a2={0};//错误

由于右边运算对象的大小可能与左边运算对象的大小不同,因此array类型不支持assign,也不允许用花括号包围的值列表进行赋值。
1.使用assign(仅顺序容器)

函数 含义
seq.assign(b,e) 将seq中的元素替换为迭代器b和e所表示的范围中的元素。b和e不能指向seq中的元素
seq.assign(i1) 将seq中的元素替换为初始化列表i1中的元素
seq.assign(n,t) 将seq中的元素替换为n个值为t的元素

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

list<string>names;
vector<const char*>oldstyle;
names.assign(oldstyle.cbegin(),oldstyle.cend());

assign的第2个版本接受一个整型值和一个元素值。它用指定数目且具有相同给定值的元素替换容器中原有的元素:

list<string>slist1(1);
slist1.assign(10,"Hi"); //10个元素,都为"Hi";

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

vector<string> svec1(10);
vector<string> svec2(24);
swap(svec1,svec2);

除array外,交换2个容器内容的操作保证会很快——元素本身并未交换,swap只是交换了两个容器的内部数据结构。元素不会被移动意味着除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。与其他容器不同,swap两个array会真正交换它们的元素。因此交换2个array所需的时间与array中元素的数目成正比。

8.2.6 容器大小操作

成员函数size返回容器中元素的数目;empty判断容器是否为空,max_size返回一个大于或等于该类型容器所能容纳的最大元素数的值。

8.2.7 关系运算符

每个容器类型都支持相等运算符,除了无序关联容器外的所有容器都支持关系运算符( >、>=、<、<=),关系运算符左右两边的运算对象必须是同类型的容器,且必须保存相同类型的元素。比较2个容器实际上是进行元素的逐对比较:
如果2个容器具有相同大小且所有元素都两两对应相等,则这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={1,3,5,7,9,12};
v1<v2
v1>v3
v1==v4

1.容器的关系运算符使用元素的关系运算符完成比较
只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。比如我们无法比较两个类类型容器的大小,除非类类型对象之间定义了相应的比较运算符。

8.3 顺序容器操作

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

8.3.1 向顺序容器添加元素

除了array之外,所有标准库容器都提供灵活的内存管理。在运行时可以动态的添加或者删除元素来改变容器大小。forward_list有自己专有版本的insert和emplace,forward_list不支持push_back和emplace_back。

函数 含义
c.push_back(t) 在c的尾部插入t
c.emplace_back(args) 在c的尾部插入一个由args创建的元素
c.push_front(t) 在c的头部插入t
c.emplace_front(args) 在c的头部插入一个由args创建的元素
c.insert(p,t) 在迭代器p指向的元素之前创建一个t
c.emplace(p,args) 在迭代器p指向的元素之前插入一个由args创建的元素
c.insert(p,n,t) 在迭代器p指向的元素之前插入n个值为t的元素。返回指向新添加的第一个元素
c.insert(p,b,e) 将迭代器b和e指定的范围内元素插入到迭代器p指向的元素之前,返回指向新添加的第一个元素
c.insert(p,il) il是一个花括号包围的元素值列表,将这些给定值插入到迭代器p指向的元素之前

不同容器使用不同的策略来分配元素空间,在一个vector或string的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素都需要移动元素。而且向一个vector或string添加元素可能引起整个对象储存空间的重新分配。
1.使用push_back
除array和forward_list之外,每个顺序容器都支持push_back。
2.使用push_front
除了push_back,list、forward_list和deque容器还支持名为push_front的类似操作。此操作将元素插入到容器头部。

list<int>list1;
for(size_t i=0;i<4;i++)
{
  list1.push_front(i);
}

deque像vector一样提供了随机访问元素的能力,但他提供了vector所不支持的push_front,deque保证在容器首尾进行插入和删除元素的操作都只花费常数时间。与vector一样,在deque首尾之外的位置插入元素会很耗时。
3.在容器的特定位置访问元素
insert成员提供了更一般的添加功能,它可以在容器的任意位置插入元素。vector、deque、list和string都支持insert操作,forward_list提供了特殊版本的insert成员。每个insert函数都接受一个迭代器作为其第一个参数,迭代器指出来了在容器什么位置添加元素。
4.插入范围元素
除了第一个迭代器参数外,insert函数还可以接受更多的参数,接受一对迭代器或一个初始化列表的insert版本将给定范围内的元素插入到指定位置之前,如果我们传递给insert一对迭代器,它们不能指向添加元素的容器。该函数返回指向第一个新加入元素的迭代器。
5.使用insert的返回值
通过使用insert的返回值,可以在容器的一个特定位置反复插入元素。
6.使用emplace操作
新标准引入了三个新成员,他们分别是emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素,这些操作分别对应的是push_front、insert、push_back。

8.3.2 访问元素

包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这2个操作分别返回首元素和尾元素的引用。
1.访问成员函数返回的是引用
在容器中访问元素的成员函数(front、back、下标和at)返回的都是引用。如果容器是一个const对象,则返回值是const的引用。如果容器不是const的,则返回值是普通引用,我们可以用来改变元素的值。
2.下标操作和安全的随机访问
提供快速随机访问的容器(string、vector、deque和array)也都提供下标运算符。下标运算符接受一个下标参数,返回容器中该位置的元素的引用。

8.3.3 删除元素

与添加元素的多种方式类似,(非array)容器也有多种删除元素的方式。

函数 含义
c.pop_back() 删除c中尾元素
c.pop_front() 删除c中首元素
c.erase§ 删除迭代器p所指向的对象
c.erase(b,e) 删除迭代器b和e所指定范围内的元素
c.clear() 删除c中所有元素

删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector和string中删除点之后位置的迭代器、引用和指针都会失效。
1.pop_front和pop_back成员函数
pop_front和pop_back成员函数分别删除首元素和尾元素。与vector和string不支持push_front一样,这些类型也不支持pop_front。类似的,forward_list不支持pop_back。与元素访问成员函数类似,不能对一个空容器执行弹出操作。
2.从容器内部删除一个元素
成员函数erase从容器中指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的erase都返回指向删除的最后一个元素之后的位置的迭代器。

8.3.4 特殊的forward_list操作

在一个单向链表中,没有简单的方法来获取一个元素的前驱,出于这个原因在一个forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。由于这些操作与其他容器上的操作实现方式不同,forward_list定义了名为insert_after、emplace_after和erase_after的三种操作。

8.3.5 改变容器大小

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

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

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

向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。
1.编写改变容器的循环程序
添加或删除vector、string或deque元素的循环程序必须考虑迭代器、引用和指针可能失效的问题,程序必须保证每个循环步中都更新迭代器、引用或指针。
2.不要保存end返回的迭代器
当我们删除或添加vector或string的元素后,或在deque中首元素之外的任何位置添加或删除元素后,原来的end返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器,一直当作容器末尾使用。

8.4 vector对象是如何增长的

为了支持快速随机访问,vector将元素连续储存——每个元素紧挨着前一个元素存储。我们不必关心一个标准库类型是如何实现的,而只需关心它如何使用。vector和string的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可用来保存更多的新元素。这样就不需要每次添加新元素都重新分配容器的内存空间了。
1.管理容量的成员函数
vector和string类型提供了一些成员函数,允许我们与它的实现中内存分配部分互动。

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

shrink_to_fit只适用于vector、string、deque,而capacity和reserve只适用于vector和string。只有当需要的内存空间超过当前容量时,reserve调用才会改变vector的容量。如果需求大小小于当前容量,reserve至少分配与需求一样大的内存空间。如果需求大小小于或等于当前容量,reserve什么也不做。特别的是,当需求大小小于当前容量时,容器不会退回内存空间,因此在调用reserve之后,capacity将会大于或等于传递给reserve的参数。
2.capacity和size
容器的size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。只有在执行insert操作时size与capacity相等,或者是在调用resize或reserve时给定的大小超过当前capacity,vector才可能重新分配内存空间。会分配多少超过给定容量的额外空间,取决于具体实现。虽然不同的实现可以采用不同的分配策略,但所有实现都应遵循一个原则:确保用push_back向vector添加元素的操作有高效率。

8.5 额外的string操作

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

8.5.1 构造string的方法

函数 含义
string s(cp,n) 拷贝cp中n个字符赋值给s
string s(s2,pos2) 从下标pos2开始拷贝赋值给s
string(s2,pos2,len2) 从下标pos2开始len2个字符拷贝赋值给s

这些构造函数接受一个string或一个const char* 参数。
1.substr操作
substr操作返回一个string,它是原始string的一部分或者是全部的拷贝。

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

8.5.2 改变string的其他方法

string类型支持顺序容器的赋值运算符,以及assign、insert和erase操作,它还定义了额外的insert和erase版本。除了接受迭代器的insert和erase版本外,string还提供了接受下标的版本。

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

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

1.append和replace函数
string类定义了2个额外的成员函数:append和replace
append操作是在string末尾进行插入操作的一种简写形式。
replace操作是调用erase和insert的一种简写形式。

string s("C++ Primer"),s2=s;
s.insert(s.size()," 4th ed");
//s=="C++ Primer 4th ed"
s2.append(" 4th ed");   //s==s2

s.erase(11,3);         //s=="C++ Primer ed";
s.insert(11,"5th");    //s=="C++ Primer 5th ed"
s.replace(11,3,"5th"); //s=="C++ Primer 5th ed"

2.改变string的多种重载函数
append、assign、insert和replace函数有多个重载版本,根据我们如何指定要添加的字符和string中被替换的部分,这些函数的参数有多个版本。

8.5.3 string搜索操作

string类有6个不同的搜索函数,且每个函数都有4个重载版本,每个搜索操作都返回一个string::size_type值,表示匹配位置发生位置的下标。若搜索失败则返回一个名为string::nops的static成员,标准库将npos定义为一个const string::size_type类型,并初始化为值-1。

string name("AnnaBelle");
auto pos1=name.find("Anna");//pos1=0;
//这段程序返回0,即子字符串在name中第一次出现的下标

搜索是对大小写敏感的:

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

1.指定在哪里开始搜索
我们可以传递给find操作一个可选的开始位置,这个参数指出从那个位置开始进行搜索。
2.逆向搜索
到现在为止,我们已经用过的find操作都是由左至右搜索,标准库还提供了类似的但由右至左的操作。rfind成员函数搜索最后一个匹配,即子字符串最靠右的出现位置。

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

8.5.4 compare函数

标准库string类型还提供了一组compare函数,这些函数与C标准库的strcmp函数很类似,根据s是等于、大于、小于参数指定的字符串,s.compare返回0,正数或负数。

8.5.5 数值转换

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

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

8.6 容器适配器

除了顺序容器,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。适配器是标准库中一个通用的概念。容器、迭代器和函数都有适配器。本质上一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。例如stack适配器接受一个顺序容器(除array或forward_list),并使其操作起来像一个stack一样。
1.定义一个适配器
每个适配器都定义2个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。

deque<int> deq;
stack<int>stk(deq);

默认情况下stack和queue是基于deque实现的,priority_queue是在vector之上实现的。
2.栈适配器
stack类型定义在stack头文件中:

stack<int>intStack;
for(size_t ix=0;ix!=10;ix++)
{
  intStack.push(ix);
}

栈默认基于deque实现,也可以在list或vector上实现。

函数 含义
s.pop() 删除栈顶元素
s.push(item) 创建一个元素压入栈顶
s.emplace(args) 创建一个元素压入栈顶
s.top() 返回栈顶元素

3.队列适配器
queue和priority_queue适配器定义在queue头文件中

函数 含义
q.pop() 返回queue的首元素
q.front() 返回首元素或尾元素
q.back() 只适用于queue
q.top() 返回最高优先级元素
只适用于priority_queue
q.push(item) 在queue末尾创建一个元素
q.emplace(args) 在queue末尾创建一个元素

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

你可能感兴趣的:(C++学习笔记,c++,开发语言,学习方法)