C++ STL 的来龙去脉

引言

对一个东西的认知过程,我们往往会存在如下几个问题:

  • 它是啥,
  • 它是咋来的,
  • 它有什么用
  • 怎么使用它。

一般而言,解决了这几个问题,我们就差不多对这个东西有一定的了解了。这篇博客就以上几个问题为主线,一层一层揭开STL的面纱。(由于本人写这篇博客的时候,对STL的了解尚浅,边学边写,博客内容也大多来源于网络,表述不当或有误之处,还望各位不吝赐教🙏)

一、简介

1 STL是啥?

STL即为标准模板库的英文(Standard Template Library)的缩写。STL是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如向量、链表、队列、栈。

STL的核心包括以下三个组件:

组件描述
容器(Containers)容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。
算法(Algorithms)算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。
迭代器(iterators)迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。

实际上,STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的,它们各自的含义如下表所示:

组成含义
容器一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。
算法STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 中,少部分位于头文件 中。
迭代器在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。
函数对象如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。
适配器可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。
内存分配器为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。

另外,在惠普实验室最初发行的版本中,STL 被组织成 48 个头文件;但在 C++ 标准中,它们被重新组织为 13 个头文件,如所示:

//7个数据结构
#include <vector>      // 定义vector序列模板,这是一个大小可以重新设置的数组类型,比普通的数组更安全,更灵活
#include <deque>       // 定义deque序列模板,支持在开始和结尾的高效插入和删除操作
#include <queue>       // 为队列(先进先出)数据结构定义序列适配器queue和priority_queue
#include <stack>       // 为堆栈(后进先出)数据结构定义序列适配器stack
#include <list>        // 定义list序列模板,这是一个序列的链表,常常在任意位置插入和删除元素
#include <set>         // set是一个关联容器类型,用于以升序方式存储唯一值。
#include <map>         // map是一个关联容器类型,允许根据键值是唯一的,且按照升序存储。

#include <algorithm>    // 提供一组给予算法的函数,包括置换、排序、合并和搜索
#include <numeric>      // 在数值序列上定义一组一般数学操作
#include <functional>   // 定义了许多函数对象类型和支持函数对象的功能,函数对象是支持operator()函数调用运算符的任意对象
#include <iterator>     // 给迭代器提供定义和支持
#include <memory>       // 给容器、管理内存的函数和auto_ptr模板类定义标准内存分配器
#include <utility>      // 定义重载的关系运算符,简化关系运算符的写入,它还定义了pair类型

注:

  • 按照 C++ 标准库的规定,所有标准头文件都不再有扩展名。以 <vector> 为例,此为无扩展名的形式,而 <vector.h> 为有扩展名的形式。
  • 但是,或许是为了向下兼容,或许是为了内部组织规划,某些 STL 版本同时存储具备扩展名和无扩展名的两份文件(例如 Visual C++ 支持的 Dinkumware 版本同时具备 <vector.h><vector>);甚至有些 STL 版本同时拥有 3 种形式的头文件(例如 SGI 版本同时拥有 <vector>、<vector.h><stl_vector.h>);但也有个别的 STL 版本只存在包含扩展名的头文件(例如 C++ Builder 的 RaugeWare 版本只有 <vector.h>)。
  • 建议读者养成良好的习惯,遵照 C++ 规范,使用无扩展名的头文件。

2 它是咋来的?

Alexander Stepanov(亚历山大·斯特潘诺夫,下图;后被誉为 STL 标准模板库之父,后简称 Stepanov),1950 年出生与前苏联的莫斯科,他曾在莫斯科大学研究数学,此后一直致力于计算机语言和泛型库研究。
在这里插入图片描述

在 20 世纪 70 年代,Stepanov 开始考虑,在保证效率的前提下,是否能将算法从诸多具体应用之中抽象出来?为了验证自己的思想,他和纽约州立大学教授 Deepak Kapur 以及伦塞里尔技术学院教授 David Musser 共同开发了一种叫做 Tecton 的语言,尽管这次尝试没有取得实用性的成果,但却给了 Stepanov 很大的启示。

