2022 年年度编程语言揭榜啦!在上个月预想的 C++、C、Python 三种候选语言中,C++ 脱颖而出,成为 TIOBE 2022 年度编程语言的最终获得者!新的一波学习热潮要来了。
TIOBE 编程语言社区排行榜是编程语言流行趋势的一个指标,每月更新,这份排行榜排名基于全球技术工程师、课程和第三方供应商的数量,其中包括了流行的搜索引擎以及技术社区,如 Google、百度、维基百科、CSDN、必应、Hao 123 等等。具体的计算方式详见:https://www.tiobe.com/tiobe-index/programming-languages-definition/。请注意这个排行榜只是反映某个编程语言的热门程度,并不能说明一门编程语言好不好,或者一门语言所编写的代码数量多少。
这个排行榜可以用来考察你的编程技能是否与时俱进,也可以在开发新系统时作为一个语言选择依据。
今天主要讲的是C++STL的内容,废话不多说,安排!
STL,英文全称 standard template library,中文可译为标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能。
STL 最初由惠普实验室开发,于 1998 年被定为国际标准,正式成为 C++ 程序库的重要组成部分。值得一提的是,如今 STL 已完全被内置到支持 C++ 的编译器中,无需额外安装,这可能也是 STL 被广泛使用的原因之一。
STL 就位于各个 C++ 的头文件中,即它并非以二进制代码的形式提供,而是以源代码的形式提供。
从根本上说,STL 是一些容器、算法和其他一些组件的集合,所有容器和算法都是总结了几十年来算法和数据结构的研究成果,汇集了许多计算机专家学者经验的基础上实现的,因此可以说,STL 基本上达到了各种存储方法和相关算法的高度优化。
注意,这里提到的容器,本质上就是封装有数据结构的模板类,例如 list、vector、set、map 等,有关这些容器的具体用法,后续章节会做详细介绍。
学STL能干什么?
为了让读者清楚地了解 STL 是什么,使用 STL 编程有哪些优势,这里举一个使用 STL 的例子。
以 C++ 定义数组的操作为例,在 C++ 中如果定义一个数组,可以采用如下方式:
int a[n];
这种定义数组的方法需要事先确定好数组的长度,即 n 必须为常量,这意味着,如果在实际应用中无法确定数组长度,则一般会将数组长度设为可能的最大值,但这极有可能导致存储空间的浪费。
所以除此之外,还可以采用在堆空间中动态申请内存的方法,此时长度可以是变量:
int *p = new int[n];
这种定义方式可根据变量 n 动态申请内存,不会出现存储空间浪费的问题。但是,如果程序执行过程中出现空间不足的情况时,则需要加大存储空间,此时需要进行如下操作:
新申请一个较大的内存空间,即执行int * temp = new int[m];
将原内存空间的数据全部复制到新申请的内存空间中,即执行memecpy(temp, p, sizeof(int)*n);
将原来的堆空间释放,即执行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,便可以集中精力去实现程序的功能,而无需再纠结某些细节如何用代码实现。
C++ STL版本有哪些?
自 1998 年 ANSI/ISO C++ 标准正式定案,C++ STL 规范版本正式通过以后,由于其实开源的,各个 C++ 编译器厂商在此标准的基础上,实现了满足自己需求的 C++ STL 泛型库,主要包括 HP STL、SGI STL、STLport、PJ STL、Rouge Wave STL 等。
HP STL
HP STL 是 Alexandar Stepanov(STL 标准模板库之父,文章后续简称 Stepanov)在惠普 Palo Alto 实验室工作时,与 Meng Lee 合作完成的。HP STL 是开放源码的,即任何人都可以免费使用、复制、修改、发布和销售该软件以及相关文档,但前提是必须在相关文档中,加入 HP STL 版本信息和授权信息。
HP STL 是 C++ STL 的第一个实现版本,其它版本的 C++ STL 一般是以 HP STL 为蓝本实现出来的。不过,现在已经很少直接使用此版本的 STL 了。
SGI STL
Stepanov 在离开 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
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。
C++ STL基本组成(6大组件+13个头文件)
通常认为,STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的,它们各自的含义如下 所示。
容器
一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。
算法
STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 <algorithm> 中,少部分位于头文件 <numeric> 中。
迭代器
在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。
函数对象
如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。
适配器
可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。
内存分配器
为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。
另外,在惠普实验室最初发行的版本中,STL 被组织成 48 个头文件;但在 C++ 标准中,它们被重新组织为 13 个头文件
按照 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++ STL vector容器详解
vector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。
vector 常被称为向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)。
vector 容器以类模板 vector<T>( T 表示存储元素的类型)的形式定义在 <vector> 头文件中,并位于 std 命名空间中。因此,在创建该容器之前,代码中需包含如下内容:
#include <vector>
using namespace std;
注意,std 命名空间也可以在使用 vector 容器时额外注明,两种方式都可以。
创建vector容器的几种方式
创建 vector 容器的方式有很多,大致可分为以下几种。
1) 如下代码展示了如何创建存储 double 类型元素的一个 vector 容器:
std::vector<double> values;
如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。
注意,这是一个空的 vector 容器,因为容器中没有元素,所以没有为其分配空间。当添加第一个元素(比如使用 push_back() 函数)时,vector 会自动分配内存。
在创建好空容器的基础上,还可以像下面这样通过调用 reserve() 成员函数来增加容器的容量:
values.reserve(20);
这样就设置了容器的内存分配,即至少可以容纳 20 个元素。注意,如果 vector 的容量在执行此语句之前,已经大于或等于 20 个元素,那么这条语句什么也不做;另外,调用 reserve() 不会影响已存储的元素,也不会生成任何元素,即 values 容器内此时仍然没有任何元素。
还需注意的是,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector<T> 容器的元素可能已经被复制或移到了新的内存地址。所以后续再使用这些迭代器时,最好重新生成一下。
2) 除了创建空 vector 容器外,还可以在创建的同时指定初始值以及元素个数,比如:
std::vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};
这样就创建了一个含有 8 个素数的 vector 容器。
3) 在创建 vector 容器时,也可以指定元素个数:
std::vector<double> values(20);
如此,values 容器开始时就有 20 个元素,它们的默认初始值都为 0。
注意,圆括号 () 和大括号 {} 是有区别的,前者(例如 (20) )表示元素的个数,而后者(例如 {20} ) 则表示 vector 容器中只有一个元素 20。
如果不想用 0 作为默认值,也可以指定一个其它值,例如:
std::vector<double> values(20, 1.0);
第二个参数指定了所有元素的初始值,因此这 20 个元素的值都是 1.0。
值得一提的是,圆括号 () 中的 2 个参数,既可以是常量,也可以用变量来表示,例如:
int num=20;
double value =1.0;
std::vector<double> values(num, value);
4) 通过存储元素类型相同的其它 vector 容器,也可以创建新的 vector 容器,例如:
std::vector<char>value1(5, 'c');
std::vector<char>value2(value1);
由此,value2 容器中也具有 5 个字符 'c'。在此基础上,如果不想复制其它容器中所有的元素,可以用一对指针或者迭代器来指定初始值的范围,例如:
int array[]={1,2,3};
std::vector<int>values(array, array+2);//values 将保存{1,2}
std::vector<int>value1{1,2,3,4,5};
std::vector<int>value2(std::begin(value1),std::begin(value1)+3);//value2保存{1,2,3}
由此,value2 容器中就包含了 {1,2,3} 这 3 个元素。
vector容器包含的成员函数
相比 array 容器,vector 提供了更多了成员函数供我们使用,它们各自的功能如表 1 所示。
除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 vector 容器包含的 begin() 和 end() 成员函数不同,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。
vector 容器还有一个 std::swap(x , y) 非成员函数(其中 x 和 y 是存储相同类型元素的 vector 容器),它和 swap() 成员函数的功能完全相同,仅使用语法上有差异。
如下代码演示了表 1 中部分成员函数的用法:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
//初始化一个空vector容量
vector<char>value;
//向value容器中的尾部依次添加 S、T、L 字符
value.push_back('S');
value.push_back('T');
value.push_back('L');
//调用 size() 成员函数容器中的元素个数
printf("元素个数为:%d\n", value.size());
//使用迭代器遍历容器
for (auto i = value.begin(); i < value.end(); i++) {
cout << *i << " ";
}
cout << endl;
//向容器开头插入字符
value.insert(value.begin(), 'C');
cout << "首个元素为:" << value.at(0) << endl;
return 0;
}
输出结果为:
元素个数为:3
S T L
首个元素为:C
STL学习视频教程
清华大佬超全超详细讲解——C++STL看这份教程就够了