C++STL

STL,全称standard template library,标准模板库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,STL 基本上达到了各种存储方法和相关算法的高度优化。

三个最为普遍的STL版本:

  1. HP STL

    其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。

  2. P.J.Plauger STL

    由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。

  3. SGI STL

    由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。

我们一般使用的STL也是SGI STL

源码:https://blog.csdn.net/jmw1407/category_10122107.html

STL的六大部件和联系

  • 容器(containers)
  • 算法(algorithm)
  • 迭代器 (iterator)
  • 适配器 (adapter)
  • 分配器 (allocator)
  • 仿函数 (functor)

容器通过分配器获取数据存储空间,算法通过迭代器存取容器数据,仿函数协助算法完成不同策略,适配器修饰仿函数

容器:各种数据结构,来存放数据,从实现角度来看是一种类模板;

算法:各种算法的封装,从实现角度来看是一种函数模板;

迭代器:是容器与算法之间的胶合剂,即泛型指针;

仿函数:一般函数指针可视为狭义的仿函数;

空间配置器:负责空间的配置与管理;

适配器(配接器):用来修饰容器或仿函数的迭代器接口。通俗的讲,就是像改锥的接口,装上不同的适配器,才能装上不同的改锥头。

容器(containers)

当程序中存在对时耗要求很高的部分时,数据结构的选择就显得尤为重要,有时甚至直接影响程序执行的成败。

为了重复利用已有的实现好的数据结构,引入了STL内部封装好的数据结构-容器,需要包含各自头文件

STL的容器主要利用了C++的模板特性来实现。需要注意:

  • 容器缓存了节点,节点类要确保支持拷贝(否则出现浅拷贝问题,导致崩溃)
  • 容器中的一般节点类,需要提供拷贝构造函数,并重载等号操作符(用来赋值)
  • 容器在插入元素时,会自动进行元素的拷贝。

容器详解:https://www.jianshu.com/p/497843e403b4

常用成员函数

  • begin:返回指向容器中第一个元素的迭代器
  • end:返回指向最后一个元素所在位置后一个位置的迭代器
  • size:返回已经保存的元素数目
  • capacity:返回当前容器容量,capacity永远不可能小于size,只要小于,就增加一半并向下取整,值得注意的是,至少增加1
  • max_size:返回容器最大支持容量,这个数是一个很大的常整数,可以理解为无穷大。这个数目与平台和实现相关
  • empty:判断容器是否为空
  • reserve:修改的是capacity的大小,无法调整为比size小的数(调整比原来size小视为无效操作)
  • resize:修改的是size的大小,未赋值的默认为0,
    • 可以调整为比capacity大的数,此时会进行重新配置
    • 缩小和扩大容器时,可能导致迭代器,指针,引用失效

容器有些可以使用下标访问,使用方式如数组,但可能发生访问越界,使用at则会检查

容器的大小size一旦超过capacity的大小,vector会重新配置内部的存储器,导致和vector元素相关的所有reference、pointers、iterator都会失效

内存的重新配置耗时较多,遵循:不到万不得已,不要重新分配内存原则。可通过Reserve()保留适当容量或者创建容器时提供足够空间

std::vector<int> v;				//1.这是空的vector
std::vector<int> v(5);			//2.这是一个capacity为5,size为5,全默认为0的容器
std::vector<int> v{0,0,0,0,0}	//3.等价于2
vector<int> v(5,1);				//4.这是一个capacity为5,size为5,全为1的容器

序列化容器

共同的特点是不会对存储的元素进行排序,元素排列的顺序取决于存储它们的顺序。

array、vector、deque、list 和 forward_list

容器 特性 底层实现
array 固定大小数组 数组
vector 可变大小数组1.支持快速随机访问 2.顺序存储 3.迭代器失效(插入,删除,扩容时) 数组
List 支持快速增删 双向链表
forward_list 只支持单向顺序访问 单向链表
queue 插入只可以在尾部进行,删除、检索和修改只允许从头部进行。按照先进先出的原则。 默认deque
stack 操作的项是最近插入序列的项,按照后进先出的原则 默认deque
deque 支持首尾(中间不能)快速增删,也支持随机访问,底层数据结构为一个中央控制器和多个缓冲区 双向队列