在随后的几年中,他又和 David Musser 等人先后用 Schema 语言(一种 Lisp 语言的变种)和 Ada 语言建立了一些大型程序库。Stepanov 逐渐意识到,在当时的面向对象程序设计思想中存在一些问题,比如抽象数据类型概念所存在的缺陷,他希望通过对软件领域中各组成部分的分类,逐渐形成一种软件设计的概念性框架。

1987 年,在贝尔实验室工作的 Stepanov 开始首次采用 C++ 语言进行泛型软件库的研究。由于当时的 C++ 语言还没有引入模板的编程技术,泛型库只能是通过 C++ 的继承机制来开发,代码表达起来非常笨拙。

但尽管如此,Stepanov 还是开发出了一个庞大的算法库。与此同时,在与 Andrew Koenig(前 ISO C++ 标准化委员会主席)和 Bjarne Stroustrup(C++ 语言的创始人)等顶级大师们的共事过程中,Stepanov 开始注意到 C/C++ 语言在实现其泛型思想方面所具有的潜在优势。

就拿 C/C++ 中的指针而言,它的灵活与高效运用使后来的 STL 在实现泛型化的同时更是保持了高效率。另外,在 STL 中占据极其重要地位的迭代器概念便是源自于 C/C++ 中原生指针的一般化推广。

1988 年,Stepanov 开始进入惠普的 Palo Alto 实验室工作,在随后的 4 年中,他从事的是有关磁盘驱动器方面的工作。直到 1992 年,由于参加并主持了实验室主任 Bill Worley 所建立的一个有关算法的研究项目,才使他重新回到了泛型化算法的研究工作上来。

项目自建立之后,参与者从最初的 8 人逐渐减少,最后只剩下 Stepanov 和 Meng Lee 两个人。经过长时间的努力,最终完成了一个包含有大量数据结构和算法部件的庞大运行库(HP 版本的 C++ STL),这便是现在 STL 的雏形。

1993 年,当时在贝尔实验室的 Andrew Koenig 看到了 Stepanov 的研究成果,在他的鼓励与帮助下,Stepanov 于 1993 年 9 月在圣何塞为 ANSI/ISO C++ 标准委员会做了一个题为“The Science of C++ Programming” 的演讲,向委员们讲述了其观念。然后又于 1994 年 3 月,在圣迭戈会议上向委员会提交了一份建议书,以期将 STL 通用库纳入 C++ 标准。

尽管这一建议十分庞大,以至于降低了被通过的可能性,但其所包含的新思想吸引了许多人的注意力。随后在众人的帮助之下,包括 Bjame Stroustrup 在内,Stepanov 又对 STL 进行了改进,同时加入了一个封装内存模式信息的抽象模块,也就是现在 STL 中的 allocator(内存分配器),它使 STL 的大部分实现都可以独立于具体的内存模式,从而独立于具体平台。

最终在 1994 年的滑铁卢会议上,委员们通过了提案,决定将 STL 正式纳入 C++ 标准化进程之中,随后 STL 便被放进了会议的工作文件中。自此,STL 终于成为 C++ 家族中的重要一员。

此后,随者 C++ 标准的不断改进,STL 也在不断地做着相应的演化。直至 1998 年,ANSI/ISO C++ 标准正式定案,STL 始终是 C++ 标准库不可或缺的重要组成部分。

3 它有什么用?

为了更加清楚地了解使用 STL 编程有哪些优势,这里举一个使用 STL 的例子。
以 C++ 定义数组的操作为例,在 C++ 中如果定义一个数组,可以采用如下方式:

int a[n];

这种定义数组的方法需要事先确定好数组的长度,即 n 必须为常量,这意味着,如果在实际应用中无法确定数组长度,则一般会将数组长度设为可能的最大值,但这极有可能导致存储空间的浪费。

所以除此之外,还可以采用在堆空间中动态申请内存的方法,此时长度可以是变量:

int *p = new int[n];

