【C++进阶 · 标准模板库】: STL 初体验

902 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

1、写在前面

大家好,我是翼同学。今天文章的内容是:

  • STL初体验

2、内容

2.1、概念

STL的概念如下:

STL(Standard Template Library),即标准模板库,是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构。也就是说,STL为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。

如今STL已完全被内置到支持 C++ 的编译器中,无需额外安装,位于各个C++的头文件中,以源代码的形式提供。

另外,C++ STL一般可分为六大组件,分别是容器、算法、迭代器、仿函数、适配器以及空间配置器。

其中:

  • 仿函数Functor):行为类似函数,可作为算法的某种策略
  • 适配器Adaptor):用于修饰容器、仿函数或者迭代器接口
  • 空间配置器allocator):负责空间的配置与管理

而容器、算法、迭代器则是STL的三大核心组件,现简单记录如下。

2.2、容器

容器Containers),是某种数据结构的统称,用于管理某一类对象的集合,比如vectormaplistdeque等。为了访问容器中的数据,可以使用由容器类输出的迭代器。在STL中,容器能自动申请和释放内存,无需newdelete操作。

一般来说,容器可分为序列式容器和关联式容器。

序列式容器

所谓序列容器Sequence containers就是一类容器的统称,这种容器的特点是线性排列某种特定类型的数据(其存储方式类似数组),其中每个元素都有固定位置,并且位置取决于插入的时机和地点。

标准库提供了几种序列式容器,如下所示:

  • array<T, N>: 数组容器,可存储N个T类型的元素,这种容器的特点是,一旦建立后,容器的大小就是固定不变的,即容量固定。
  • Vector<T>: 向量容器,可存放T类型的数据元素,特点是长度可变,在存储空间不足时,会自动申请更多的内存。该容器在尾部增加或删除元素的效率最高(时间复杂度为O(1)O(1)),在其它位置插入或删除元素效率较差(O(n)O(n))。
  • deque<T>: 双端队列容器,类似Vector<T>,区别在于,该容器不仅尾部插入和删除元素高效(时间复杂度为O(1)O(1)),在头部插入或删除元素也同样高效(时间复杂度为O(1)O(1)),但是在容器中某一位置处插入或删除元素时效率就较低(时间复杂度为O(n)O(n))。
  • list<T>: 双向链表容器,长度可变,由T类型数据元素组成的容器,可在任意位置上插入或删除元素(内部只需操作指针即可,时间复杂度为O(1)O(1)),但是访问该容器中元素的速度就较慢,因为该容器必须从首元素或末元素开始,沿着链表逐一访问元素。
  • forward_list<T>: 正向链表容器,和list<T>类似,但以单链表的形式组织元素,访问指定元素时只能从首元素开始,逐一访问,是一类比list<T>更快,更节省内存的容器。

关联式容器

前面讲的序列式容器存储的数据元素都是单一的基本数据类型或者结构体自定义类型数据,而关联式容器与序列式容器的不同点在于,关联式容器在存储数据元素的同时,还为每个元素配上一个值,这个值被称为“键”。

这样的好处在于,我们在使用关联式容器时,可以直接通过“键”来获取对应的数据元素。而无需遍历整个容器来得到元素。因此关联式容器的特点在于快速查找、读取或者删除所存储的元素,同时插入元素的效率也比序列式容器高。

标准库提供了几种序列式容器,如下所示:

  • map: 该容器定义在<map>头文件中,其中每个数据元素都有唯一的键,并且相同数值的元素只能出现一次。另外,容器会根据键的大小默认对数据元素进行升序排序。
  • set: 该容器定义在<set>头文件中,其中每个数据元素的值都和键值相同,并且相同数值的元素只能出现一次。该容器也会自动根据各个元素的键值大小进行升序排序。
  • multimap: 该容器定义在<map>头文件中,与map容器的不同在于multimap容器中存储元素的键可以重复。
  • multiset: 该容器定义在<set>头文件中,与set容器的不同在于multiset容器中存储元素的值可以重复(值重复代表着键也是重复的)。

