STL标准模板库概述
STL(Standard TemplateLibrary),即标准模板库,STL提供了一组表示容器、迭代器、函数对象和算法的模板。这些“容器”有list、vector、set、map等,STL也是算法和其他一些组件的集合。STL的目的是标准化组件,这样就不用重新开发,可以使用现成的组件。
STL六大组件
-
容器(Containers):各种数据结构,如
Vector
,Deque
,List
,Set
,Map
,用来存放数据,STL容器是一种Class Template,就体积而言,这一部分很像冰山在海面的比率。 -
算法(Algorithms):各种常用算法,如
Sort
,Search
,Copy
,Erase
,从实现的角度来看,STL算法是一种Function Templates。 -
迭代器(Iterators):扮演容器与算法之间的胶合剂,是所谓的“泛型指针”,共有五种类型,以及其它衍生变化,从实现的角度来看,迭代器是一种将:
Operators*
,Operator->
,Operator++
,Operator–
等相关操作予以重载的Class Template。所有STL容器都附带有自己专属的迭代器,只有容器设计者才知道如何遍历自己的元素,原生指针(Native pointer)也是一种迭代器。 -
仿函数(Functors): 即函数对象,行为类似函数,可作为算法的某种策略(Policy),从实现的角度来看,仿函数是一种重载了Operator()的Class 或 Class Template。一般函数指针可视为狭义的仿函数。
-
配接器(适配器)(Adapters):一种用来修饰容器(Containers)或仿函数(Functors)或迭代器(Iterators)接口的东西,例如:STL提供的
Queue
和Stack
,虽然看似容器,其实只能算是一种容器配接器,因为它们的底部完全借助Deque
,所有操作有底层的Deque
供应。改变Functor接口者,称为Function Adapter;改变Container接口者,称为Container Adapter;改变Iterator接口者,称为Iterator Adapter。配接器的实现技术很难一言蔽之,必须逐一分析。 -
分配器(Allocators):即空间配置器,负责空间配置与管理,从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的Class Template。
STL
六大组件交互关系:
Container
通过Allocator
取得数据储存空间Algorithm
通过Iterator
存取Container
内容Functor
可以协助Algorithm
完成不同的策略变化Adapter
可以修饰或套接Functor
.
容器
通用操作
- 类型
实例 | 描述 |
---|---|
iterator | 迭代器 |
const_iterator | 只读迭代器 |
迭代器是一个广义的指针,可以是指针,也可以是对其进行类似指针操作的对象。模
板使算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型。例如,使用迭代器输出vector容器中的元素,代码如下:
for(vector<int>::iterator it=a.begin(); it != a,g=end(); it++) {
cout << *it << endl;
}
- 构造函数
实例 | 描述 |
---|---|
A a1 | 默认构造函数,创建空容器 |
A a2(a1) | 使用c1拷贝一份数据构造a2 |
A a2 = a1; | a2初始化为a1的拷贝,a1和a2类型必须相同(容器和内部元素类型都相同);array 则两者数量大小须相等 |
A a3(b, c) | 构造a3,将迭代器[b,c) 范围内的元素拷贝到a3 |
A a4 {a, b, c, d, e} | 列表初始化 |
A a4 = {a, b, c, d, e} | a4初始化为列表中元素的拷贝,列表中元素必须与A容器类型相同, |
只有顺序容器(除array
)的构造函数支持整数入参:
实例 | 描述 |
---|---|
A a(n) | a 中包含n 个元素,且进行了值初始化,其构造函数是explicit 的 |
A a(n, value) | a 中包含n 个值为value 的元素 |
- 大小
实例 | 描述 |
---|---|
a.size() | 获取a中元素数量,无符号整型 |
a.empty() | 判断容器是否为空,返回bool值 |
a.max_size() | a中可保存的最大元素数量 |
- 逻辑运算
实例 | 描述 |
---|---|
== | 所有容器都支持相等运算符 |
- - | 所有容器都支持不等运算符 |
<, <=, >, >= | 关系运算符(无序关联容器不支持) |
关系运算符左右两边的运算符对象必须是相同类型的容器,且必须保存相同类型的元素。
两个容器比较运算是容器内元素两两逐对比较:
a. 如果两容器大小相等,且元素两两对应相等,说明两容器相等。
b. 如果大小不等,且小容器元素与大容器元素对应相等,子序列关系,说明小容器小于大容器。
c. 如果大小不等,且非子序列关系,大小取决于首个不相等元素的比较结果。
例如:
vector<int> v1 = {1, 2, 5, 7, 9, 12};
vector<int> v2 = {1, 3, 9};
vector<int> v3 = {1, 3, 5, 7};
vector<int> v4 = {1, 3, 5, 7, 9, 12};
v1 < v2 // true, v1[1] < v2[1]
v1 < v3 // false, v3是v1的子序列,且数量不等,因此v3 < v1
v1 == v4 // true, 大小相等, 且元素逐对相等
v1 == v2 // false, 大小不等,v1[1] < v2[1], 所以应该是v1 < v2
- 赋值和交换
实例 | 描述 |
---|---|
a1 = a2 | 使用a2中元素替换a1中的元素,a1容器元素数量将与a2相同 |
a1 = {a, b, c,…} | 使用a,b,c,…等元素初始化a1 |
a1.swap(a2) | 交换a1和a2两容器的元素 |
swap(a1, a2) | 同a1.swap(a2) |
assign | 容器赋值,仅支持顺序容器 |
swap
:
操作交换两个相同类型容器内容
除了array
之外,交换两容器内容的操作很快,容器内容未交换,只交换容器内部数据结构。因此,交换不会移动元素,除string
外,指向容器的迭代器,指针和引用在swap
操作之后不会失效,任然指向swap
之前的元素,但是这些元素在swap
之后只不过属于不同的容器。
重点:与其他容器不同,对string
交换之后会导致上述迭代器,指针和引用失效。
重点:与其他容器不同,swap
两个array
对象是会真正交换他们的元素的。
assign
:
由于
array
要求赋值左右运算对象必须相同类型,assign
左右运算符对象大小可能不一样,因此array类型assign不支持
赋值运算符要求左右运算符对象具有相同类型,将右侧对象中的元素拷贝到左侧运算符对象。
顺序容器(除array
)定义了assign
成员,实现从右侧不同类型但相容的类型赋值,或从容器的子序列赋值。
assign
使用场景一:
// 从vector<const char*>中赋值给list<string>
list<string> names;
vector<const char*> oldStyleNames;
names = oldStyleNames; // 报错,左右容器类型不匹配
names.assign(oldStylesNames.begin(), oldStyleNames.end());
assign
使用场景二:
// assign(int, new_value)
list<string> list1(1); // 含有1个空string元素
list1.assign(5, "test"); // list1含有5个元素,值为"test"
// 等价于
list1.clear();
list1.insert(list1.begin(), 5, "test");
- 增删元素
实例 | 描述 |
---|---|
a1.insert(args) | 将args元素拷贝到a1 |
a1.erase(args) | 删除容器中args元素 |
a1.clear() | 删除a1中所有元素,返回void |
- 迭代器
实例 | 描述 |
---|---|
a1.begin() | 返回a1容器指向首元素迭代器 |
a1.end() | 返回a1容器指向尾元素之后的迭代器 |
a1.cbegin() | 返回指向首元素的const_iterator类型迭代器 |
a1.cend() | 返回指向尾元素之后的const_iterator类型迭代器 |
- 反向容器
不支持forward_list
reverse_iterator: 按逆序寻址元素的迭代器。
const_reverse_iterator:不能修改元素的逆序迭代器
实例 | 描述 |
---|---|
a1.rbegin() | 逆序访问,返回a1的尾元素迭代器 |
a1.rend() | 逆序访问,返回a1的尾元素之后的迭代器 |
a1.crbegin() | 逆序访问,返回a1的尾元素的const_reverse_iterator迭代器 |
a1.crend() | 逆序访问,返回a1的首元素之后的const_reverse_iterator迭代器 |
迭代器
标准容器类型上的所有迭代器均可通过解引用运算符来实现访问容器元素,类似指针,可通过递增、递减运算符来在不同元素之间移动。
例外:forward_list不支持–运算符操作, ++,- -运算符只能用于vector, string, deque, array,不能用于其他容器。
迭代器范围(iterator range
)由一对迭代器表示,通常是begin
和end
。
[begin, end)
取值范围左闭右开区间,其中end
迭代器指向容器尾元素之后的位置。
- 可以说明
if (a1.begin() == a1.end()) {
// 意为着begin和end之间的范围为空
}
if (a1.begin() != a1.end()) {
// [begin,end)之间至少有一个元素
}
while(a1.begin() != a1.end()) {
*begin = new_value; // 给begin指向元素赋新值
begin++; // 递增移动到下一个元素
}
十大容器概述
顺序容器(vector、deque、list)
vector
-
vector
是表示可变大小数组的序列容器,底层是内存可进行二倍扩容的数组。 -
支持数组表示法和随机访问:类似数组(内部线性存储结构),支持下标访问。
-
动态分配:当新元素插入时候,底层数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组,代价高。
-
分配空间策略:
vector
会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。 -
vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
-
与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起lists和forward_lists统一的迭代器和引用更好。
deque(double-ended queue)
-
支持下标访问,头尾快速插入元素和删除元素(vector只在尾端快速进行此类操作)。
-
存取元素时,deque的内部结构会多一个间接过程,所以元素的存取和迭代器的动作会稍稍慢一些。
-
迭代器需要在不同区块间跳转,所以必须是特殊的智能型指针,非一般指针。
-
在对内存区块有所限制的系统中(例如PC系统),deque可以内含更多元素,因为它使用不止一块内存。因此deque的max_size()可能更大。
-
deque不支持对容量和内存重分配时机的控制。特别要注意的是,除了头尾两端,在任何地方插入或删除元素,都将导致指向deque元素的任何指针、引用、迭代器失效。不过,deque的内存重分配优于vector,因为其内部结构显示,deque不必在内存重分配时复制所有元素。
-
deque的内存区块不再被使用时,会被释放。deque的内存大小是可缩减的。
list
- list就是一个带头结点的双向非循环链表,list将元素按顺序储存在链表中。
- 与vector相比, 访问随机元素不如vector快,随机的插入元素比vector快。
- 对每个元素分配空间,所以不存在空间不够,重新分配的情况。
- list可以快速地在所有地方添加删除元素,但是只能快速地访问最开始与最后的元素。
关联容器(set、multiset、map、multimap)
关联容器和顺序容器的根本不同在于:**关联容器中的元素是按关键字来保存和访问的,**而顺序容器中的元素则是按它们在容器中的位置来顺序保存和访问的。
multiset
- multiset 是排序好的集合(元素已经进行了排序),并且允许有相同的元素。
- 不能直接修改 multiset 容器中元素的值。因为元素被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。因此,如果要修改 multiset 容器中某个元素的值,正确的做法是先删除该元素,再插入新元素。
set
- set 和 multiset 类似,差别在于set中不能有重复的元素 。multiset 的成员函数 set 中也都有。
- 同样不能直接修改 set 容器中元素的值。
- 由于不能有重复元素,所以set中插入单个元素的
insert
成员函数与multiset中的有所不同
multimap
-
multimap 的每个元素都分为关键字和值两部分,容器中的元素是按关键字排序的,并且允许有多个元素的关键字相同。
-
不能直接修改 multimap 容器中的关键字。因为 multimap 中的元素是按照关键字排序的,当关键字被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。
-
multimap 中的元素都是pair 模板类的对象。元素的 first 成员变量也叫“关键字”,second 成员变量也叫“值”。multimap 容器中的元素是按关键字从小到大排序的。
map
-
map 和 multimap 十分类似,区别在于 map 容器中元素的关键字不能重复。multimap 有的成员函数,map 都有。
-
此外,map 还有成员函数 operator[]:
T & operator[] (Key k);
该成员函数返回 first 值为 k 的元素的 second 部分的引用。如果容器中没有元素的 first 值等于 k,则自动添加一个 first 值为 k 的元素。如果该元素的 second 成员变量是一个对象,则用无参构造函数对其初始化。
容器适配器(stack、queue、priority_queue)
STL中的容器适配器有 stack、queue、priority_queue 三种。它们都是在顺序容器的基础上实现的,屏蔽了顺序容器的一部分功能,突出或增加了另外一些功能。
容器适配器都有以下三个成员函数:
-
push:添加一个元素。
-
top:返回顶部(对 stack 而言)或队头(对 queue、priority_queue 而言)的元素的引用。
-
pop:删除一个元素。
参考资料
- 《STL源码剖析》 1.2 STL六大组件 功能与运用