通读《STL源码剖析》之后的一点读书笔记

[QQ群:
189191838,对算法和C++感兴趣可以进来]

直接逼入正题。 Standard Template Library简称STL。STL可分为容器(containers)迭代器(iterators)、空间配置器(allocator)、配接器(adaptors)、算法(algorithms)、仿函数(functors)六个部分。

迭代器和泛型编程的思想在这里几乎用到了极致。模板或者泛型编程其实就是算法实现时不指定具体类型,而由调用的时候指定类型,进行特化。在STL中,迭代器保证了STL中算法的连续性和紧密型,使得各算法不需要考虑具体的数据结构。

 仿函数实质就是在结构体中重载operator
()、<、>等操作符。仿函数实际上更多的是一种概念和思想性的东西,而非一种物质上的实际提升。

容器和算法是STL中最为核心的部件,一切工作的铺垫(例如迭代器、空间配置器等)都是根据他们而展开的。下面概要的讲一下容器和算法的主要内容。

 
 容器:

   1、vector

vector其实是一种顺序性的容器,当然这里的顺序不是说排好序,而是说操作的时候按照一定的次序关系。另外,vector和数组array是非常相似的,最大的不同是array是静态的,而vector是动态,这就是为什么大部分时候使用vector的原因了,因为我们往往不知道数组开多大比较合适,开大了浪费,开小了保存不下来,选择vector是一个比较优的选择。其实vector本质上就是一个array,只不过它是一个变化的array,那么一个固定大小的array怎么变成vector的呢?我们看一下它的数据结构或许会更加明白一些。


 1 template<class T,class Alloc=alloc>
2 class vector{
3 public:
4 typedef T value_type;
5 typedef value_type* point;
6 typedef value_type* iterator;
7 typedef value_type& reference;
8 typedef size_t size_type;
9 ...
10 protected:
11 iterator start;//目前使用空间的头部
12 iterator finish;//目前使用空间的尾
13 iterator end_of_storage;//能够使用空间的尾
14 ...
15 };

我们注意到vector中的start,finish,end_of_storage。

好了,有了这三个变量,一切就有了答案。比如我们初始化vector的大小为10,那么end_of_storage=start+10了。我们可以通过维护finish这个变量来使得它看起来是动态的,比如我们插入5个值,那么通过计算finish与start的差值就能得到size()为5了。我们还可以继续插入,只不过此时需要finish向后移动一位就OK。但是,随之而来的问题又来了,当finish等于end_of_storage之后怎么办呢?显然,如果要继续插入的话,不能进行操作啊。

莫慌,请别怀疑程序猿是智商最高的那一类,我们新建一个array,这次申请的空间比上一个大那不就OK了吗?同时把旧的array复制到newarray中去,同时delete掉old
array。这样就保证了还可以继续添加。但是有时候申请资源会不成功哦,所以插入操作并不总是OK的啦。程序猿的世界,bug总是无处不在。这个时候需要重回old
array,同时delete 掉新申请的array.

我们已经解决掉了怎么使一个固定大小的array变成一个动态vector的问题,就是建一个更大的array。那么更大到底是多大呢?STL中把这个更大定义为两倍的当前SIZE.这个可能是根据经验得到的数据,两倍会是一个比较优的解。

vector中提供了insert,erase,pushback还有重载操作符等方法。其实insert就是在后面添加一个值,vector容量不够了先扩容再insert.

erase也就是擦掉某个值的操作其实就是删除某个值,然后后面的值前移,vector中erase效率极其低下,如果需要频繁的erase操作,建议还是换一个数据结构会比较好。

还有例如begin(),end(),size().这些其实都是通过维护start,finish,end_of_storage得到的。

 
2、list

在STL中list表示的是一种环状的双向链表(带头),相比vector需要维护start finish
end_of_storage,list只需要维护一个node指针结点(头结点)就可以了。

双向环状链表的初始过程如下:

1     node=getNode();//新建一个结点
2 node->next=node;//后一结点是node
3 node->pre=node;//前一结点是node

其他插入、删除、取值等操作只需要了解链表的操作都是非常easy了。

3、deque(双端队列,两边都可进可出)

deque在这里其实也是一个array,只不过是分段而已,所谓分段在这里的意思就是。deque可能连续第一组有连续十个空间存放值,第二组也有十个空间存放值,但是这两组空间不是连续的,也就是说第一组空间的尾+1,不等于第二组空间的头。这两组空间的组织和管理是通过链表(在STL中称为map,(这个map翻译成地图更合适,而非数据结构中的map))的方式形成的。

4、stack(先进后出的数据结构),queue(先进先出的数据结构)

