STL

STL

参考链接:

http://c.biancheng.net/stl/

概念

STL,英文全称 standard template library,中文可译为标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能。

STL 最初由惠普实验室开发,于 1998 年被定为国际标准,正式成为 C++ 程序库的重要组成部分。值得一提的是,如今 STL 已完全被内置到支持 C++ 的编译器中,无需额外安装,这可能也是 STL 被广泛使用的原因之一。

STL 就位于各个 C++ 的头文件中,即它并非以二进制代码的形式提供,而是以源代码的形式提供。

从根本上说,STL 是一些容器、算法和其他一些组件的集合,所有容器和算法都是总结了几十年来算法和数据结构的研究成果,汇集了许多计算机专家学者经验的基础上实现的,因此可以说,STL 基本上达到了各种存储方法和相关算法的高度优化。

注意,这里提到的容器,本质上就是封装有数据结构的模板类,例如 list、vector、set、map 等

STL的头文件:algorithm,numeric,vector,deque,list,queue,stack,set,map,iterator,memory,utility

1. Algorithm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <algorithm> // 使用Algorithm必须包含的头文件
#include <cstdio>
using namespace std;

int main () {
//排序
int a[5] = {5, 1, 2, 3,7};
sort (a, a+5);
// stable_sort (a, a+5);
for (int i = 0; i < 5; i++)
cout << a[i] << " ";
cout << endl;

// 反转
reverse(a, a+5);
for (int i = 0; i < 5; i++)
cout << a[i] << " ";
cout << endl;

// 绝对值
cout << abs(-5) << endl;

// 最大值最小值
cout << max(5, 3) << " " << min(5, 3) << endl;

// 交换
int x = 1, y = 2;
swap (x, y);
cout << x << " " << y << endl;

// 赋值函数fill
int b[4] = {1, 2, 3, 4};
fill (b, b+4, -1); //前4个元素全赋值为-1
for (int i = 0; i < 4; i ++)
cout << b[i] << " ";
cout << endl;

//求全排列的下一个顺序, next_permutation若有下一个全排列返回true没有则返回false
char str[3] = {'a', 'b', 'c'};
do {
cout << str << endl;
}while (next_permutation(str, str+3));
return 0;
}

2. Container

2.1 序列容器

主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即序列容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。

vector

vector相比数组的好处是可以根据存储数据的数量自动变长,并且有很多方法可以直接调用。

vector容器在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <vector>

int main(){
// -----------------------初始化-----------------------
vector<int> a; //定义容器a,当前a长度为0,但和普通数组不同的是,此a可以根据存储数据的数量自动变长。
vector<int> b {}; //指定为空
vector<int> c(20); //指定有20个元素,且全为0
vector<int> d(20, -1); //指定有20个元素,且全为-1,这里可以使用变量进行初始化
// 使用其他数据结构初始化
int array[] = {1,2,3}
vector<int> value(array,array+3); // 使用数组初始化vector
vector<int> value1{1,2,3,4,5};
vector<int> value2(value1.begin(),value1.begin()+3); // 使用vector初始化vector
value2.assign(value1.begin(),value1.begin()+3); //使用assign方法初始化

// -----------------------手动调整大小-----------------------
a.resize(100); // 默认初始化为0
a.resize(20, -1) //重新调整 a 的大小为 20,并存储 20 个 -1 元素。

// 赋值
a[9] = 100;
// -----------------------添加元素-----------------------
for (int i = 0; i < 10 ; i++){ //向a中添加10个元素
a.emplace_back(i);
}
a.insert(a.begin(), 1002); //向指定位置的前面添加元素,2个100
a.emplace(a.begin(), 100); //emplace每次只能插入一个元素,但是emplace的效率更高

// -----------------------访问元素-----------------------
cout<<a[0]<<endl; // 访问单个元素,直接使用下标,可能越界
cout << "首个元素为:" << a.at(0) << endl; // 使用at方法访问,会进行越界判断
cout << "values 首元素为:" << values.front() << endl;
cout << "values 尾元素为:" << values.back() << endl;
for (auto i = a.begin(); i < a.end(); i++) { //使用迭代器遍历容器
cout << *i << " ";
}

// -----------------------删除-----------------------
a.pop_back(); // 删除最后一个元素,size会减小,但capacity不会变
a,erase(a.begin(), a,begin+3); // 删除指定位置的元素,并返回指向被删除元素下一个位置元素的迭代器,size会减小,但capacity不会变
auto it = remove(a.begin(), a.end(), 3);; // 删除等于指定值的元素,并返回指向被删除元素下一个位置元素的迭代器,,size和capacity都不会变
a.clear(); // 清空,size变为0,capacity不会变

// -----------------------交换元素-----------------------
// 注意,swap() 函数在头文件 <algorithm> 和 <utility> 中都有定义,使用时引入其中一个即可。
swap(*(a.begin()+1),*(a.end()-1)); //等同于 swap(a[1],a[4])

return 0;
}

