前言

这本书虽然年代久远但依然很经典。可以详细地了解STL的底层实现机制,同时也可以对常用数据结构,C++内存管理拥有更深的理解。特别对于找工作的C++软件开发工程师帮助很大。

但个人觉得读这本书的过程中可以详略得当,有些只需要大概了解,有些则需要细嚼慢咽。这篇文章记录我在读这本书的过程中印象比较深刻的知识点。

思维导图

图片格式的看起来不直观,请访问在线思维导图:  https://www.processon.com/view/link/5d415690e4b058ef96bba000

第一章、STL概论 1、STL提供六大组件,彼此可以组合套用

(1)容器

(2)算法

(3)迭代器

(4)仿函数:重载了operator()的class或class template

(5)适配器:修饰容器或仿函数或迭代器的东西。比如queue和stack,虽然看上去是容器,但实际上却是deque的一层包装。

(6)配置器:负责空间配置与管理。

第二章、空间配置器 1、STL定义在<memory>中与空间配置相关的有三个部分:

(1)<stl_construct.h>

(2)<stl_alloc.h>

(3)<stl_uninitialized.h>

2、构造和析构的基本工具:contruct()和destroy()

用来构造和析构STL对象,会根据对象类型而选取最优的方式。

3、空间的配置与释放

(1)考虑到小型区块所可能造成的内存破碎问题,SGI设计了双层配置器,第一级配置器直接使用malloc和free,第二季配置器则视情况不同采取不同的策略:

(2)第一级配置器直接调用C函数执行实际的内存操作,并实现类似C++ new handler的机制。

(3)当区块超过128bytes时,视之为足够大,便调用第一级配置器。当配置器小于128bytes时,视之为过小,便采用内存池的方式。每次配置一大块内存,并维护对应之自由链表,下次若再有相同大小的内存需求,就直接从free-list中拔出。如果客端释还小额区块,就由配置器回收到free-list中。为了方便管理,SGI第二季配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客端要求30bytes,就自动调整为32bytes),并维护16个free-list,大小分别是8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes。

(4)内存池

如果水量充足,就直接调出20个区块返回给free-list,如果不够20则返回不足实际个数。如果一个都拿不出,则调用malloc配置40个区块的空间,其中20个给free-list,剩下的20个留在内存池。如果malloc分配失败,则调用第一级配置器。因为第一级配置器有new-handler机制,获取能够整理出多余的空间。、

(5)内存基本处理工具

uninitialized_copy,uninitialized_fill,uninitialized_fill_n,能够将内存配置与对象构造行为分离开来。并且会对POD(即标量型别或传统C struct)对象采用最有效率的初值填写手法,而对non-POD型别采取最保险安全的做法。

第三章、迭代器概念与traits编程技法 1、迭代器定义

iterator模式定义如下:提供一种方法,使之能够依序巡访某个聚合物所含的各个元素,而无需暴露该聚合物的内部表述方式。

2、Traits编程技法

迭代器通过内嵌型别声明的方式来推到实际参数的类型。

最常用的迭代器相应型别有五种:

(1)value_type,就是对象的型别

(2)difference type,表示两个迭代器之间的距离,因此它可以用来表示一个容器的最大容量。

(3)reference type,函数如果需要传回左值,是以by reference的方式进行的。

(4)pointer type,传回一个左值,令他代表所指之物。

(5)iterator_category

这个就比较重要了,分为五种类型,input iterator,output iterator,fowared iterator,bidirectional iterator,random access iterator,其中含义不言而喻。

3、std::iterator的保证

STL提供了一个Iterators class,如果每个新设计的迭代器都继承自它,就可保证符合STL所需之规范。

第四章、序列式容器

这章之前读过,可参考:https://blog.csdn.net/luchengtao11/article/details/76849570

1.vector

动态增长机制:GCC以2的指数速度增长,vs以1.5倍的速度增长。

而在不断pop的过程中,无论原先分配的空间有多大,都不会动态减少。

迭代器特性:可以以普通指针做迭代器。一旦引起空间重新配置,指向vector的所有迭代器就都失效了。

2.List

List基于双向链表实现

List的插入、删除或者拼合操作不会造成原有迭代器的失效。

List不能用STL 中的sort函数进行排序,而是要用自身的sort函数。List仅支持随机访问迭代器,而List是双向迭代器。

3.deque

是一个双向队列