这种定义方式可根据变量 n 动态申请内存,不会出现存储空间浪费的问题。但是,如果程序执行过程中出现空间不足的情况时,则需要加大存储空间,此时需要进行如下操作:

  1. 新申请一个较大的内存空间,即执行int * temp = new int[m];
  2. 将原内存空间的数据全部复制到新申请的内存空间中,即执行memecpy(temp, p, sizeof(int)*n);
  3. 将原来的堆空间释放,即执行delete [] p; p = temp;

而完成相同的操作,如果采用 STL 标准库,则会简单很多,因为大多数操作细节将不需要程序员关心。下面是使用向量模板类 vector 实现以上功能的示例:

vector <int> a; //定义 a 数组,当前数组长度为 0,但和普通数组不同的是,此数组 a 可以根据存储数据的数量自动变长。
//向数组 a 中添加 10 个元素
for (int i = 0; i < 10 ; i++)
    a.push_back(i)
//还可以手动调整数组 a 的大小
a.resize(100);
a[90] = 100;
//还可以直接删除数组 a 中所有的元素,此时 a 的长度变为 0
a.clear();
//重新调整 a 的大小为 20,并存储 20 个 -1 元素。
a.resize(20, -1)

对比以上两种使用数组的方式不难看出,使用 STL 可以更加方便灵活地处理数据。所以,大家只需要系统地学习 STL,便可以集中精力去实现程序的功能,而无需再纠结某些细节如何用代码实现。

4 STL版本有哪些?

自 1998 年 ANSI/ISO C++ 标准正式定案,C++ STL 规范版本正式通过以后,由于其实开源的,各个 C++ 编译器厂商在此标准的基础上,实现了满足自己需求的 C++ STL 泛型库,主要包括 HP STL、SGI STL、STLport、PJ STL、Rouge Wave STL 等。

版本描述
HP STLHP STL 是 Alexandar Stepanov(STL 标准模板库之父,文章后续简称 Stepanov)在惠普 Palo Alto 实验室工作时,与 Meng Lee 合作完成的。HP STL 是开放源码的,即任何人都可以免费使用、复制、修改、发布和销售该软件以及相关文档,但前提是必须在相关文档中,加入 HP STL 版本信息和授权信息。HP STL 是 C++ STL 的第一个实现版本,其它版本的 C++ STL 一般是以 HP STL 为蓝本实现出来的。不过,现在已经很少直接使用此版本的 STL 了。
SGI STLStepanov 在离开 HP 之后,就加入到了 SGI 公司,并和 Matt Austern 等人开发了 SGI STL。严格意义上来说,它是 HP STL 的一个继承版本。和 HP STL 一样,SGI STL 也是开源的,其源代码的可读性可非常好,并且任何人都可以修改和销售它。
注意,和 STL 官方版本来说,SGI STL 只能算是一个“民间”版本,因此并不是所有支持 C++ 的编译器都支持使用 SGI STL 模板库,唯一能确定的是,GCC(Linux 下的 C++ 编译器)是支持的,所以 SGI STL 在 Linux 平台上的性能非常出色。
STLport为了使 SGI STL 的基本代码都适用于 VC++ 和 C++ Builder 等多种编译器,俄国人 Boris Fomitchev 建立了一个 free 项目来开发 STLport,此版本 STL 是开放源码的。
PJ STL(全称为 P.J. Plauger STL)是由 P.J.Plauger(美国人,1965 年毕业于普林斯顿大学,物理专业学士)参照 HP STL 实现出来的,也是 HP STL 的一个继承版本,因此该头文件中不仅含有 HP STL 的相关授权信息,同时还有 P.J.Plauger 本人的版权信息。其实 PJ STL 是 P.J.Plauger 公司的产品,尽管该公司当时只有 3 个人。PJ STL 被 Visual C++ 编译器所采用,但和 PH STL、SGI STL 不同的是,PJ STL 并不是开源。
Rouge Wave STL该版本的 STL 是由 Rouge Wave 公司开发的,也是继承 HP STL 的一个版本,它也不是开源的。Rouge Wave STL 用于 Borland C++ Builder 编译器中,我们可以在 C++ Builder 的 Inculde 子目录中找到该 STL 的所有头文件。值得一提的是,尽管 Rouge Wave STL 的性能不是很好,但 C++ Builder 对 C++ 语言标准的支持还算不错,所以在一定程度上使 Rouge Wave STL 的表现得以改善。遗憾的是,由于 Rouge Wave STL 长期没有更新且不完全符合标准,因此 Rouge Wave STL 在 6.0 版本时改用了 STLport 版本(之后的版本也都采用了 STLport),不过考虑到和之前版本的兼容,6.0 版本中依旧保留了 Rouge Wave STL。
Rouge Wave 公司在 C++ 程序库领域应该说是鼎鼎大名,对 C++ 标准化的过程出力甚多。不过 Rouge Wave STL 版本不仅更新频率慢,费用还高,基于这两个原因,Borland 在 6.0 版本决定弃用 Rouge Wave STL 而改用 STLport。