注意,vector 容器在使用resize()申请更多内存的同时,容器中的所有元素可能会被复制或移动到新的内存地址,这会导致之前创建的迭代器失效。因此,为了保险起见,每当 vector 容器的容量发生变化时,我们都要对之前创建的迭代器重新初始化一遍:

注意,不要使用vector<bool>,该类型使用bit进行存储,会有很多问题

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

函数成员 函数功能 array vector deque
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。且++是向左移动
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
assign() 用新元素替换原有内容。 -
size() 返回实际元素个数。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
at() 使用经过边界检查的索引访问元素。
resize() 改变实际元素的个数。 -
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
operator[] 使用索引访问元素。
push_back() 在序列的尾部添加一个元素。 -
insert() 在指定的位置插入一个或多个元素。 -
emplace_back() 在序列尾部生成一个元素。 -
pop_back() 移出序列尾部的元素。 -
erase() 移出一个元素或一段元素。 -
clear() 移出所有的元素,容器大小变为 0。 -
swap() 交换两个容器的所有元素。
data() 返回指向容器中第一个元素的指针。 -

list

list容器以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list<T> 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。

另外还有一个forward_list<T>(正向链表容器),和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

deque

deque容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;

相比vector,deque可以更方便的在头部增删元素。

deque容器提供的成员函数:其余未展示的函数基本与vector相同

函数成员 函数功能
push_back() 在序列的尾部添加一个元素。
push_front() 在序列的头部添加一个元素。
emplace_front() 在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程。
emplace_back() 在容器尾部生成一个元素。和 push_back() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程。
pop_back() 移除容器尾部的元素。
pop_front() 移除容器头部的元素。

array