通常,使用vector是最好的选择

vector
底层结构

vector占用一块连续分配的内存,一种可以存储任意类型的动态数组。使用 3 个迭代器(指针):

Myfirst 指向的是 vector 容器对象的起始字节位置;Mylast 指向当前最后一个元素的末尾字节;_myend 指向整个 vector 容器所占用内存空间的末尾字节。

扩容的本质

另外需要指明的是,当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:

  1. 完全弃用现有的内存空间,重新申请更大的内存空间;
  2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  3. 最后将旧的内存空间释放。

遍历

  • []方式,如果越界或出现其他错误,不会抛出异常,可能会崩溃,可能数据随机出现
  • at方式,如果越界或出现其他错误,会抛出异常,需要捕获异常并处理

常见操作

  • 插入:

    • insert函数,结合迭代器位置插入指定的元素。如果迭代器位置越界,会抛出异常。
    • emplace() 是C++11 标准新增加的成员函数,用于在 vector 容器指定位置之前插入一个新的元素。
    • 不同点
      • insert可以一次插入多个,但emplace只能一次插入一个
      • emplace() 在插入元素时,是在容器的指定位置直接构造元素,效率更高
      • 通过 insert() 函数向 vector 容器中插入 testDemo 类对象,需要调用类的构造函数和移动构造函数;先单独生成,再将其复制(或移动)到容器中
      • 当拷贝构造函数和移动构造函数同时存在时,insert() 会优先调用移动构造函数。
  • 删除

    • erase(iterator)函数,用迭代器指定某个位置或区域删除;删除后会返回当前迭代器的下一个位置。

    • clear():删除 vector 容器中所有的元素,使其变成空的 vector 容器。

      • 该函数会改变 vector 的size=0,但其容量capacity仍然保存不变
      • 当存储元素是object时,会调用析构函数;当存储元素是指针时,并不会调用这些指针所指对象析构函数(所以指针注意自己delete)
      • 如果你想同时做到清空vector的元素和释放vector的容量, 你可以使用swap
      • 使vector离开其自身的作用域,从而强制释放vector所占的内存空间,总而言之,释放vector内存最简单的方法是vector<int>.swap(v1)
  • remove() 的实现原理是,在遍历容器中的元素时,一旦遇到目标元素,就做上标记,然后继续遍历,直到找到一个非目标元素,即用此元素将最先做标记的位置覆盖掉,同时将此非目标元素所在的位置也做上标记,等待找到新的非目标元素将其覆盖。

    • vector<int>demo{ 1,3,3,4,3,5 };
      //交换要删除元素和最后一个元素的位置
      auto iter = std::remove(demo.begin(), demo.end(), 3);
      
      
    - 在对容器执行完 remove() 函数之后,由于该函数**并没有改变容器原来的大小和容量**,因此无法使用之前的方法遍历容器,而是需要向程序中那样,借助 remove() 返回的迭代器完成正确的遍历。
    
      - 因此,如果将上面程序中 demo 容器的元素全部输出,得到的结果为 `1 4 5 4 3 5`。
    
迭代器失效

迭代器失效是对容器insert ,push_back,erase操作不当引起的。

erase分为两种情况

第一种情况:

使用erase删除某一个结点之后,vector迭代器虽然还是指向当前位置,而且也引起了元素前挪,但是由于删除结点的迭代器就已经失效,指向删除点后面的元素的迭代器也全部失效,

所以不能对当前迭代器进行任何操作;需要对迭代器重新赋值或者接收erase它的返回值;(即当前迭代器失效,需重新赋值)

第二种情况:erase返回擦除的当前点的下一个位置,越界了。

push_back造成扩容后迭代器完全失效,需从头开始

list