将数据分为多个存储区域,存储区域的指针存储在map存储区中,当map区域不够大时可以动态增长。所以deque可以在常数时间在首尾进行存取操作,也支持随机访问。

但是代价就是复杂的迭代器,从而导致在许多方面表现的都比vector差。

 

deque的迭代器具有以下元素:

T *cur; //此迭代器所指之缓冲区的当前元素T *first; //此迭代器所指缓冲区的头T *last; //此迭代器所指之缓冲区的尾(个人觉得last所指区域为非法区域,即未分配区域,强行访问会造成崩溃)map_pointer node; //指向管控中心

 deque的迭代器的++操作,远比vector的复杂:

void set_node(map_pointer new_node){ node = new_node; first = *new_node;//新缓冲区的第一个元素的指针 last = first + difference_type(buffer_size); //新缓冲区的末尾}self& opertator++(){ ++cur; //切换到下一个元素 if (cur == last) //如果已经到达了当前缓冲区的尾端 { set_node(node+1); //切换到下一个缓冲区 cur = first; //也就是指向新缓冲区的第一个元素 } return *this;}

 

4.stack

statck可以基于多种容器实现,默认是基于deque

statck只能在尾部对元素进行存取,不允许遍历行为,不提供迭代器。

考虑过STL中为什么默认的stack是基于deque而不是vector,个人认为其实到底选哪个要看具体的应用场景。deque在各种情况下表现的都一般,但不差。比如如果疯狂push(),deque只需要增加缓冲区即可,而vector则需要请求一块更大的内存区域,把所有元素搬进新区域,再把原有区域释放。显然,在这种情况下,deque的性能是更优的。

5.queue

可以基于多种容器实现,默认是deque

值允许在尾部添加元素,在首部删除元素,不允许遍历行为,不提供迭代器。

6.heap

如果用vector来存储二叉树。如果v[1]是根节点,则v[i]的父节点为v[i/2],v[i]的子节点为v[2*i]和v[2*i+1]。真是巧了

插入:先添加到尾部,然后不断与父节点比较,不断上升。

删除:先将根节点与最后一个节点交换位置,然后将最后一个节点(即原来的根节点)弹出,然后再讲根节点(即原来的最后一个节点)不断下降。

初始化:初始化有两种方式,第一种是插入法,第二种是调整法,可参考:https://blog.csdn.net/chen_lin111/article/details/50477642

 

第五章、关联式容器

这章可谓是干货满满呀。

1、二叉搜索树

任何节点的键值一定大于其左子树中的每一个节点的键值,并小于其右子树中的每一个节点的键值。其增删改查都很简单。

2、AVL-tree

在二叉搜索树的基础上,要求任何节点的左右子树高度相差最多1。

当插入新节点时可能会改变平衡状态,此时需要重新调整。只需要调整“插入点指根节点”路径上,平衡状态被破坏之各节点中最深的哪一个,假设此点为X,分为四种情况:

插入点位于X的左子节点的左子树–左左插入点位于X的左子节点的右子树–左右插入点位于X的右子节点的左子树–右左插入点位于X的右子节点的右子树–右右

对于1,4情况,可以采用单旋转操作调整解决,对于2,3情况,可以采用双旋转操作调整解决。

3、红黑树

红黑树在二叉搜索树的基础上需要满足以下规则:

每个节点不是黑色就是红色根节点为黑色如果节点为红色,其子节点必须为黑色任一节点至NULL的任何路径,所含之黑节点数必须相同。

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

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

 

4.set 

基于红黑树实现。

不能通过迭代器改变元素值。(如果改变的话会破坏红黑树的结构)。

插入和删除操作都会导致之前的迭代器失效。

对于关联容器,使用自身的find()函数会比使用STL的find()更加有效。

5.map

不能通过迭代器改变key值,但是可以改变value值,删除和插入操作不会使之前的迭代器失效。
Insert操作返回的是pair,first是被插入元素的迭代器,second是bool,表示是否插入成功

Mulimap、mulitset与普通的map和set差不多,只不过是允许有重复值。

6.hash_table

线性探测:f(value)=value%length+i;

二次探测:f(value)=value%length+i^2

开链:相同key值的value以链表的方式保存在同一个桶中。当元素个数大于桶的个数时就重建,然后再插入重建后的相应位置。

hash_map和hash_set都是基于hash_table.

vs说hash_map和hash_set都过时了,要用unordered_map,unorder_set,性能最佳。