他们的底层实现都是list,只是根据List做一些量身定制而已。就比如都是一件衣服,不同人穿的时候,稍微在外表上做点修饰,本质不改变。

上面说的都是顺序性的容器,当然顺序性容器中海油优先队列,slist等,它们其实都是大同小异,不具体阐述了。

 
5、set、map、multiset、multimap


set和map的最大不同就是set是单值,而map是键值对,
当然set也可以理解成键值对相等的特殊键值对。这样讲的话,我们大概就知道了,它们的底层实现就是一个东西。那么multiset和set有什么区别呢?主要值能不能重复的问题,如果可以保存多个相同值的话就使用multiset否则就使用set。因为插入的时候,它们分别调用的是insert和unique_insert;同理,map和multimap也是这个意思。

map有排序的功能,并且能够快速的插入和查找,甚至删除。它是怎么做到的呢?

这就要看的底层实现了。它们的底层都是基于红黑树,请看博主这篇文章:map,set的底层实现:红黑树[多图,手机慎入]

博文已经讲得非常清楚了,这里不再累述。

 
6、hashtable、hashset、hashmap、hash_multiset,hash_multimap

其实这里面主要是讲了一个东西,就是hashtable,同样hashset和hashmap的问题是是否是键值对的问题。而mullti..是是否是多值的问题。

hash的最重要的功能就是插入和查找相当迅速,我们使用hash更多的是看中他查找的时间复杂度几乎是O(1).那么hash是怎么实现如此高效的查找呢?

我们需要看他的数据结构,其实hash是一个array,通过某个合理的方式把值映射到array某个array[i]中。这就是hash的最核心思想了。说的简单,我们这里有几个问题需要解决:

  a、如何才是高效的映射?
 

   
b、如果两个不同的元素映射到同一个array[i]中又怎么办?

   
 c、数据量太小,array太大,会造成浪费;如果数据量太大,array太小,查找效率会非常低下,
怎么解决?

我们一个一个来看。首先会到a.这里映射关系需要用到某个合适哈希函数,实验说明质数是一个非常好的选择,比如(value)mod(某个质数)。

b,提到的其实就是冲突处理的问题,冲突一般有以下方式,线性探测:冲突之后把元素放到下一个位置,下一个也有元素继续后面。二次探测:即一个哈希函数冲突了之后,用下一个哈希函数;开链法:就是在该array[i]下用一个链表表示。冲突了就放到链表中。STL中选择的是开链法。

对于问题c,STL中的解决方法是,选择某个质数K作为N,当K小于hash中的元素个数之后,用更大的质数代替N,同时对之前的每一个元素重构hash.

 其他算法

其他例如find,accumulate...等算法,其实就是维护迭代器,一般都是fistIterator,lastIterator,operatorValue.这种架构。

从始至终,迭代器和泛型在几乎每一个STL容器或算法中都有出现、这才是精髓!

  

空间配置器

   
 
这里在STL中说的空间配置器更多的是指内存的申请和释放。在STL中,一般拥有两级空间配置器,其中第一级配置器更像我们传统意义上的申请;而第二级配置器是一个池,一般称为内存池。

这里对空间配置器进行一个简短的综述,空间配置器管理存储资源的申请和释放的策略是当申请的资源较大时,这里说的较大时大于126BYTES时,调用第一级配置器,采用malloc和free的方式申请和释放;当小于126BYTES时,调用第二级配置器,第一级配置器实际上是由一个链表进行维护,但freeList中找到了合适大小的内存,直接给申请者使用,如果实在不够的话,就只能用部分,甚至再用堆申请一部分,即用malloc申请。释放的时候加入到freeList中。

采用二级配置器的一个利好就是,能够较大程度上减少碎片化,提高内存内部的使用率,减少资源浪费。

参考文献:STL源码剖析

版权所有,欢迎转载,但是转载请注明出处:潇一

通读《STL源码剖析》之后的一点读书笔记,布布扣,bubuko.com

时间: 2024-12-19 09:00:59

通读《STL源码剖析》之后的一点读书笔记的相关文章

《STL源码剖析》---stl_pair.h阅读笔记