底层双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据

  • distance函数可以求出当前的迭代器指针it距离头部的位置,也就是容器的指针
    • 用法: distance(v1.begin(), it)
  • 删除
    • erase是通过位置或者区间来删除,主要结合迭代器指针来操作
    • remove是通过值来删除
deque

非连续存储结构,存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于在内存的不同区域。

提供了两级数组结构, 第一级类似vector的动态数组(map),存储指针,指向那些真正用来存储数据的各个连续空间,;另一级连续存放实际元素。扩容与vector类似

迭代器实现

  1. 迭代器在遍历 deque 容器时,必须能够确认各个连续空间在 map 数组中的位置;
  2. 迭代器在遍历某个具体的连续空间时,必须能够判断自己是否已经处于空间的边缘位置。如果是,则一旦前进或者后退,就需要跳跃到上一个或者下一个连续空间中。
template<class T,...>
struct __deque_iterator{
    ...
    T* cur;
    T* first;
    T* last;
    map_pointer node;//map_pointer 等价于 T**
}

deque相当于vector与list的折中

  • 如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector
  • 如果你需要大量的插入和删除,而不关心随机存取,则应使用list
  • 如果你需要随机存取,而且关心两端数据的插入和删除,则应使用deque

关联式容器

STL为什么选择红黑树而不是AVL-tree?

红黑树的平衡性是比AVL-tree弱的,但是搜索效率几乎相等。两者的插入和删除操作都是O(logn),但是就旋转操作而言,AVL-tree是O(n),而红黑树是O(1),任何不平衡都会在三次旋转之内解决

  • 红黑树适合查找
  • 哈希表适合增删,增删时间复杂度接近O(1)

根据红黑树的原理,可以实现O(lgn)的查找,插入和删除。

容器 特性 底层结构
set 存储的关键字就是value,有序,不重复 红黑树
multiset 存储的关键字就是value,有序,可重复 红黑树
map 存储的是有序,不重复 红黑树
multimap 存储的是有序,可重复 红黑树

map与set的insert

insert返回的是pair<iterator, bool>类型,pair的第一个属性表示当前插入的迭代器的位置,第二个属性表示插入是否成功的bool值

map

只有map有[]操作符

当key在map中存在时,[]完成的是读操作。当key不存在是,[]完成一个写操作。写入一个新的pair<key,T()>。

为啥有unordered_map,还需要map

  • log(n)不一定比常数大
  • 哈希表是以空间换时间,需要内存消耗;且hashtable创建需要时间
  • 如果考虑效率,特别当元素达到一定数量级时,用hash_map。
  • 考虑内存,或者元素数量较少时,用map。

hashtable如何避免地址冲突?

1)线性探测:先用hash function计算某个元素的插入位置,如果该位置的空间已被占用,则继续往下寻找,知道找到一个可用空间为止。

其删除采用惰性删除:只标记删除记号,实际删除操作等到表格重新整理时再进行。

2)二次探测:如果计算出的位置为H且被占用,则依次尝试H+12,H+22等(解决线性探测中主集团问题)。

3)链表:每一个表格元素中维护一个list,hash function为我们分配一个list,然后在那个list执行插入、删除等操作。

总结

vector deque list set mutliset map multimap
内存结构 单端数组 双端数组 双向链表 红黑树 红黑树 红黑树 红黑树
随机存取 对key而言是
查找速度 非常慢 对key而言快 对key而言快
插入删除 尾端 头尾两端 任何位置 - - - -

deque可以看成vector和list的折中,随机存取比vector慢,

无序关联容器

又叫哈希容器,底层结构是hash table

在已提供有 4 种关联式容器的基础上,又新增了各自的“unordered”版本(无序版本、哈希版本),提高了查找指定元素的效率。

即unordered_map、unordered_multimap、unordered_set 以及 unordered_multiset。

STL中的hashtable使用的是开链法解决hash冲突问题

