C++ 容器之顺序容器

Container——Sequential Container & Container Adaptor

写在前面:本篇主要介绍顺序容器,文章内容主要来自于C++ Primer

所有容器共享公共接口,不同容器按不同方式对其进行扩展。每种容器提供了不同性能和功能的权衡。

容器大致可分为三类:顺序容器,关联容器和无序容器。

顺序容器中的元素与其加入容器时的位置相对应。顺序容器提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而与元素加入容器时的位置相对应。

关联容器中的元素的位置由元素相关联的关键字值决定。

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

Summary of Sequential Container

下表中列出了标准库中的顺序容器,所有顺序容器都提供了快速顺序访问元素的能力。

但是这些容器在以下方面有不同的性能折中:

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

与内置数组相比,array是一种更安全,更易用的数组类型。与内置数组类似,array对象的大小是固定的。因此array不支持增删元素以及改变容器大小的操作

forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此没有size操作,因为保存或计算其大小就会比手写的链表多出很多额外开销。

确定使用哪种顺序容器

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

  1. 通常,vector是最好的选择,除非有很好的理由选择其他的容器
  2. 如果程序有很多小的元素,且空间的额外开销很重要,那么就不要使用list或forward_list
  3. 如果程序要支持随机访问,需要用vector或deque
  4. 如果要求在容器中间插入或删除元素,应该用list或forward_list
  5. 如果在头/尾进行增删操作,应该用deque
  6. 如果既有随机访问,又有容器中间插入元素,那么需要对两者进行权衡来选择vector或list

容器库概览

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

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

一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。容器均定义为模板类。

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

顺序容器几乎可以保存任意类型的元素。并且我们可以定义一个容器,其元素的类型是另一个容器。

在构造某些类的容器的时候,这个类可能没有默认构造函数,我们在构造它的容器的时候,需要传递一个初始化器,用来初始化元素。

容器操作
类型别名说明
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(array不支持)
C c{a, b, c…};列表初始化c
赋值与swap说明
c1 = c2将c1中的元素替换为c2中的元素
c1 = {a, b, c,…}将c1中的元素替换为列表中的元素(不适用于array)
a.swap(b)交换a和b中的元素
swap(a,b)同上
大小说明
c.size()c中元素的数目(不支持forward_list)
c.max_size()c可保存的最大元素数目
c.empyt()若c中存储了元素,则返回false,否则返回true
增删元素(不适用于array,并且不同容器这些操作的接口都不同)说明
c.insert(args)将args中的元素拷贝到c
c.emplace(ints)使用inits构造c中的一个元素
c.erase(args)删除args指定的元素
c.clear()删除c中的所有元素,返回void
关系运算符说明
==, !=所有容器都支持相等(不等)运算符
<, <=, >, >=关系运算符(无序关联容器不支持)
获取迭代器说明
c.begin(), c.end()返回指向c的首元素和尾后位置的迭代器
c.cbegin(), c.cend()返回const_iterator
反向容器的额外成员(不支持forward_list)说明
reverse_iterator逆序寻址元素的迭代器
const_reverse_iterator上述的const版本
c.rbegin(), c.rend()前向指向c的尾元素和首前元素的迭代器
c.crbegin(), c.crend()上述的const版本
以vector为例举些例子
定义和初始化vector对象说明
vector<T> v1v1是一个空vector,它的元素是T类型的,执行默认初始化
vector<T> v2(v1)v2中包含有v1元素的副本
vector<T> v2 = v1等价于v2(v1)
vector<T> v3(n, val)v3包含了n个重复元素,值为val
vector<T> v4(n)v4包含了n个重复执行了值初始化的对象
vector<T> v5{a, b, c…}v5包含了初始值个元素,每个元素被赋予相应的初始值
vector<T> v5 = {a, b, c…}相当于v5{a,b,c…}

初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号,如:

vector<int> v1(10);					// v1有10个元素, 每个元素值都被默认初始化为0
vector<int> v2{10};					// v2有1个元素,被显式初始化为10

vector<int> v3(10, 5);				// v3有10个元素,每个元素都被显式初始化为5
vector<int> v4{10, 5};				// v4有2个元素,分别被显式初始化为10和5
vector支持的操作说明
v.empty()是否为空,返回bool
v.size()返回元素个数
v.push_back(t)向v的尾端添加一个元素t
v[n]返回v中第n个位置上元素的引用,
只能访问已存在元素,不能用来添加元素
v1 = v2用v2中元素拷贝替换v1中的
v1 = {a,b,c…}用列表中元素拷贝替换v1中的
v1 == v2
v1 != v2
<,<=,>,>=

迭代器