pair是STL中的模板类型,它可以存储两个元素,它也被称作"对组".在map中已经用到了它,pair其实就是一个struct结构,存有两个public的元素,重载了几个运算符,没有什么成员函数,源代码很简单. G++ 2.91.57,cygnus\cygwin-b20\include\g++\stl_pair.h 完整列表 /* * * Copyright (c) 1994 * Hewlett-Packard Company * * Permission to use, copy,

《STL源码剖析》---stl_tree.h阅读笔记

STL中,关联式容器的内部结构是一颗平衡二叉树,以便获得良好的搜索效率.红黑树是平衡二叉树的一种,它不像AVL树那样要求绝对平衡,降低了对旋转的要求,但是其性能并没有下降很多,它的搜索.插入.删除都能以O(nlogn)时间完成.平衡可以在一次或者两次旋转解决,是"性价比"很高的平衡二叉树. RB-tree(red black tree)红黑树是平衡二叉树.它满足一下规则 (1)每个节点不是红色就是黑色. (2)根节点是黑色. (3)如果节点为红色,则其子节点比为黑色. (4)任何一个节

《STL源码剖析》---stl_iterator.h阅读笔记

STL设计的中心思想是将容器(container)和算法(algorithm)分开,迭代器是容器(container)和算法(algorithm)之间的桥梁. 迭代器可以如下定义:提供一种方法,能够依序寻访某个容器内的所有元素,而又无需暴露该容器的内部表达方式. 在阅读代码之前,要先了解一个新概念:Traits编程技法 template <class T> struct MyIter { typedef T value_type //内嵌型别声明 T *ptr; MyIter(T *p = 0

《STL源码剖析》---stl_hashtable.h阅读笔记

在前面介绍的RB-tree红黑树中,可以看出红黑树的插入.查找.删除的平均时间复杂度为O(nlogn).但这是基于一个假设:输入数据具有随机性.而哈希表/散列表hash table在插入.删除.查找上具有"平均常数时间复杂度"O(1):且不依赖输入数据的随机性. hash table的实现有线性探测.二次探测.二次散列等实现,SGI的STL是采用开链法(separate chaining)来实现的.大概原理就是在hash table的每一项都是个指针(指向一个链表),叫做bucket.

《STL源码剖析》---stl_alloc.h阅读笔记

这一节是讲空间的配置与释放,但不涉及对象的构造和析构,只是讲解对象构造前空前的申请以及对象析构后空间怎么释放. SGI版本的STL对空间的的申请和释放做了如下考虑: 1.向堆申请空间 2.考虑了多线程.但是这节目的只是讲解空间配置与释放,因此忽略了多线程,集中学习空间的申请和释放. 3.内存不足时的应变措施 4.考虑到了内存碎片的问题.多次申请释放小块内存可能会造成内存碎片. 在C++中,内存的申请和释放是通过operator new函数和operator delete函数,这两个函数相当于C语

《STL源码剖析》---stl_multiset.h阅读笔记

STL中的set不允许键值重复,因此就有了multiset.multiset和set操作一样,功能一样,但是multiset允许键值重复,在使用红黑树的插入操作是,用的是insert_equal,而set用的是insert_unique,其他代码一样. G++ 2.91.57,cygnus\cygwin-b20\include\g++\stl_multiset.h 完整列表 /* * * Copyright (c) 1994 * Hewlett-Packard Company * * Permi

《STL源码剖析》---stl_deque.h阅读笔记(1)

双端队列deque是容器的一种.它比vector更强大,vector只可以在尾端插入元素,deque不只是可以再尾端插入,也可以在队列头插入.下面借助<STL源代码剖析>的图片讲解. 1.deque的内存结构 vector是开辟一段连续的内存,deque可以在前端插入元素,如果像vector开辟一段连续的内存,向前面插入元素不易维护.例如如果只是开辟一段连续内存,那么前端到开辟内存的起始位置要空多少个位置?当这段内存满了时,拷贝到更大内存时,前端空留多少位置?显然难以维护.deque在STL中

《STL源码剖析》---stl_deque.h阅读笔记(2)

看完,<STL源码剖析>---stl_deque.h阅读笔记(1)后,再看代码: G++ 2.91.57,cygnus\cygwin-b20\include\g++\stl_deque.h 完整列表 /* * * Copyright (c) 1994 * Hewlett-Packard Company * * Permission to use, copy, modify, distribute and sell this software * and its documentation fo

《STL源码剖析》---stl_list.h阅读笔记

STL中链表list是一个常用的容器.list在内存中是不连续的双向链表,且是环形的.要了解链表细节是如何操作的话,阅读STL关于链表的代码是最好的方法. G++ 2.91.57,cygnus\cygwin-b20\include\g++\stl_list.h 完整列表 /* * * Copyright (c) 1994 * Hewlett-Packard Company * * Permission to use, copy, modify, distribute and sell this

《STL源码剖析》---stl_multimap.h阅读笔记

multimap和map的关系和multiset和set关系一样,multimap允许有重复的键值,它在使用底层数据结构红黑树用,插入操作用的是insert_equal,而不是insert_unique. G++ 2.91.57,cygnus\cygwin-b20\include\g++\stl_multimap.h 完整列表 /* * * Copyright (c) 1994 * Hewlett-Packard Company * * Permission to use, copy, modi