集合 底层实现 是否有序 数值是否可重复 能否修改数值 查询效率 增删效率
set 红黑树 有序 O(logn) O(logn)
multiset 红黑树 有序 O(logn) O(logn)
unordered_set 哈希表 无序 O(1) O(1)
映射 底层实现 是否有序 数值是否可重复 能否修改数值 查询效率 增删效率
map 红黑树 key有序 key不可重复 key不可修改 O(logn) O(logn)
multimap 红黑树 key有序 key可重复 key不可修改 O(logn) O(logn)
unordered_map 哈希表 key有序 key不可重复 key不可修改 O(1) O(1)

算法(algorithm)

算法部分主要由头文件组成。

其中常用到的功能范围涉及到比较、交换、查找、遍历操作、复制、修改、移除、反转、排序、合并等等

STL中算法大致分为四类:
1)非可变序列算法:指不直接修改其所操作的容器内容的算法。
2)可变序列算法:指可以修改它们所操作的容器内容的算法。
3)排序算法:对序列进行排序和合并的算法、搜索算法以及有序序列上的集合操作。
4)数值算法:对容器内容进行数值计算。

对集合的查找,最好不要用通用函数find(),它对集合使用的时候,性能非常的差,最好用集合自带的find()函数,它针对了集合进行了优化,性能非常的高。

sort算法要求随机访问迭代器,包括array,deque,string,vector

迭代器 (iterator)

尽管不同容器的内部结构各异,但它们本质上都是用来存储大量数据的,所以对不同容器数据进行遍历的操作方法应该是类似的。

可以利用泛型技术,将它们设计成适用所有容器的通用算法,从而将容器和算法分离开,并且不需暴露该对象的内部表示。

因此引入中介-迭代器,泛型指针,是一种智能指针,重载了*,++,==,!=,=运算符。

标准库为每一种标准容器定义了一种迭代器类型,容器的迭代器的功能强弱,决定了该容器是否支持 STL 中的某种算法。

基础迭代器分为

迭代器名 功能 应用
输入迭代器 只读不写,单遍扫描,只能递增 istream
输出迭代器 只写不读,单遍扫描,只能递增 ostream,inserter
前向迭代器 可读写,多遍扫描,只能递增 forward_list
双向迭代器 可读写,多遍扫描,可递增递减 list,set,multiset,map,multimap
随机访问迭代器 随机读写 vector,deque,array,string
//创建vector
vector<int> v1;
//遍历-迭代器遍历
for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++) {
    cout << *it << " ";
}
cout << endl;

//遍历-迭代器逆向遍历
for (vector<int>::reverse_iterator it = v1.rbegin(); it != v1.rend(); it++) {
    cout << *it << " ";
}
cout << endl;

适配器(adapter)

一种用来修饰容器(container)或仿函数(functor)或迭代器(iterator)接口的东西。

  • 改变functor接口者,称为functor adapter,

  • bind

  • 改变container接口者,称为container adapter

    • 封装了序列容器的类模板,它在一般序列容器的基础上提供了一些不同的功能。

    • 底层可以更换不同容器

      //可以自定义底层容器
      std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈
      
    • 分类

      • stack 栈适配器,默认deque
      • queue 队列适配器,默认deque
        • push(x) -- 将一个元素放入队列的尾部。
        • pop() -- 从队列首部移除元素。
        • peek() -- 返回队列首部的元素。
        • empty() -- 返回队列是否为空。
      • priority_queue 优先权队列适配器,默认vector容器,堆存储结构
        • top 访问队头元素
        • empty 队列是否为空
        • size 返回队列内元素个数
        • push 插入元素到队尾 (并排序)
        • emplace 原地构造一个元素并插入队列
        • pop 弹出队头元素
        • swap 交换内容
      #include<queue>//头文件
      priority_queue<Type, Container, Functional>//定义
      //对于基础类型 默认是大顶堆
      priority_queue<int> a; 
      //等同于 priority_queue<int, vector<int>, less<int> > a;
      priority_queue<int, vector<int>, greater<int> > c;  //这样就是小顶堆
      
      
      //STL中的仿函数less<T>和greater<T>的使用范围仅限于基本类型,当优先队列需要保存我们自定义的数据类型时,
      //我们需要重载小于号(operator <)或者重写仿函数
      //注意:小顶堆>,大顶堆<
      //方法1:重写仿函数
      struct mycomparison 
      {
      	bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) 	  	
          {
              return lhs.second > rhs.second;//小顶堆
          }
      };
      //方法2:类运算符重载<,就无需更换仿函数
      struct tmp1 
      {
          int x;
          tmp1(int a) {x = a;}
          bool operator<(const tmp1& a) const
          {
              return x < a.x; //大顶堆
       	}
      };
      
      priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparison>pri_que;
      priority_queue<tmp1>pri_que2;
      
  • 改变iterator接口者,称为iterator adapter。

    • 迭代器适配器模板类的内部实现,是通过对以上 5 种基础迭代器拥有的成员方法进行整合、修改,甚至为了实现某些功能还会添加一些新的成员方法。
    • 分类
      • 反向迭代器(reverse_iterator)
      • 插入型迭代器(insert_iterator)
      • 流迭代器(istream_iterator / ostream_iterator)流缓冲区迭代器(istreambuf_iterator / ostreambuf_iterator)
      • 移动迭代器(move_iterator)