与容器一样,迭代器有公共的接口:如果一个迭代器提供某个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是相同的

标准容器迭代器的运算符说明
*iter返回迭代器iter所指元素的引用
iter->mem解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++iter令iter指示容器中下一个元素
–iter令iter指示容器中上一个元素
iter1 == iter2判断两个迭代器是否相等,如果两个迭代器指示的是同一个元素
或它们是同一个容器的尾后迭代器,则相等;反之不相等
iter1 != iter2同上

注意,forward_list不支持--运算。

迭代器范围

迭代器范围的概念是标准库的基础

一个迭代器范围(iterator range)由一对迭代器表示,两个迭代器分别指向同一容器中的元素或是尾后元素。这两个迭代器可以指向同一个元素。这两个迭代器通常被称为begin和end。这个元素范围被称为左闭右开区间: [begin, end)

也就是说,迭代器范围中的元素包含begin指的那个直到end所指那个元素前面那个(不包含end,通常end是尾后元素,不能解引用end,因为它是无效值)。

两个迭代器begin和end构成迭代器范围的要求:

  • 它们指向同一个容器中的元素,或是尾后元素
  • 可以通过反复递增begin到达end,也就是说,end不在begin前

编译器不会强制要求上面这两条,确保其符合规则是程序员的责任

左闭右开的好处

使用左闭右开范围是因为这种范围有三个好处:

  1. 如果begin与end相等,则范围为空(也就是这个迭代器范围中没有元素)
  2. 如果begin与end不相等,则范围至少包含一个元素,并且begin指向该范围中的第一个元素
  3. 可以对begin递增若干次,使得begin==end
begin 和 end成员

begin和end操作生成指向容器第一个元素和尾后元素的迭代器。如前面的介绍,begin和end有很多个版本:带r的版本返回反向迭代器,以c开头的版本则返回const迭代器。

不以c开头的函数都是被重载过的,也就是说,实际上有两个名为begin的成员,一个是const成员,返回容器的const_iterator类型。另一个是非常量成员,返回容器的iterator类型。当对非常量对象调用这些成员,得到的是iterator的版本,如果对常量对象调用,则得到一个const版本的迭代器。但是对一个非常量对象调用带c的begin或end可以将一个普通的iterator转换为对应的const_iterator。

以c开头的版本是C++新标准引入的,用以支持auto与begin和end函数结合使用。过去没得选,只能显式声明希望使用哪种类型的迭代器:

// 显式指定类型
list<string>::iterator it = a.begin();
list<string>::const_iterator it2 = a.begin();

// 依赖于a的类型推断是iterator还是const_iterator
auto it3 = a.begin();		// 仅当a是const时,it3是const_iterator
auto it4 = a.cbegin();		// it4是const_iterator

auto与begin 和end结合使用时,获得迭代器类型依赖于容器类型,与我们想如何使用迭代器毫不相关。但以c开头的版本还是可以得到const_iterator,不管容器的类型是什么。

当不需要写访问的时候,应该使用cbegin和cend

容器定义和初始化

array

在定义一个array时,除了指定元素类型之外,还需要指定容器大小,因为大小是array类型的一部分:

array<int, 42>		// 类型为:保存42个int的数组

array不支持普通的容器构造函数,因为那些构造函数都隐式或显式地确定容器大小。

虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但对array并无此限制

赋值和swap

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

如果两个容器原来大小不同,赋值后两者大小都与右边容器的原大小相同

内置数组不同,标准库array类型允许赋值。赋值号左右两边的运算对象必须具有相同的类型。

由于右边运算对象的大小可能与左侧运算对象的大小不同,因此array类型不支持assign,也不允许用花括号包围的值列表进行赋值:

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

注意赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、指针和引用失效。

使用assign(仅顺序容器)

赋值运算要求左边和右边运算对象具有相同的类型

顺序容器(除array)还定义了一个名为assign的成员,允许我们从不同但相容的类型赋值,或从容器的一个子序列赋值。

使用swap

swap操作交换两个相同类型容器的内容。除了 array之外,交换两个容器的内容的操作保证会很快——元素本身并未交换,swap只是交换了两个容器内部的数据结构

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

元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作后不会失效。它们仍指向swap操作之前的那些元素。只是,swap之后,那些元素不属于之前的容器了。

与其他容器不同,对一个string调用swap会导致迭代器、引用和指针都失效

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

容器提供了成员函数版本的swap,也提供了非成员版本的swap。

容器大小操作

除了forward_list外,所有容器类型都有三个与大小有关的操作:

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

forward_list不支持size。

关系运算符

所有容器类型都支持相等运算符(==和!=)。