二、使用

关于几个结构的使用,突然发现有篇博文写的很好,不想重复造轮子,所以看下面这个链接就行。

https://blog.csdn.net/qq_42322103/article/details/99685797

1 知识点

下面是一些我觉得比较精华的知识点:

1、根据数据在容器中的排列特性,这些数据分为序列式容器和关联式容器两种。

  • 序列式容器强调值的排序,序列式容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。Vector容器、Deque容器、List容器等。
  • 关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。关联式容器另一个显著特点是:在值中选择一个值作为关键字key,这个关键字对值起到索引的作用,方便查找。Set/multiset容器 Map/multimap容器。

2、array、vector和deque的区别:array无法成长;vector虽可成长,却只能向尾端成长,而且其成长其实是一个假象,事实上(1) 申请更大空间 (2)原数据复制新空间 (3)释放原空间 三步骤,如果不是vector每次配置新的空间时都留有余裕,其成长假象所带来的代价是非常昂贵的;Deque是由一段一段的定量的连续空间构成。两端都可成长。

vector<int> v;
Vector<Teacher> T;//Teacher 是一个用户自定义类
Vector<int>::iterator it1; // it1的型别其实就是Int*,
Vector<Teacher>::iterator it2; // it2的型别其实就是Teacher*.

3、Multimap和map的操作类似,唯一区别multimap键值可重复。multiset特性及用法和set完全相同,唯一的差别在于它允许键值重复。这四个容器的底层实现都是红黑树。

4、重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象,也叫仿函数(functor),其实就是重载“()”操作符,使得类对象可以像函数那样调用。函数对象(仿函数)是一个类,不是一个函数。函数对象(仿函数)重载了”() ”操作符使得它可以像函数一样调用。

  • 1、函数对象通常不定义构造函数和析构函数,所以在构造和析构时不会发生任何问题,避免了函数调用的运行时问题。
  • 2、函数对象超出普通函数的概念,函数对象可以有自己的状态
  • 3、函数对象可内联编译,性能好。用函数指针几乎不可能
  • 4、模版函数对象使函数对象具有通用性,这也是它的优势之一

5、谓词 是指普通函数或重载的operator()返回值是bool类型的函数对象(仿函数)。如果operator接受一个参数,那么叫做一元谓词,如果接受两个参数,那么叫做二元谓词,谓词可作为一个判断式。

6、内建函数对象:STL内建了一些函数对象。分为:算数类函数对象,关系运算类函数对象,逻辑运算类仿函数。这些仿函数所产生的对象,用法和一般函数完全相同,当然我们还可以产生无名的临时对象来履行函数功能。

2 容器的构造函数

关于几个容器的构造函数:

string();//创建一个空的字符串 例如: string str;      
string(const string& str);//使用一个string对象初始化另一个string对象
string(const char* s);//使用字符串s初始化
string(int n, char c);//使用n个字符c初始化 

