C++复习(三)STL序列式容器(array,vector)

STL容器是什么

STL标准库中所有的序列式容器,包括 array、vector、deque、list 和 forward_list 容器。所谓STL序列式容器,其共同的特点是不会对存储的元素进行排序,元素排列的顺序取决于存储它们的顺序。

不同序列式容器的适用场景不同,本章不仅会介绍各个序列容器的特性和功能,还将系统讲解各个容器的具体用法,包括如何创建容器以及如何实现对容器中元素的增删改查等。STL 中提供了专家级的几乎我们所需要的各种容器,功能更好,复用性更高。

简单的理解容器,它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)。STL 提供有 3 类标准容器,分别是序列容器、排序容器和哈希容器,其中后两类容器有时也统称为关联容器。它们各自的含义如表 1 所示。

表 1 STL 容器种类和功能

容器种类功能
序列容器主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。
排序容器包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。
哈希容器C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。

另外,以上 3 类容器的存储方式完全不同,因此使用不同容器完成相同操作的效率也大不相同。所以在实际使用时,要善于根据想实现的功能,选择合适的容器。

C++ STL迭代器(iterator)用法详解

无论是序列容器还是关联容器,最常做的操作无疑是遍历容器中存储的元素,而实现此操作,多数情况会选用“迭代器(iterator)”来实现。我们知道,尽管不同容器的内部结构各异,但它们本质上都是用来存储大量数据的,换句话说,都是一串能存储多个数据的存储单元。因此,诸如数据的排序、查找、求和等需要对数据进行遍历的操作方法应该是类似的。既然类似,完全可以利用泛型技术,将它们设计成适用所有容器的通用算法,从而将容器和算法分离开。但实现此目的需要有一个类似中介的装置,它除了要具有对容器进行遍历读写数据的能力之外,还要能对外隐藏容器的内部差异,从而以统一的界面向算法传送数据。这是泛型思维发展的必然结果,于是迭代器就产生了。简单来讲,迭代器和 C++ 的指针非常类似,它可以是需要的任意类型,通过迭代器可以指向容器中的某个元素,如果需要,还可以对该元素进行读/写操作。

迭代器类别

STL 标准库为每一种标准容器定义了一种迭代器类型,这意味着,不同容器的迭代器也不同,其功能强弱也有所不同。

容器的迭代器的功能强弱,决定了该容器是否支持 STL 中的某种算法。

常用的迭代器按功能强弱分为输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器 5 种。本节主要介绍后面的这 3 种迭代器。输入迭代器和输出迭代器比较特殊,它们不是把数组或容器当做操作对象,而是把输入流/输出流作为操作对象。有关这 2 个迭代器,我们会在后续章节做详细介绍。

1) 前向迭代器(forward iterator)
假设 p 是一个前向迭代器,则 p 支持 ++p,p++,*p 操作,还可以被复制或赋值,可以用 == 和 != 运算符进行比较。此外,两个正向迭代器可以互相赋值。

2) 双向迭代器(bidirectional iterator)
双向迭代器具有正向迭代器的全部功能,除此之外,假设 p 是一个双向迭代器,则还可以进行 --p 或者 p-- 操作(即一次向后移动一个位置)。

3) 随机访问迭代器(random access iterator)
随机访问迭代器具有双向迭代器的全部功能。除此之外,假设 p 是一个随机访问迭代器,i 是一个整型变量或常量,则 p 还支持以下操作:

  • p+=i:使得 p 往后移动 i 个元素。
  • p-=i:使得 p 往前移动 i 个元素。
  • p+i:返回 p 后面第 i 个元素的迭代器。
  • p-i:返回 p 前面第 i 个元素的迭代器。
  • p[i]:返回 p 后面第 i 个元素的引用。

此外,两个随机访问迭代器 p1、p2 还可以用 <、>、<=、>= 运算符进行比较。另外,表达式 p2-p1 也是有定义的,其返回值表示 p2 所指向元素和 p1 所指向元素的序号之差(也可以说是 p2 和 p1 之间的元素个数减一)。
表 1 所示,是 C++ 11 标准中不同容器指定使用的迭代器类型。