除了 关联容器外,都支持关系运算符(>,>=,<,<=)。关系运算符两侧运算对象必须是相同类型的容器,必须保存相同类型的元素

只有容器中保存的元素类型也定义了相应的比较运算符时,才可以使用关系运算符来比较两个容器。容器的相等运算和关系运算实际上是使用元素的==<运算符实现的。

Operation of Sequential Container

上一部分介绍了所有容器都支持的操作,本节介绍顺序容器特有的操作。

本节主要包括:向容器添加元素、访问元素、删除元素,即增删查改

向顺序容器添加元素

关键概念:容器元素是拷贝。当我们使用对象来初始化容器,或将一个对象插入到容器时,实际上放入到容器的是对象值的一个拷贝,而不是对象本身,就像对非引用形参类型函数传参一样,容器中的元素与提供值的对象之间没有任何关联,随后对容器中元素的任何修改都不会影响到原始对象。

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

向顺序容器添加元素的操作说明
c.push_back(t)
c.emplace_back(args)
在c的尾部创建一个值为t或由args创建的元素。返回void
c.push_front(t)
c.emplace_front(args)
在c的头部创建一个值为t或由args创建的元素。返回void
c.insert(p,t)
c.emplace(p,args)
在迭代器p指向的元素之前创建一个值为t或由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

上述的insert操作,都需要提供一个用来标记插入位置的迭代器p,如果插入的元素个数不为0,则返回指向新插入的那些元素的首元素的迭代器,如果插入元素个数为0,则返回传入的那个迭代器p。

注意:

  • 上面的操作会改变容器的大小,所以array不支持上述操作
  • forward_list有自己专有版本的insert和emplace
  • forward_list不支持push_back和emplace_back
  • vector和string不支持push_front和emplace_front
  • 向一个vector,string或deque插入元素会使所有指向容器的迭代器、引用和指针都失效
emplace操作

新标准引入了三个emplace相关的操作:emplace_front,emplace和emplace_back,这些操作构造而不是拷贝元素。

我们调用一个emplace成员函数的时候,是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理 的内存空间直接构造元素。如:

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

使用emplace_back时,会在容器管理的内存空间中直接创建对象,而调用push_back则会创建一个局部临时对象,并将其压入容器中,这造成一定的性能开销。

访问元素

如果容器中没有元素,则访问操作的结果是未定义的。

对一个空容器调用front和back,就像使用一个越界的下标一样,是一种严重的程序设计错误。因此在使用前需要使用empty()成员函数判断是否为空。

顺序容器中访问元素操作说明
c.back()返回c中尾元素的引用。若c为空,函数行为未定义
c.front()返回c中首元素的引用。若c为空,函数行为未定义
c[n]返回c中下标为n的元素的引用,n是个无符号整数。若c>=c.size(),则函数行为未定义
c.at(n)返回下标为n的元素引用。如果下标越界,则抛出out_of_range异常

注意:

  • at和下标操作只适用于string,vector,deque和array
  • back不适用于forward_list
访问成员函数返回的是引用

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

我们可以使用auto变量来保存这些返回值,如果想使用此变量改变元素的值,必须记得将变量定义为引用类型:

auto &v = c.back();			// 使用变量v来作为返回值的引用,现在可以用这个reference v来改变容器中元素的值
下标操作和安全的随机访问

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

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

删除元素

除array外,容器有多种删除元素的方法。

顺序容器的删除操作说明
c.pop_back()删除c中尾元素。若c为空,则函数行为未定义。函数返回void
c.pop_front()删除c中首元素。若c为空,则函数行为未定义。函数返回void
c.erase§删除迭代器p所指的元素,返回一个指向被删元素之后元素的迭代器,若p指向尾元素,则返回尾后迭代器。若p是尾后迭代器,则函数行为未定义
c.erase(b,e)删除迭代器b和e所指定范围内的元素。返回一个指向最后一个被删元素之后元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器
c.clear()删除c中所有元素,返回void

注意:

  • 这些操作会改变容器大小,所以不适用于array
  • forward_list有特殊版本的erase
  • forward_list不支持pop_back;vector和string不支持pop_front
  • 删除deque中除首位外的任何元素都会使所有迭代器,引用和指针失效
  • 指向vector或string中删除节点之后位置的迭代器,引用和指针都会失效
  • 删除元素的成员函数并不会检查其参数,在删除元素之前,程序员必须确保它们是存在的

forward_list的特殊操作

forward_list是一个单向链表,当添加或删除一个元素时,添加或删除的元素之前的那个元素的后继会发生变化。因此,为了增删一个元素,我们需要访问其前驱,以改变前驱的链接。但是forward_list是一个单向链表,没有简单的方法获取一个元素的前驱。