array<T,N>(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;

1
2
3
4
5
6
7
8
#include <array>
using namespace std;

int main(){
// 初始化
std::array<double, 10> v; // 名为v的有10个double类型的元素的array
std::array<double, 10> v {}; // 初始化全为0
}

除此之外,stack<T> 和 queue<T> 本质上也属于序列容器,只不过它们都是在 deque 容器的基础上改头换面而成,通常更习惯称它们为容器适配器

2.2 排序容器

包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。multimap、multiset与map、set的唯一不同在于其键可以重复。

STL中可以使用 pair 类模板来创建“键值对”形式的元素。pair类模板还提供有一个 swap() 成员函数,能够互换 2 个 pair 对象的键值对,其操作成功的前提是这 2 个 pair 对象的键和值的类型要相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <utility> // pair 类模板定义在<utility>头文件中
#include <string>
using namespace std;
int main() {
// 调用默认构造函数
pair<string, string> pair1;
pair1.first = "animal";
pair1.second = "cat";
// 直接使用 2 个元素初始化 pair 对象
pair<string, string> pair2("animal","cat");
pair<string, string> pair2 = {"animal","cat"};
// 使用pair初始化pair
pair<string, string> pair3(pair2);
// 调用移动构造函数
pair<string, string> pair4(make_pair("animal", "cat"));
pair<string, string> pair4 = make_pair("animal", "cat");
// 输出
cout << "pair1: " << pair1.first << " " << pair1.second << endl;

// 交换键值对
pair1.swap(pair2);
return 0;
}

map

定义在 <map> 头文件中,使用该容器存储的数据,其各个元素的键必须是唯一的(即不能重复),该容器会根据各元素键的大小,默认进行升序排序。

注意,使用insert函数插入元素val时,val 参数表示键值对变量,同时该方法会返回一个 pair 对象,其中 pair.first 表示一个迭代器,pair.second 为一个 bool 类型变量:

  • 如果成功插入 val,则该迭代器指向新插入的 val,bool 值为 true;
  • 如果插入 val 失败,则表明当前 map 容器中存有和 val 的键相同的键值对(用 p 表示),此时返回的迭代器指向 p,bool 值为 false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <map> //使用 map 容器,必须引入该头文件
#include<utility> // 使用pair
#include <string>
using namespace std;
int main()
{
// -------------------------初始化-------------------------
//创建一个空的 map 关联式容器,该容器中存储的键值对,其中键为 string 字符串,值也为 string 字符串类型
map<string, string> mymap;
//向 mymap 容器中添加数据
mymap["animal"] = "cat";
mymap["plant"] = "flower";
mymap["planet"] = "earth";
// 定义的同时初始化
map<string, int> myMap{ {"animal",10},{"plant",20} };
map<string, int> myMap{ make_pair("animal",10), make_pair("plant",20) };
map<string, int> newMap(myMap)
// map默认使用升序排序初始化,所以下面两行代码等价
map<string, int, less<string>> myMap{ {"C语言教程",10},{"STL教程",20} };
map<string, int >myMap{ {"C语言教程",10},{"STL教程",20} };
// map使用降序排序初始化
map<string, int, greater<string>>myMap{ {"C语言教程",10},{"STL教程",20} };

// -------------------------添加键值对-------------------------
myMap.emplace("planet","earth");
pair<string, string> a("planet","earth");
myMap.insert(a); // 因为map会自动根据键进行排序,所以插入时没有必要指定插入位置

// -------------------------遍历和索引-------------------------
//使用 map 容器的迭代器,遍历 mymap 容器,并输出其中存储的各个键值对
for (auto it = mymap.begin(); it != mymap.end(); ++it) {
//输出各个元素中的键和值
cout << it->first << " => " << it->second << '\n';
}
string a = mymap["animal"];
string a = myMap.at("animal");

// -------------------------查找-------------------------
auto iter = myMap.find("animal"); //查找键为 "animal" 的键值对

//找到第一个键的值大于或等于 "animal" 的键值对
auto iter = myMap.lower_bound("animal");
cout << "lower:" << iter->first << " " << iter->second << endl;

//找到第一个键的值大于 "animal" 的键值对
iter = myMap.upper_bound("animal");
cout <<"upper:" << iter->first << " " << iter->second << endl;

// 找到值与 "animal" 的值相等的键值对
auto myPair = myMap.equal_range("animal"); // equal_range返回一个pair,第一个元素为lower_bound的返回值,第二个元素为upper_bound的返回值
for (auto iter = myPair.first; iter != myPair.second; ++iter) {
cout << iter->first << " " << iter->second << endl;
}
return 0;
}

set

定义在 <set> 头文件中,使用该容器存储的数据,各个元素键和值完全相同,且各个元素的值不能重复(保证了各元素键的唯一性)。该容器会自动根据各个元素的键(其实也就是元素值)的大小进行升序排序。

和 map、multimap 容器不同,使用 set 容器存储的各个键值对,要求键 key 和值 value 必须相等

1
2
3
4
5
6
7
8
9
10
11
12
#include<set>
using namespace std;

set<string> mySet{"animal", "plant"}
string str = "planet";
auto retpair = mySet.insert(str);
auto retpair = mySet.emplace(str);

// 删除元素
int num = mySet.erase("animal"); // 根据值定位,返回成功删除的元素个数
auto iter = mySet.erase(myset.begin(), --myset.end()); // 根据迭代器定位,返回删除后所指向的迭代器
mySet.clear()

insert()函数返回的 pair 类型的值,其包含 2 个数据,一个迭代器和一个 bool 值:

  • 当向 set 容器添加元素成功时,该迭代器指向 set 容器新添加的元素,bool 类型的值为 true;
  • 如果添加失败,即证明原 set 容器中已存有相同的元素,此时返回的迭代器就指向容器中相同的此元素,同时 bool 类型的值为 false。

emplace()函数与insert()一样,也能像set容器中添加元素,且效率更高。

2.3 哈希容器

C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。

3. iterator迭代器

在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂,使得算法的设计可以泛化到各种数据结构,隐藏容器的内部差异。

STL标准库为每一种标准容器定义了一种迭代器类型,这意味着,不同容器的迭代器也不同,其功能强弱也有所不同。容器的迭代器的功能强弱,决定了该容器是否支持 STL 中的某种算法。

分类

常用的迭代器按功能强弱分为输入迭代器、输出迭代器、正向迭代器双向迭代器随机访问迭代器 5 种。主要使用后三种。

1) 正向迭代器

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

2) 双向迭代器

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

3) 随机访问迭代器

随机访问迭代器具有双向迭代器的全部功能。除此之外,假设 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 之间的元素个数减一)。

下表为 C++ 11 标准中不同容器所支持的迭代器类型:

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

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

定义

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

*迭代器名就可以表示迭代器指向的元素。

反向迭代器和正向迭代器的区别在于:

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

注意,以上 4 种定义迭代器的方式,并不是每个容器都适用。有一部分容器同时支持以上 4 种方式,比如 array、deque、vector;而有些容器只支持其中部分的定义方式,例如 forward_list 容器只支持定义正向迭代器,不支持定义反向迭代器

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector> //需要引入 vector 头文件
using namespace std;
int main()
{
vector<int> v{1,2,3,4,5,6,7,8,9,10}; //v被初始化成有10个元素
//第一种遍历方法:使用索引遍历,size返回元素个数
for (int i = 0; i < v.size(); ++i)
cout << v[i] <<" ";
//第二种遍历方法:创建一个正向迭代器,当然,vector也支持其他 3 种定义迭代器的方式
vector<int>::iterator it;
for (i = v.begin(); i != v.end(); ++i) // 或者i < v.end()
cout << *i << " ";
//间隔一个输出
i = v.begin();
while (i < v.end()) {
cout << *i << " ";
i += 2; // 随机访问迭代器支持 "+= 整数" 的操作
}
}

4. 函数对象

如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。

5. 适配器

可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。

6. 内存分配器

为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。