vector<T> v; //采用模板实现类实现,默认构造函数
vector(v.begin(), v.end());//将v[begin(), end())区间中的元素拷贝给本身。
vector(n, elem);//构造函数将n个elem拷贝给本身。
vector(const vector &vec);//拷贝构造函数。

deque<T> deqT;//默认构造形式
deque(beg, end);//构造函数将[beg, end)区间中的元素拷贝给本身。
deque(n, elem);//构造函数将n个elem拷贝给本身。
deque(const deque &deq);//拷贝构造函数。

stack<T> stkT;//stack采用模板类实现, stack对象的默认构造形式: 
stack(const stack &stk);//拷贝构造函数

queue<T> queT;//queue采用模板类实现,queue对象的默认构造形式:
queue(const queue &que);//拷贝构造函数

list<T> lstT;//list采用采用模板类实现,对象的默认构造形式:
list(beg,end);//构造函数将[beg, end)区间中的元素拷贝给本身。
list(n,elem);//构造函数将n个elem拷贝给本身。
list(const list &lst);//拷贝构造函数。

set<T> st;//set默认构造函数:
mulitset<T> mst; //multiset默认构造函数: 
set(const set &st);//拷贝构造函数

//对组:将一对值合成一个值
template <class T1, class T2> struct pair.
//创建对组的几种方式
//第一种方法创建一个对组
pair<string, int> pair1(string("name"), 20);
cout << pair1.first << endl; //访问pair第一个值
cout << pair1.second << endl;//访问pair第二个值
//第二种
pair<string, int> pair2 = make_pair("name", 30);
cout << pair2.first << endl;
cout << pair2.second << endl;
//pair=赋值
pair<string, int> pair3 = pair2;
cout << pair3.first << endl;
cout << pair3.second << endl;

map<T1, T2> mapTT;//map默认构造函数: 
map(const map &mp);//拷贝构造函数
//向 map 中插入元素的几种方式
map.insert(...); //往容器插入元素,返回pair<iterator,bool>
map<int, string> mapStu;
// 第一种 通过pair的方式插入对象
mapStu.insert(pair<int, string>(3, "小张"));
// 第二种 通过pair的方式插入对象
mapStu.inset(make_pair(-1, "校长"));
// 第三种 通过value_type的方式插入对象
mapStu.insert(map<int, string>::value_type(1, "小李"));
// 第四种 通过数组的方式插入值
mapStu[3] = "小刘";
mapStu[5] = "小王";

3 使用时机

Itemvectordequelistsetmultisetmapmultimap
典型内存结构单端数组双端数组双向链表二叉树二叉树二叉树二叉树
可随机存取对key而言:不是
元素搜寻速度非常慢对key而言:快对key而言:快
元素安插移除尾端头尾两端任何位置----

几大容器的使用场景:

  • vector的使用场景:比如软件历史操作记录的存储,我们经常要查看历史记录,比如上一次的记录,上上次的记录,但却不会去删除记录,因为记录是事实的描述。

  • deque的使用场景:比如排队购票系统,对排队者的存储可以采用deque,支持头端的快速移除,尾端的快速添加。如果采用vector,则头端移除时,会移动大量的数据,速度慢。

  • list的使用场景:比如公交车乘客的存储,随时可能有乘客下车,支持频繁的不确实位置元素的移除插入。

  • set的使用场景:比如对手机游戏的个人得分记录的存储,存储要求从高分到低分的顺序排列。

  • map的使用场景:比如按ID号存储十万个用户,想要快速要通过ID查找对应的用户。二叉树的查找效率,这时就体现出来了。如果是vector容器,最坏的情况下可能要遍历完整个容器才能找到该用户。

vector与deque的比较:

  • vector.at()比deque.at()效率高,比如vector.at(0)是固定的,deque的开始位置 却是不固定的。
  • 如果有大量释放操作的话,vector花的时间更少,这跟二者的内部实现有关。
  • deque支持头部的快速插入与快速移除,这是deque的优点。

参考

C语言中文网
菜鸟教程
CSDN 优秀博文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流浪猪头拯救地球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值