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个对象的空间,然后把之前的对象复制过来,再销毁之前的内存。

posted @   花与不易  阅读(581)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
历史上的今天:
2020-03-06 阶段性回顾总结与计划(☆_☆)
点击右上角即可分享
微信分享提示