可以注意到:map中的元素是键值对(key-value):键起到索引的作用,值表示与键相关联的数据;而set就是键的简单集合(set中的值和键是相同的)

上述容器中,底层使用的都是红黑树,而C++ 11新增了几种哈希容器,底层采用了哈希表,严格来说也属于关联式容器,如unordered_mapunordered_multimapunordered_setunordered_multiset.

2.3、算法

算法Algorithms),用来操作容器中的数据的模板函数。STL提供了很多数据结构算法,它们都被设计成一个个的模板函数,包括对容器内容执行初始化、排序、搜索和转换等操作。这些算法在std命名空间中定义,其中大部分算法都包含在头文件<algorithm>中,少部分位于头文件<numeric><functional>中。一般算法可以分为两类,一是可变序列算法,即可以修改容器中的内容;另一种则是非可变序列算法,即不会直接修改容器中的内容。

头文件记录:

  • <algorithm>: 由许多模版函数组成。函数功能如查找、修改、排序、合并、反转、比较以及交换等。
  • <numeric>: 体积小,定义一些简单数学运算的模板函数。
  • <functional>: 定义一些模板类,用于声明函数对象。

2.4、迭代器

对于容器而言,涉及最多的操作就是遍历容器中的数据元素。于是STL就利用了泛型技术,设计了适用于所有容器的通用算法,除了能对容器进行遍历读写数据,又能对外隐藏容器的内部差异,这也体现了泛型思维。

因此,迭代器iterators),提供了访问容器中数据元素的方法,而又不会暴露该容器的内部。有时可以将迭代器跟指针进行类比,迭代器可以是任意数据类型,通过迭代器,我们可以在不知道对象内部表示的情况下,按照一定的顺序访问对象中的各个数据元素,从而进行读写操作。

迭代器种类

  • 随机访问迭代器:支持读写操作,可以跳跃着访问任意数据,功能强大
  • 双向迭代器:支持读写操作,能向前或向后操作
  • 单向迭代器:支持读写操作,只能向前推进迭代器
  • 输入迭代器:对数据只读访问
  • 输出迭代器:对数据只写访问

一般来说,前两种迭代器使用的频率较高。

2.5、STL初体验

介绍了这么多,我们现在来举个小例子,感受一下STL编程。

之前我们定义数组一般是这样:

int arr[n];

这种定义数组的方式,n必须为常量,而且数组的大小必须先确定好。这时如果我们无法确定数组的长度,则会将长度设置为可能的最大值,有时会造成空间的浪费。

另外一种定义数组的方式则是在堆中创建,如下所示:

int *p = new int[n];

这种数组定义的方式可根据变量n来动态创建内存空间,确实不错。但是如果定义数组后,在程序运行阶段出现空间不够的情况,需要加大存储空间时,则需要进行以下操作:

  1. 重新创建一个较大的内存空间
  2. 将原数组中的数据元素复制到新数组中
  3. 再释放掉原先空间的内存

现在以向量容器vector<T>为例。

定义数组arr

vector<int> arr;    // 空数组,无任何元素

这里定义的数组arr,其长度为0,属于变长数组,也就是说,该数组的长度随数据元素的添加而自动变长。

当然,我们在定义数组时可以这样指定元素个数:

vector<int> arr(10);    // 容器 arr 中有 10 个元素,默认初始值为 0

需要注意,如果换成花括号,代表的含义就不同了:

vector<int> arr{10};    // 容器 arr 中只有一个元素 10

下面是数组的一些操作:

// 1. 向数组 arr 中添加数据元素
for(int i = 1; i <= 101; i++) {
    arr.push_back(i);
}
// 2. 去掉数组最后一个数据
arr.pop_back();
// 3. 调整数组大小,使其至少可以容纳249个元素
arr.resize(249);
// 4. 清空数组
arr.clear();

事实上,学习如何使用STL,可以让我们更加高效灵活的处理数据。


3、写在最后

好了,今天文章的内容就到这里,感谢观看。