读书笔记

来源:互联网 发布:java 管理系统框架 编辑:程序博客网 时间:2024/05/16 12:03

前面有几篇文章的图片是用截图复制,粘贴过来的,结果发布直接没了。。好坑。


第一章 STL概论与版本简介

STL 六大组件

1:容器(vector,map,etc)STL容器是一种class template

2:算法:sort,search,srase。STL算法是一种function template

3:迭代器:扮演容器和算法的胶合剂。是一种class template

4:仿函数(functors):行为类似函数,是一种重载了operator的class或class template

5:配接器(adapters):一种用来修饰容器,或者仿函数或迭代器接口的东西

6:配置器(allocators):负责空间配置与管理,是一个实现了动态空间配置、空间管理、空间释放的class template


第二章 空间配置器

trivial destructor相关(点击打开链接)

如果用户不定义析构函数,而是用系统自带的,则说明,析构函数基本没有什么用(但默认会被调用)我们称之为trivial destructor。反之,如果特定定义了析构函数,则说明需要在释放空间之前做一些事情,则这个析构函数称为non-trivial destructor。如果某个类中只有基本类型的话是没有必要调用析构函数的,delelte p的时候基本不会产生析构代码,

在C++的类中如果只有基本的数据类型,也就不需要写显式的析构函数,即用默认析构函数就够用了,但是如果类中有个指向其他类的指针,并且在构造时候分配了新的空间,则在析构函数中必须显式释放这块空间,否则会产生内存泄露,

construct:在已经分配空间的地方上构造

destroy:析构函数

在STL中空间配置时候destory()函数会判断要释放的迭代器的指向的对象有没有 trivial destructor(STL中有一个 has_trivial_destructor函数,很容易实现检测)放,如果有trivial destructor则什么都不做,如果没有即需要执行一些操作,则执行真正的destory函数。

C++ placement new :在已有的内存块上面创建对象。用于需要反复创建并删除的对象上,可以降低分配释放内存的性能消耗

而alloc的原理是:


C++ new handler机制是,你可以要求系统在内存配置需求无法被满足时,调用一个你所指定的函数。



现在看了第二章,我算是基本明白他的原理了,还有原因再看看,然后分这两部分讲一下,书上也有

空间配置器的目的,就是为了给各种类,变量,制造一个内存空间(不初始化),然后这个SGI设计这个功能的宗旨是这4个:1)向system heap要求空间 2)考虑多线程3)考虑内存不足时的应变措施4)考虑过多小型区块可能造成的内存碎片问题。

然后对于空间配置,无非两个问题,分配和回收。

先说分配空间,SGI是双层级配置器(这个是针对(4))考虑的,小额区块带来的不仅仅是内存碎片,配置时的额外负担也是问题。这个的图为:


意思就是,假如需要的空间大于128byte,则用第一级,而第一级就是直接用malloc和free(因为大)。

假如小鱼128bytes,则会用第二级,第二级的设计是个比较有趣的东西,采用的是内存池管理。

首选看一个图:

第二级,主要是维护这么个东西,#0表示,我维护的都是大小为8的内存块,#1为16,依次类推,直到128byte。

任何的请求,不是8的倍数,会上调到8的倍数,这样就可以用free_list去分配。其中以#0为例,他维护的结构如图所示:


#0就是图里free-list,然后各个内存块之间是用指针指向,每一个长条的数据结构为:


通过union公用,节省空间。另外,free-list是怎么来的呢,是由内存池给他的,这个怎么理解呢,按照书本的比喻,内存池好比一个水缸,会有一定量的水,free-list会向水缸请求水,例如请求8*20,64*20,这样的方式,假如水池水足够,就给free-style,假如不够,要是我能给你几个8或者64,我就给尽量多,比如给8*8,给64*1;假如1个都给不了,水缸和水龙头(堆heap,满足1)说,放水放水(一般是需求的2倍,例如,需要8*20,则分配8*40给水缸),然后再给free-list。

数据结构有了,我们就开始描述一个具体场景,并分析做法(有些是自己理解的做法,或许不正确)

现在我们申请了一个vector<double> xx(10),假如需要8*10个空间(vector应该不是这么算空间的,这里是假设),然后先去看free-list的#0,有没有足够的8,没有,就通过chunk_alloc()函数去向内存池要,内存池此时要是有,就直接给了,否则,会向上面描述的一样处理。

然后这个这个vector用完了,需要释放了,调用deallocate函数,这个函数就是回收空间

具体做法是,如图:


关键是q->free_list_link = * my_free_list和下一句;相当于把这个空间重新放入#0的空间里,通过指针在连起来


特例:heap都不够,怎么办,第二级找不到,会直接改到第一级,然后再不行,会有bad_alloc异常。