因此,在forward_list中,增删元素的操作是通过改变给定元素之后的元素来完成的。

这样,我们总是可以访问到被添加或删除操作所影响的元素。

那么forward_list所定义的接口也就好理解了,通过一个迭代器来操作其后继元素的增删:)

在forward_list中增删元素的操作说明
lst.before_begin()
lst.cbefore_begin()
返回指向链表首前元素的迭代器,这个迭代器不能解引用。c版本返回一个const_iterator
lst.insert_after(p, t)
lst.insert_after(p, n, t)
lst.insert_after(p, b,e)
lst.insert_after(p, il)
在迭代器p之后的位置插入元素。t是一个对象,n是数量,b和e是表示范围的一对迭代器,il(initialization list)是一个花括号列表。返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回p。若p为尾后迭代器,则函数行为未定义。
emplace_after(p, args)使用args在p指定的位置之后创建一个元素。返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数行为未定义。
lst.erase_after§
lst.erase_after(b,e)
删除p指向的位置之后的元素,或删除从p之后直到(但不包含)e之间的元素。返回一个指向被删除元素之后元素的迭代器。若不存在这样的元素,则返回尾后迭代器。如果p指向lst的尾元素或者是一个尾后迭代器,则函数未定义。

改变容器大小

可以使用resize来增大或缩小容器,array不支持该操作。如果当前大小大于所要求的大小,则容器后部的元素都会被删除;如果当前大小小于新大小,则会将新元素添加到容器后部。

容器操作可能导致迭代器失效

向容器添加元素和从容器删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效

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

使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题。

向容器添加元素后

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

删除一个元素后

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

vector对象如何增长

管理容量的成员函数

vector和string类型提供了一些成员函数,允许我们与它的实现中内存分配部分互动。

capacity操作高速我们容器在不扩张内存的情况下可以容纳多少个元素

reserve操作允许我们通知容器它应该准备保存多少个元素(至少这些个元素,具体取决于容器的实现)。

容器大小管理操作说明
c.shrink_to_fit()仅适用于vector,string,deque,将capacity()减少为与size()相同大小
c.capacity()仅适用于vector,string。不重新分配内存空间的话,c可以保存多少个元素(也就是当前为这个容器分配的内存空间,而非已经在容器内存放的元素个数)
c.reserve()仅适用于vector,string。分配至少能容纳n个元素的内存空间

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

只有当reserve(n)调用里的n大于当前容器容量,才会改变当前容器的容量。如果需求小于当前容量,则reserve什么也不做,容器不会退回内存空间。

调用reserve永远不会减少容器占用的内存空间

热size成员函数只会改变容器中元素的数目,而不是容器的容量。

strink_to_fit是一种回收多余空间的请求,标准库并不保证退还内存

vector内存分配策略的原则是:只有到迫不得已时才分配新的内存空间

容器适配器Adaptor

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

适配器是标准库中一个通用概念。

容器、迭代器和函数都有适配器。

本质上,适配器是一种机制,能使某种事物的行为看起来像另一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。

定义一个适配器

每个适配器都有两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器。比如,可以用一个deque<int> que来初始化一个新的stack:

stack<int> stk(deq);

上面讲的顺序容器适配器都有默认的底层实现容器,我们可以通过在创建一个适配器时将命名的顺序容器作为第二个类型参数,来重载默认容器类型。如:

// 在vector上实现stack
stack<string,vector<string>> str_stk;

对于一个给定的适配器,可以使用哪些底层容器是由限制的,所有适配器都要求实现容器有添加和删除元素的能力。因此适配器不能构造在array上。

适配器默认底层实现容器可重载底层实现容器
stackdeque可以list,vector
queuedeque可以list,不能vector
priority_queuevector可以deque,不能list(因为还要求有随机访问的能力)
stack 适配器
stack适配器支持的操作说明
s.pop()删除栈顶,但不返回该元素
s.push(item)
s.emplace(args)
创建一个新元素压入栈顶,该元素通过移动或拷贝item而来,或者通过args构造
s.top()返回栈顶元素,但不将该元素弹出
队列适配器

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

queue和priority_queue适配器支持的操作说明
q.pop()弹出queue的首元素,或priority_queue的最高优先级元素,但不返回该元素
q.front()只适用于queue返回首元素,但不弹出该元素
q.back()只适用于queue,返回尾元素,但不将其弹出
q.top()只适用于优先队列,返回最高优先级元素,但不弹出该元素
q.push(item)
q.emplace(args)
在queue末尾或优先队列中合适位置创建一个元素
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值