STL小结——仿写TinySTL项目有感
六大组件:容器(类模板)、算法(函数模板)、迭代器(类模板,设计模式)、配置器(类模板)、配接器(容器适配器、函数配接器)、仿函数(类或类模板)。整个项目采用大多的是泛型编程的思想(模板机制)。使得多种数据类型在同一个算法或结构上都可以操作,在编译器就确定聚类数据类型。
(一)配置器
一般new都是要先申请空间,再在空间构造对象。delete是要先析构对象,再释放空间。
项目为了更好的分工,把对象的构造和析构(construct和destroy)、空间的申请和释放(malloc和自由链表)分成两大块。
一级配置器(class __malloc_alloc_template):调用malloc和realloc,如果不成功,改调用oom_malloc和oom_realloc(抛出异常或exit(1))。
二级配置器(class __default_alloc_template):16个自由链表
内存配置过程:小于128bytes就找16个自由链表里面合适的那个链表(上调到8的倍数)还有没有,有就直接返回。如果没有,就向内存池申请所需块的大小挂到自由链表(refill/chunk_alloc(),可能获得20个新节点,也可能小于20个)。如果还是没有,使用malloc从堆中申请,申请的大小是需求的两倍(一倍放在自由链表,一倍放在内存池)。如果malloc还是失败,那再从剩下的自由链表找找。实在不行就bad_alloc异常。
(二)迭代器。
精髓在于trait思想。trait可以被称为萃取机,通过它来获取迭代器的一些信息,在STL里面发挥巨大作用。
template<class Iterator> struct iterator_traits { typedef typename Iterator::iterator_category iterator_category; typedef typename Iterator::value_type value_type; typedef typename Iterator::difference_type difference_type; typedef typename Iterator::pointer pointer; typedef typename Iterator::reference reference; }
//下面针对原生指针。通过类模板的特化来支持原生指针。(模板偏特化)
template<class _Tp>
struct iterator_traits<_Tp*>
{
typedef ptrdiff_t difference_type;
typedef typename _Tp::value_type value_type;
typedef typename _Tp::pointer pointer;
typedef typename _Tp::reference reference;
typedef typename _Tp::iterator_category iterator_category;
}
可以通过iterator_traits<Iterator>::itereator_category获取迭代器的信息,如果要直接使用typename后面的那个简单一点的Iterator::iterator_type来获取迭代器的信息,前提是迭代器必须是一个类。但是实际上并不是所有的迭代器都是一个类,原生指针也是一种迭代器,但是原生指针不是类,所以没法定义内嵌型别typeof。
迭代器类别有五种。Input_Iterator是只读的,支持==/!=/++/*/->等操作。Output_Iterator是只写的,支持++/*等操作。
Forward_Iterator是单向一步的,可读写。Bidirection_Iterator是双向一步的。RandomAccess_Iterator是随机多步的。
(三)容器和适配器
STL容器有:vector、list双向链表、deque、map(unordered/multi)、set(unordered/multi)
适配器是将一个类的接口转换成客户希望的另一个接口。比如把香港插头转成内地插头。
STL容器适配器有:stack(可以使用deque做底层),queue(可以使用deque做底层),heap(平时虽然都是静态数组,但可以使用vector动态的来实现),priority_queue(使用vector[容器]和heap[push和pop算法]做底层),slist是单向链表。
序列式容器是通过元素在容器的位置,顺序存储和访问元素。关联式容器时通过键key来存储和读取元素。
分别从每个容器的迭代器、构造函数、属性获取来展开介绍。
(1)关于vector。
线性存储结构。需要三个迭代器分别指向数据的头(start)、数据的尾(finish)、数组的尾(end_of_storage)。
属性end()返回的是finish。
push_back函数,插入新元素的时候,会先检查是否有备用空间,如果有就直接在备用空间构造元素,并调整迭代器finish。如果没有备用空间,就要调用insert_aux函数扩充空间(重新配置-移动数据-释放原空间)。pop_back函数就是直接移动finish迭代器。
erase函数可以删除[first, last)范围中的所有元素,也可以删除指定迭代器位置的元素。删除范围内的元素,第一步要将finish迭代器后面的元素拷贝,然后返回拷贝完成的尾部迭代器,最后再删除。删除指定位置的就是将指定位置后面所有元素向前移动,最后析构最后一个元素。
insert函数分成三种情况。如果备用空间足够而且插入点后面的现有元素多于新增元素;如果备用空间足够而且插入点后面的现有元素少于新增元素;如果备用空间不够。
由于元素空间重新配置会导致之前的迭代器失效,比如插入元素或者删除元素。
优点:动态扩容,便于随机访问,节省空间。
缺点:插入删除的时间复杂度高,只能在末端pop和push,重分配麻烦。
(2)关于list。
双向链表,链式存储结构。有pre和next两个指针。可以头插和尾插。插入删除就是双向链表的。
优点:插入删除方便,两端可push和pop。
缺点:不支持随机访问,相比vector占用空间多。
(3)关于deque。
线性存储+链式存储(分段连续空间)。逻辑上也是线性存储。和vector的区别在于,deque可以在常数时间内插入删除(头尾都可以),但是deque没有容量概念,因为它是动态以分段连续空间组合而成,随时可以增加一块新的空间并拼接起来。
中控器:采用哈希(指针,缓冲区)作为deque的存储空间主体。其中缓冲区就是另外一段比较大的连续线性空间。每个缓冲区都设计了三个迭代器(cur表示当前所指的位置,first表示当前数组中头的位置,last表示当前数组中尾的位置)
优点:随机访问方便,插入删除方便,可在两端push和pop。
缺点:因为涉及比较复杂,采用分段连续空间,所以占用内存多。
(4)关于map和set。 只有前面带unordered的才是哈希表实现的,否则一定是红黑树实现的(因为要排序)
红黑树特性:每个节点都是红色或者黑色。根节点一定是黑色。红节点的左右一定是黑色。每个叶子节点一定是黑色。(三个一定黑)。从某节点到其所有叶子节点都包含相同数量的黑色节点。
应用于Linux系统的网络epoll调用、虚拟内存管理、STL的set和map。
键是不能改变的,因为红黑树的排序的,如果改变键的话整棵树都乱了。
如果前缀带multi就是允许键重复,那就采用insert_equal,而不是insert_unique。
(三)算法
resize是直接改变容器内元素的数量,赋初始值。
reverse只是改变容器的最大容量,不会放值进去。如果reverse的容量大于之前的,那么会重新分配一块能存len个对象的空间,然后把之前的对象复制过来,再销毁之前的内存。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
2020-03-06 阶段性回顾总结与计划(☆_☆)