STL有5个全局函数,作用于未初始化空间上。其中两个就是construct和destroy;另外三个是uninitialized_copy(),uninitialized_fill()uninitialized_fill_n()。

uninitialized_copy():

假如实现一个容器,1:配置内存区块,足以包含范围内的所有元素 2:使用uninitialized_copy(),在该内存区块上构造元素;

uninitialized_fill():

uninitialized_fill_n();

POD:标量型别或传统的C struct型别。必然有析构,构造,copy,operator = 等。对POD采取最有效的初值填写算法。(一个聚合体,他的非static成员都不是 pointer to class member,pointer to class member function、非POD结构、非POD联合,以及这些类型的数组、引用、const)

对POD型别,采用最有效率的复制手法,而对non-POD型别采取最保险安全的做法。


第三章:迭代器的设计思维-STL关键所在

23种设计模式。。

iterator:提供一种方法,使之能够依序巡防某个聚合物(容器)所含的各个元素,而又无需暴露该聚合物的内部表达方式。

STL的中心思想是:将数据容器和算法分开,彼此独立设计,最后再以一种胶着剂将他们撮合在一起。

迭代器是一种smart pointer,指针的主要目的就是内容提领和成员访问。因此迭代器最重要的是事先operator *和

operator ->

假如采用一般的指针(或者智能指针)去作为迭代器的内核,不得不暴露容器的信息(其实按我的理解,例如你指针++,假如是顺序的,得知道容器每个元素占的空间,假如是链式的,就得用到next)

stl说C++只支持sizeof,不支持typeof,那是不是,如今的auto,decltype就是为了这个设计的。书本的stl用 function template 解决这个问题,本质就是函数里有I iter,但是我要*I,不能用*I,会报错,只能再传一个变量过来,传*iter,然后就可以 T tmp了


他大量的设计都是为了萃取,因为C++语法的一些约束,导致无法识别类型,识别了类,也有可能是原生数据,因此需要进行全部类型的考虑,原生数据不能用上面的办法,因为(原生指针并不是一种类类型,它是无法定义内嵌类型的),然后得用模板偏特化(这个我其实并没见过之前。)

但是C++11不是有了 decltype了么,这个反射机制,应该可以简化工作,我找了找,果然发现一篇写这个的:

点击打开链接,当然,其STL的思想还是需要领会的。


traits萃取器:最长需要萃取得到的:iterator_category;value_type;difference_type;poingter;reference;

迭代器的类型:只读;唯写;forward iterator(读写),bidirectional iterator:可双向移动。Randdom Access iterator:涵盖所有算数能力


设计算法时,如果可能,尽可能对图中的确定种类的迭代器,提供一个做法,对更强化的,提供另一种,这样才能效率最大化

书中以advanced()函数为例,介绍了不同类型的迭代器,进行advance的具体做法。然后希望在编译的时候就确定,用哪个做法,这个就牵扯到重载。

任何一个迭代器,其类型永远应该落在“该迭代器所隶属之各种类型中”,最强化的那个。就是上图中最下面

std::iterator的保证

为了符合规范,任何迭代器都应该提供五个内嵌相应相应型别,以利于traits萃取。

traits编程技法,利用“内嵌型别”的编程技巧与编译器的template参数推到功能:目的是体用,型别认证(如今有了其他办法)

而SGI里面有__type_traits。双底线前缀词意指这是SGI,STL内部的东西。


第三章 序列式容器

先是vector;看看他几个重要函数的源码:



其实理解的难度都不打,第一个,push_back,假如还有空间,则执行全局的construct函数,finish增加,假如不够了,则代用自身的一个函数,应该是要扩容,复制在添加的。

erase函数,其实也很简单,先看是不是最后一个,假如是最后一个,不用执行copy全局函数,直接finsh--,然后destroy就行了,假如不是,则要copy,再执行。

vector是连续空间,普通指针也可以满足全部条件,因此他的迭代器类型是random access iterators.vector还是比较简单的。


list:精准控制元素空间,list的本质是一个双向链表。list的迭代器必须有能力指向list的节点,list提供的Bidirectional Iterators,他的插入和结合操作,都不会使用来的迭代器失效。


注:STL有“前闭后开”区间的要求,意思就是begin()是第一个数,end()是最后一个数的后面,即无效数;

为啥前闭后开,例如假如是双闭,那写循环的时候,得是<=这种,这对迭代器要求有点高(例如list),所以循环采用!(参考知乎的回答)


deque:双端队列,deque竟然也是一种双向开口的连续线性空间。deque提供random access itraator。迭代器比vector复杂。除非必要,尽可能选择vector。

deque系由一段一段的定量连续空间构成,deque最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,一图表明


deque的迭代器是这样的结构:


因为缓冲区的buffer长度是一样的,因此,可以实现随机存储,可以算。

push_back就是看,这个空间还要嘛,有的话,直接构造到这个区域,没有就得新多一个缓冲区