分配器(allocator)

负责空间配置与管理。是一个实现了动态空间配置、空间管理、空间释放的class template。

STL的内存配置器在我们的实际应用中几乎不用涉及,它只是在背后默默为各种容器做了大量工作,为容器分配并管理内存,实现统一内存管理

SGI-STL的空间配置器有2种,默认的空间配置器是第二级的配置器。

  1. 仅仅对c语言的malloc和free进行了简单的封装,
  2. 设计到小块内存的管理等,运用了内存池技术

alloc

SGI使用时std::alloc作为默认的配置器

  • alloc把内存配置和对象构造的操作分开,分别由alloc::allocate()::construct()负责
  • 内存释放和对象析够操作也被分开分别由alloc::deallocate()和::destroy()负责

这样可以保证高效,因为对于内存分配释放和构造析构可以根据具体类型(type traits)进行优化。

内存分配

STL内存分配分为两级

  • 当申请的内存大于128byte时,就启动第一级分配器,通过malloc直接向系统的堆空间分配
  • 当申请的内存小于128byte时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表free_list组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。

8~128byte,刚好到128个字节,这就是为什么大于128bytes要使用第一级配置器了,因为,第二级配置器最多只能分配128bytes,

原因

  • 小对象的快速分配。小对象是从内存池分配的,这个内存池是系统调用一次malloc分配一块足够大的区域给程序备用,当内存池耗尽时再向系统申请一块新的区域,从而更快分配小对象内存
  • 避免了内存碎片的生成。程序中的小对象的分配极易造成内存碎片,给操作系统的内存管理带来了很大压力,系统中碎片的增多不但会影响内存分配的速度,而且会极大地降低内存的利用率。

仿函数(functor)

又叫做函数对象,可以实现类似于函数一样的对象类型,函数最直接的调用形式就是:返回值 函数名(参数列表),仿函数实现了operator()操作符,使用类似于函数

一般函数指针、回调函数可视为狭义的仿函数。以操作数的个数划分,可分为一元和二元仿函数

为什么要使用仿函数呢?

1).仿函数比一般的函数灵活,且可以实现代码复用,又不必维护公共变量

2).仿函数有类型识别,可以作为模板参数。

3).执行速度上仿函数比函数和指针要更快的。

class less 
{ 
	bool operator()(int a,int b){ 
		return a<b?; 
	} 
}; 
int main()
{ 
	int a=1,b=2; 
	bool isLess = less (a,b); 
	cout <<isLess<< '\n'; 
	return 0; 
}

如果要自定义仿函数,并且用于STL接配器,那么一定要从binary_function或者,unary_function继承。

其实binary_function只是做一些类型声明而已,但为了方便,安全,可复用性

posted @ 2021-05-05 13:27  AMzz  阅读(150)  评论(0编辑  收藏  举报
//字体