容器对应的迭代器类型
array随机访问迭代器
vector随机访问迭代器
deque随机访问迭代器
list双向迭代器
set / multiset双向迭代器
map / multimap双向迭代器
forward_list前向迭代器
unordered_map / unordered_multimap前向迭代器
unordered_set / unordered_multiset前向迭代器
stack不支持迭代器
queue不支持迭代器

 

注意,容器适配器 stack 和 queue 没有迭代器,它们包含有一些成员函数,可以用来对元素进行访问。

迭代器的定义方式

尽管不同容器对应着不同类别的迭代器,但这些迭代器有着较为统一的定义方式,具体分为 4 种,如表 1 所示。

表 2 迭代器的 4 种定义方式

迭代器定义方式具体格式
正向迭代器容器类名::iterator  迭代器名;
常量正向迭代器容器类名::const_iterator  迭代器名;
反向迭代器容器类名::reverse_iterator  迭代器名;
常量反向迭代器容器类名::const_reverse_iterator  迭代器名;

值得一提的是,表 2 中的反向迭代器全称为 "反向迭代器适配器",后续章节会做详细讲解,这里读者只需要知道其用法即可。

通过定义以上几种迭代器,就可以读取它指向的元素,*迭代器名 就表示迭代器指向的元素。其中,常量迭代器和非常量迭代器的分别在于,通过非常量迭代器还能修改其指向的元素。另外,反向迭代器和正向迭代器的区别在于:

  • 对正向迭代器进行 ++ 操作时,迭代器会指向容器中的后一个元素;
  • 而对反向迭代器进行 ++ 操作时,迭代器会指向容器中的前一个元素。

注意,以上 4 种定义迭代器的方式,并不是每个容器都适用。有一部分容器同时支持以上 4 种方式,比如 array、deque、vector;而有些容器只支持其中部分的定义方式,例如 forward_list 容器只支持定义正向迭代器,不支持定义反向迭代器。具体容器支持定义迭代器的方式,讲具体容器时会详细说明。另外,读者也可以通过 C++ STL标准手册,查询具体容器迭代器支持的定义方式。

以上对迭代器做了很详细的介绍,下面就以 vector 容器为例,带领大家实际感受迭代器的用法和功能。通过前面的学习,vector 支持随机访问迭代器,因此遍历 vector 容器有以下几种做法。下面的程序中,每个循环演示了一种做法:

//遍历 vector 容器。
#include <iostream>
//需要引入 vector 头文件
#include <vector>
using namespace std;
int main()
{
    vector<int> v{1,2,3,4,5,6,7,8,9,10}; //v被初始化成有10个元素
    cout << "第一种遍历方法:" << endl;
    //size返回元素个数
    for (int i = 0; i < v.size(); ++i)
        cout << v[i] <<" "; //像普通数组一样使用vector容器
    //创建一个正向迭代器,当然,vector也支持其他 3 种定义迭代器的方式
    
       cout << endl << "第二种遍历方法:" << endl;
       vector<int>::iterator i;
    //用 != 比较两个迭代器
    for (i = v.begin(); i != v.end(); ++i)
        cout << *i << " ";
    
       cout << endl << "第三种遍历方法:" << endl;
    for (i = v.begin(); i < v.end(); ++i) //用 < 比较两个迭代器
        cout << *i << " ";
   
       cout << endl << "第四种遍历方法:" << endl;
    i = v.begin();
    while (i < v.end()) { //间隔一个输出
        cout << *i << " ";
        i += 2; // 随机访问迭代器支持 "+= 整数"  的操作
    }
}
第一种遍历方法:
1 2 3 4 5 6 7 8 9 10
第二种遍历方法:
1 2 3 4 5 6 7 8 9 10
第三种遍历方法:
1 2 3 4 5 6 7 8 9 10
第四种遍历方法:
1 3 5 7 9

再举一个例子,我们知道,list 容器的迭代器是双向迭代器。假设 v 和 i 的定义如下:

//创建一个 v list容器
list<int> v;
//创建一个常量正向迭代器,同样,list也支持其他三种定义迭代器的方式。
list<int>::const_iterator i;

则以下代码是合法的:

for(i = v.begin(); i != v.end(); ++i)
    cout << *i;

以下代码则不合法,因为双向迭代器不支持用“<”进行比较:

for(i = v.begin(); i < v.end(); ++i)
    cout << *i;

以下代码也不合法,因为双向迭代器不支持用下标随机访问元素:

for(int i=0; i<v.size(); ++i)
    cout << v[i];

其实在 C++ 中,数组也是容器。数组的迭代器就是指针,而且是随机访问迭代器。例如,对于数组 int a[10],int * 类型的指针就是其迭代器。则 a、a+1、a+2 都是 a 的迭代器。

C++序列式容器(STL序列式容器)是什么

所谓序列容器,即以线性排列(类似普通数组的存储方式)来存储某一指定类型(例如 int、double 等)的数据,需要特殊说明的是,该类容器并不会自动对存储的元素按照值的大小进行排序。

需要注意的是,序列容器只是一类容器的统称,并不指具体的某个容器,序列容器大致包含以下几类容器:

  • array<T,N>(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;
  • vector<T>(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);
  • deque<T>(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;
  • list<T>(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list<T> 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
  • forward_list<T>(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

注意,其实除此之外,stack<T> 和 queue<T> 本质上也属于序列容器,只不过它们都是在 deque 容器的基础上改头换面而成,通常更习惯称它们为容器适配器,有关它们的介绍,会放到后续章节中。

图 1 说明了可供使用的序列容器以及它们之间的区别。

图 1 标准的序列容器

图 1 中每种类型容器的操作都可以高效执行,但进行除此之外的其他操作,效率会稍差一些。在本章的剩余部分,会详细介绍每一类序列容器的具体用法。

容器中常见的函数成员

序列容器包含一些相同的成员函数,它们的功能也相同。

表 2 展示了 array、vector 和 deque 容器的函数成员,它们中至少有两个容器实现了同样的函数成员。

表 2 array、vector 和 deque 容器的函数成员

函数成员函数功能array<T,N>vector<T>deque<T>
begin()返回指向容器中第一个元素的迭代器。
end()返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin()返回指向最后一个元素的迭代器。
rend()返回指向第一个元素所在位置前一个位置的迭代器。
cbegin()和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend()和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin()和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend()和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
assign()用新元素替换原有内容。-
operator=()复制同类型容器的元素,或者用初始化列表替换现有内容。
size()返回实际元素个数。
max_size()返回元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
capacity()返回当前容量。--
empty()判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
resize()改变实际元素的个数。-
shrink _to_fit()将内存减少到等于当前元素实际所使用的大小。-
front()返回第一个元素的引用。
back()返回最后一个元素的引用。
operator[]()使用索引访问元素。
at()使用经过边界检査的索引访问元素。
push_back()在序列的尾部添加一个元素。-
insert()在指定的位置插入一个或多个元素。-
emplace()在指定的位置直接生成一个元素。-
emplace_back()在序列尾部生成一个元素。-
pop_back()移出序列尾部的元素。-
erase()移出一个元素或一段元素。-
clear()移出所有的元素,容器大小变为 0。-
swap()交换两个容器的所有元素。
data()返回指向容器中第一个元素的指针-

表 3 list 和 forward_list 的函数成员

函数成员函数功能list<T>forward_list<T>
begin()返回指向容器中第一个元素的迭代器。
end()返回指向容器最后一个元素所在位置后一个位置的迭代器。
rbegin()返回指向最后一个元素的迭代器。-
rend()返回指向第一个元素所在位置前一个位置的迭代器。-
cbegin()和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
before_begin()返回指向第一个元素前一个位置的迭代器。-
cbefore_begin()和 before_begin() 功能相同,只不过在其基础上,增加了 const 属性,即不能用该指针修改元素的值。-
cend()和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin()和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。-
crend()和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。-
assign()用新元素替换原有内容。
operator=()复制同类型容器的元素,或者用初始化列表替换现有内容。
size()返回实际元素个数。-
max_size()返回元素个数的最大值,这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
resize()改变实际元素的个数。
empty()判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
front()返回容器中第一个元素的引用。
back()返回容器中最后一个元素的引用。-
push_back()在序列的尾部添加一个元素。-
push_front()在序列的起始位置添加一个元素。
emplace()在指定位置直接生成一个元素。-
emplace_after()在指定位置的后面直接生成一个元素。-
emplace_back()在序列尾部生成一个元素。-
cmplacc_front()在序列的起始位生成一个元索。
insert()在指定的位置插入一个或多个元素。-
insert_after()在指定位置的后面插入一个或多个元素。-
pop_back()移除序列尾部的元素。-
pop_front()移除序列头部的元素。
reverse()反转容器中某一段的元素。
erase()移除指定位置的一个元素或一段元素。-
erase_after()移除指定位置后面的一个元素或一段元素。-
remove()移除所有和参数匹配的元素。
remove_if()移除满足一元函数条件的所有元素。
unique()移除所有连续重复的元素。
clear()移除所有的元素,容器大小变为 0。
swap()交换两个容器的所有元素。
sort()对元素进行排序。
merge()合并两个有序容器。
splice()移动指定位置前面的所有元素到另一个同类型的 list 中。-
splice_after()移动指定位置后面的所有元素到另一个同类型的 list 中。-

C++ array(STL array)容器用法详解

array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全(原因后续会讲),且效率并没有因此变差。和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。

STL 还提供有可动态扩展或收缩存储空间的 vector 容器,后续章节会对其做详细介绍。

array 容器以类模板的形式定义在 <array> 头文件,并位于命名空间 std 中,如下所示:

namespace std{
    template <typename T, size_t N>
    class array;
}

因此,在使用该容器之前,代码中需引入 <array> 头文件,并默认使用 std 命令空间,如下所示:

#include <array>
using namespace std;

在 array<T,N> 类模板中,T 用于指明容器中的存储的具体数据类型,N 用于指明容器的大小,需要注意的是,这里的 N 必须是常量,不能用变量表示。array 容器有多种初始化方式,如下代码展示了如何创建具有 10 个 double 类型元素的 array 容器:

std::array<double, 10> values;

提示,如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。初始化:

std::array<double, 10> values {};//初始化为0.0
std::array<double, 10> values {0.5,1.0,1.5,,2.0}; //剩下元素初始化为0.0

表 2 array容器成员函数汇总

成员函数功能
begin()返回指向容器中第一个元素的随机访问迭代器。
end()返回指向容器最后一个元素之后一个位置的随机访问迭代器,通常和 begin() 结合使用。
rbegin()返回指向最后一个元素的随机访问迭代器。
rend()返回指向第一个元素之前一个位置的随机访问迭代器。
cbegin()和 begin() 功能相同,只不过在其基础上增加了 const 属性,不能用于修改元素。
cend()和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin()和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend()和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size()返回容器中当前元素的数量,其值始终等于初始化 array 类的第二个模板参数 N。
max_size()返回容器可容纳元素的最大数量,其值始终等于初始化 array 类的第二个模板参数 N。
empty()判断容器是否为空,和通过 size()==0 的判断条件功能相同,但其效率可能更快。
at(n)返回容器中 n 位置处元素的引用,该函数自动检查 n 是否在有效的范围内,如果不是则抛出 out_of_range 异常。
front()返回容器中第一个元素的直接引用,该函数不适用于空的 array 容器。
back()返回容器中最后一个元素的直接应用,该函数同样不适用于空的 array 容器。
data()返回一个指向容器首个元素的指针。利用该指针,可实现复制容器中所有元素等类似功能。
fill(val)将 val 这个值赋值给容器中的每个元素。
array1.swap(array2)交换 array1 和 array2 容器中的所有元素,但前提是它们具有相同的长度和类型。

除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 array 容器包含的 begin() 和 end() 成员函数不同的是,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。

另外,在 <array> 头文件中还重载了 get() 全局函数,该重载函数的功能是访问容器中指定的元素,并返回该元素的引用。

正是由于 array 容器中包含了 at() 这样的成员函数,使得操作元素时比普通数组更安全。

#include <iostream>
//需要引入 array 头文件
#include <array>
using namespace std;
int main()
{
    std::array<int, 4> values{};
    //初始化 values 容器为 {0,1,2,3}
    for (int i = 0; i < values.size(); i++) {
        values.at(i) = i;
    }
    //使用 get() 重载函数输出指定位置元素
    cout << get<3>(values) << endl;
    //如果容器不为空,则输出容器中所有的元素
    if (!values.empty()) {
        for (auto val = values.begin(); val < values.end(); val++) {
            cout << *val << " ";
        }
    }
}

注意,代码中的 auto 关键字,可以使编译器自动判定变量的类型。运行这段代码,输出结果为:

3
0 1 2 3

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值