他还能支持双端操作,因此每个操作都要考虑是否在缓冲区内,在怎么处理,不在怎么处理。

deque的最初状态,保有一个缓冲区。


stack:deque很容易实现,默认,stack就是用deque实现的。stack系以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌”,称之为adapter(配接器).stack不提供迭代器。list也有双向开口的容器,也能实现stack功能。


queue:先进先出,deque可以做到,queue也没有迭代器,且list也可以做到。


heap:并不归属STL容器组件,但是他是个幕后英雄,扮演proiority queue(这个结构貌似是当时腾讯实习生面试的题目,让我实现这个,底层数据结构用啥,我用的是链表,然后push的时候,数据就找准自己的位置,后来又衍生了题目,好像是如何提供find函数,等等。。原来都是已经有的东西。。。。让我看看实际怎么处理的吧)的助手。binary max heap(原来就是最大堆啊。。我日)具有特性:以任意词语推入容器,然后取出,一定是优先级最高的。


书本说的没错,我那种办法效率不行,一开始得遍历找到位置。有一种做法是用二查搜索树(当时实习的时候还不知道这个玩意),这样,插入是取值有logN的表现,但是有点小题大做。

binary heap就是一种complete binary tree,然后用array存,父结点必定是i/2,其左子节点必定是2i,右是2i+1(其实在堆排里面就是如此,尼玛,heap就是堆).array可以轻易实现complete binary tree,这叫隐式表述法。

heap没有迭代器


第五章 关联式容器

标准的STL关联式容器分为set和map两大类。还有multiset,multimap,其底层都是红黑树

只有,unordered_map,和unordered_set的底层都是哈希表(hashtable)C++11,这本stl没说。


先是有,二叉搜索树,左子树的所有节点都比根节点小。右子树xxx

但是要是数据进来的很顺序,就会退化链表,如图:


AVL tree,要求任何节点的左右字数高度不超过1。

插入可能破坏性质,因此要采用单旋转的方式,总共4中情况:


对于外侧插入情况,直接采取,单旋转即可。对于左上图的情况。把14提上去,14的右边就作为18的左子树:如图:


但是对于,右上图的情况,单旋转就不行了,得作双旋转,因为左边的左子树有较多节点,因此,先转,把左子树的右子树的节点变少,然后再和上面的单旋转一样:


RB-tree(红黑树)

1每个节点不是红色就是黑色

2根节点是黑色

3如果节点为红,子节点必须为黑

4任何一个节点至NULL(树尾端)的任何路径,所含黑节点数必须相同。

根据规则4,新增的节点必须是红,根据3,其父节点必须是黑,不满足就得旋转。

真正的情况分类,太复杂了。。。


set:所有元素根据键值自动被排列。而且不允许有相同的值

set的迭代器是不允许修改set元素值,会严重破坏set组织。iterator 是一种 constant iterators。删除和新增,不会使原来的迭代器失效(是因为,树以节点的形式存着,树的样子虽然在变,但是也是指针指向内容在变)

对于关联容器,应该使用容器提供的find,因为STL算法里的find,是按照顺序的。


map:也会根据元素的键值自动被排序。map不能修改键值,但是能修改value.


俩者不能重复,实现的机制是,RB-tree里的insert_unique;首先先查找,是否有对应的值,有的话就不插入了。真正插入的函数是__insert();


本来想谈谈map和set的区别,但是他的底层几乎一样,没啥区别可言了。。


hashtable:这种结构在插入、删除、搜寻等操作上具有“常数平局时间”

hash function:散列函数,最终要的东西,就是映射函数,问题是,会有碰撞问题。解决方法

线性探测:hash function算出来的已经被用了,则向下寻找,找完了去上面找,直到找到位置。


二次探测:线性是探测H+1,H+2,二次探测是探测H+1,H+4,H+9(好像假如表格大小为质数,且负载系数在0.5以下,探测次数不超过2,真tm的都行。。)


开链:就是拉链法。


第六章 算法(略看)

STL算法都作用在由迭代器所标示上。所谓“质变算法”,就是运算过程中会更改区间内的元素内容。例如,拷贝,互换,填写,删除,排列,分割,等等。质变算法,通常提供两个版本,一个是in-place;一个是copy版。


例如power算法,其实就不是一个个乘上去,而是类似于2分的感觉去处理,减少次数。

牛逼的copy算法(就是要效率好):

看源码,是因为很多算法,这里都有吧,比如经典的partition.


第七章 仿函数(略看)

一个class,里面定义了operator ()


第八章 配接器(略看)

将一个class的接口转换为另一个class的接口,使原本因接口不兼容而不能合作的classes,可以一起运作。。

应用于容器,就是 queue和stack。


应用于迭代器L就比如,insert iterators,reverse iterators,iostream iterators。



原创粉丝点击