C++ STL
什么事STL,STL包含哪些组件
广义上讲,STL分为3类:Algorithm(算法)、Container(容器)和Iterator(迭代器),容器和算法通过迭代器可以进行无缝地连接。
详细的说,STL由6部分组成:容器(Container)、算法(Algorithm)、 迭代器(Iterator)、仿函数(Function object)、适配器(Adaptor)、空间配制器(Allocator)。
STL提供了六大组件,彼此之间可以组合套用,这六大组件分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
STL六大组件的交互关系:
1. 容器通过空间配置器取得数据存储空间
2. 算法通过迭代器存储容器中的内容
3. 仿函数可以协助算法完成不同的策略的变化
4. 适配器可以修饰仿函数。
容器
各种数据结构,如vector、list、deque、set、map等,用来存放数据,从实现角度来看,STL容器是一种class template。
算法
各种常用的算法,如sort、find、copy、for_each。从实现的角度来看,STL算法是一种function template.
迭代器
扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器是⼀种将operator* , operator-> , operator++, operator-- 等指针相关操作予以重载的class template.。
所有STL容器都附带有自己专属的迭代器,只有容器的设计者才知道如何遍历自己的元素。
原生指针(native pointer)也是一种迭代器。
仿函数
行为类似函数,可作为算法的某种策略。从实现角度来看,仿函数是⼀种重载了operator()的class 或者class template
适配器
⼀种用来修饰容器或者仿函数或迭代器接口的东西。
STL提供的queue 和 stack,虽然看似容器,但其实只能算是⼀种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。
空间配置器
负责空间的配置与管理。从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template.
一般的分配器的std:alloctor都含有两个函数allocate与deallocte,这两个函数分别调用operator new()与delete(),这两个函数的底层又分别是malloc()and free();但是每次malloc会带来格外开销(因为每次malloc⼀个元素都要带有附加信息)
容器之间的实现关系以及分类:
STL的优点
STL 具有高可重用性,高性能,高移植性,跨平台的优点。
1、高可重用性:
STL中几乎所有的代码都采用了模板类和模版函数的方式实现,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。
2、高性能:
如 map 可以高效地从十万条记录里面查找出指定的记录,因为 map 是采用红黑树的变体实现的。
3、高移植性:
如在项目A上用 STL 编写的模块,可以直接移植到项目B上。
STL 的⼀个重要特性是将数据和操作分离
数据由容器类别加以管理,操作则由可定制的算法定义。迭代器在两者之间充当**“粘合剂”**,以使算法可以和容器交互运作。
常见的STL容器
- 序列容器
vector(向量):std::vector
是一个动态数组实现,提供高效的随机访问和在尾部进行插入/删除操作。
list(链表):std::list
是一个双向链表实现,支持在任意位置进行插入/删除操作,但不支持随机访问。
deque(双端队列):std::deque
是一个双端队列实现,允许在两端进行高效插入/删除操作。
array(数组):std::array
是一个固定大小的数组实现,提供对数组元素的高效随机访问。
forward_list(前向链表):std::forward_list
是一个单向链表实现,只能从头到尾进行遍历,不支持双向访问。 - 关联容器
set(集合):std::set
是一个有序的集合,不允许重复元素,支持快速查找、插入和删除。
multiset(多重集合):std::multiset
是一个有序的多重集合,允许重复元素。
map(映射):std::map
是⼀个有序的键值对集合,不允许重复的键,支持快速查找、插入和删除。
multimap(多重映射):std::multimap
是一个有序的多重映射,允许重复的键。
unordered_set(无序集合):std::unordered_set
是一个无序的集合,不允许重复元素,⽀持快速查找、插入和删除。
unordered_multiset(无序多重集合):std::unordered_multiset
是一个无序的多重集合,允许重复元素。
unordered_map(无序映射):std::unordered_map
是一个无序的键值对集合,不允许重复的键,支持快速查找、插入和删除。
unordered_multimap(无序多重映射):std::unordered_multimap
是一个无序的多重映射,允许重复的键。 - 容器适配器
stack(栈):std::stack
是一个基于底层容器的栈实现,默认使用std::deque
。
queue(队列):std::queue
是一个基于底层容器的队列实现,默认使用std::deque
。
priority_queue(优先队列):std::priority_queue
是一个基于底层容器的优先队列实现,默认使用std::vector
。
pair容器
保存两个数据成员,用来生成特定类型的模板。
使用: pair<T1, T2>p;
内部定义
namespace std {
template <typename T1, typename T2>
struct pair {
T1 first;
T2 second;
};
}
pair在底层被定义为⼀个struct,其所有成员默认是public,两个成员分别是first和second
其中map的元素是pair,pair<const key_type,mapped_type>
可以用来遍历关联容器
map<string,int>p;
auto map1 = p.cbegin();
while(map1 != p.cend())
{
cout<<map1->first<<map1->second<<endl;
++map1;
}
对map进行插入,元素类型是pair:
p.insert({
word, 1});
p.insert(pair<string, int>(word, 1));
insert对不包含重复关键字的容器,插入成功返回pair<迭代器,bool>迭代器指向给定关键字元素,bool指出插入是否成功。
vector<pair<char, int>>result(val.begin(), val.end());
sort(result.begin(), result.end(),[](auto &a, auto &b){
return a.second > b.second;
});
vector容器实现与扩充
底层实现
Vector在堆中分配了一段连续的内存空间来存放元素
1、三个迭代器
(1)first:指向的是vector中对象的起始字节位置
(2)last:指向当前最后⼀个元素的末尾字节
(3)end:指向整个vector容器所占用内存空间的末尾字节
(1)last - first:表示 vector 容器中目前已被使用的内存空间
(2)end - last:表示 vector 容器目前空闲的内存空间
(3)end - first:表示 vector 容器的容器
扩容过程
如果集合已满,在新增数据的时候,就要分配⼀块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素
所以对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了
size() 和 capacity()
(1)堆中分配内存,元素连续存放,内存空间只会增长不会减少
vector有两个函数,⼀个是capacity(),在不分配新内存下最多可以保存的元素个数,另⼀个size(),返回当前已经存储数据的个数
(2)对于vector来说,capacity是永远大于等于size的
capacity和size相等时,vector就会扩容,capacity变大(翻倍)
这里涉及到了vector扩容方式的选择,新增的容量选择多少才适宜呢?
1、固定扩容
机制:
每次扩容的时候在原 capacity 的基础上加上固定的容量,比如初始capacity为100,扩容⼀次为 capacity + 20,再扩容仍然为capacity + 20;
缺点:
考虑⼀种极端的情况,vector每次添加的元素数量刚好等于每次扩容固定增加的容量 + 1,就会造成⼀种情况,每添加⼀次元素就需要扩容⼀次,而扩容的时间花费十分高昂。所以固定扩容可能会面临多次扩容的情况,时间复杂度较高;
优点:
固定扩容方式空间利用率比较⾼。
2、加倍扩容
机制:
每次扩容的时候原capacity翻倍,比如初始capcity=100, 扩容⼀次变为 200, 再扩容变为 400;
优点:
⼀次扩容capacity翻倍的方式使得正常情况下添加元素需要扩容的次数大大减少(预留空间较多),时间复杂度较低;
缺点:
因为每次扩容空间翻倍,而很多空间没有利用上,空间利用率不如固定扩容。
在实际应永中,⼀般采用空间换时间的策略。
3、resize()和reserve() **
resize():改变当前容器内含有元素的数量**(size()),而不是容器的容量
1. 当resize(len)中len>v.capacity(),则数组中的size和capacity均设置为len;
2. 当resize(len)中len<=v.capacity(),则数组中的size设置为len,而capacity不变;
reserve():改变当前容器的最大容量(capacity)
1. 如果reserve(len)的值 > 当前的capacity(),那么会重新分配⼀块能存len个对象的空间,然后把之前的对象通过copy construtor复制过来,销毁之前的内存;
2. 当reserve(len)中len<=当前的capacity(),则数组中的capacity不变,size不变,即不对容器做任何改变。
vector源码
templeta<class T,class Alloc=alloc>
class vector {
public:
typedef T value_type;
typedef value_type *pointer;
typedef value_type &reference;
typedef value_type *iterator;
typedef size_t size_type;
typedef ptrdiff_t difference_type
//嵌套类型定义,也可以是关联类型定义
protected:
typedef simple_alloc <value_type, Alloc> data_alloctor
//空间配置器(分配器)
iterator start;
iterator finish;
iterator end_of_storage;
//这3个就是vector⾥的数据,所以⼀个vector就是包含3个指针12byte,下面有图介绍
void insert_aux(iterator position, const T &x);
//这个就是vector的自动扩充函数,在下⾯章节我会拿出来分析
void deallocate() {
if (start)
data_allocator::deallocate(start, end_of_storage);
}
//析构函数的部分实现函数
void fill_initialize(size_type n, const T &value) {
start = allocate_and_fill(n, value);
finish = start + n;
end_of_storage = finish;
}
//构造函数的具体实现
public:
iterator begin() {
return start; };
iterator end() {
return finish; };
size_type size() const {
return size_type(end() - begin()); };
size_type capacity() const {
return size_type(end_of_storage - begin()); }
bool empty() const {
return begin() == end(); }
reference operator[](size_type n) {
return *(begin() + n); };
//重载[]说明vector⽀持随机访问
vector() : start(0), end(0), end_of_storage(0) {
};
vector(size_type n, const T &value)(fill_initialize(n, value););
vector(long n, const T &value)(fill_initialize(n, value););
vector(int n, const T &value)(fill_initialize(n, value););
explicit vector(size_type n) {
fill_initialize(n, T()); };
//重载构造函数
~vector() {
destory(start, finish);//全局函数,析构对象
deallocate();//成员函数,释放空间
}
//接下来就是⼀些功能函数
reference front() {
return *begin(); };
reference back() {
return *(end() - 1); };
void push_back(const T &x) {
if (finsih != end_of_storage) {
construct(finish, x);
++finish;
}
else insert_aux