这本书在全书开头介绍了本书定位、适合的读者、阅读方式、剖析方式、编译环境和叙述风格等,总的来说就是作者苦口婆心的说了一堆概(废)况(话)。
第一章:
先是讲了STL的概念和发展历史,以及一些常用的组件的介绍,然后谈了谈源码开放的发展史和意义。说实在的第一章就列出了128个文件,当时人都快被吓傻,大部分都没见过,然后将它们分类。列了好多编译器的东西,极不友好(是我太菜了),看的我是云里雾里,甚至在怀疑自己是不是买错书了,还是说面试就问这些?还是老实回家种田得了。回到正题,接着讲了一些常见困(很)惑(骚)的语法(我TM会写出这种东西难为自己?)。然后结束了左闭右开即[)的表示方法,这样做的目的是为了有效的实现很多情况,例如[1,1)就可以表示空区间。
第二章:
主要是在围绕空间适配器讲,这是精华(作者讲的),可能平常对一些细节不知道,随便用个东西并没能体会到中间开辟了空间使用。SGI STL使用一个专属的、拥有层次配置能力的、效率优越(往好的吹)的特殊配置器——alloc,不接受任何参数(你??)还提供了一个标准配置器——simple_alloc。
new算式内含两个阶段(1)调用::operator new配置内存;(2)调用函数构建对象内容。delete算式也内含两阶段操作(1)调用函数讲对象析构;(2)调用::operator delete释放内存。内存配置操作由alloc:allocate()负责,内存释放操作由alloc::deallocate()负责,对象构造操作由::construct()负责,对象析构操作由::destroy()负责。
SGI以malloc()和free()完成内存的配置与释放。SGI设计了双层级配置器,第一级配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略:当配置区块超过128bytes时,视之为“足够大”,便调用第一级配置器;当配置器区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用复杂的memory pool整理方式,而不再求助于第一级配置器。每次配置一大块内存,并维护对应之自由链表(free-list)。下次若再有相同大小的内存需要,就直接从free-lists中拔出,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数,并维护16个free-lists。当发现free list中没有可用区块了时,就调用refill(),准备为free list重新填充空间。新的空间将取自内存池(经由chunk_alloc()完成)。缺省取20个新节点,万一内存池空间不足获得的节点数可能小于20。
chunk_alloc()函数以end_free - start_free来判断内存池的水量,如果水量充足,就直接调出20个区块返回给free list。如果水量不足以提供20个区块,但还可以供应一个以上的区块,就拔出这不足20个区块的空间出去。这时候其pass by reference的nobjs参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,对客端显然无法交代,此时便需利用malloc()从heap中配置内存,为内存池注入源头活水以应付需求。新水量的大小为需求量的两倍,再加上一个随着配置次数增加而愈来愈大的附加量。如果整个system heap空间都不够了,malloc()行动失败,chunk_alloc()就四处寻找有无“尚有未用区块,且区块够大”之free lists。找到了就挖一块交出,找不到就调用第一级配置器,第一级配置器其实也是使用malloc()来配置内存,但它有out-of-memory处理机制,或许有机会释放其它的内存拿来此处使用。如果可以,就成功,否则发出bad_alloc异常。
SGI以malloc而非::operator new来配置内存,因此SGI不能直接使用C++的set_new_handler(),必须仿真一个类似的set_malloc_handler()。uninitialized_copy()使我们能够将内存的配置与对象的构造行为分离开来。容器的全区间构造函数通常以两个步骤完成:配置内存区块,足以包含范围内的所有元素;使用uninitialized_copy(),在该内存区块上构造元素。要么"构造出所有必要元素”,要么“不构造任何东西”。uninitialized_fill_n()为指定范围内的所有元素设定相同的初值。
第三章:
auto_ptr是一个用来包装原生指针的对象,可以解决内存泄漏问题。auto_prt角括号内放的是“原生指针所指对象”的型别,而不是原生指针的型别。traits用来萃取迭代器的型别,vlaue type是指迭代器所指对象的型别,difference type用来表示两个迭代器之间的距离,因此它也可以用来表示一个容器的最大容量。
Input Iterator:只读 Output Iterator:只写 Forward Iterator:允许读写 Bidirectional Iterator:可双向移动 Random Access Iterator:涵盖所有指针算术能力
STL算法的命名规则:以算法所能接受的最低阶迭代器类型,来为其迭代器型别参数命名。distance()可接受任何类型的迭代器。
iterator class不含任何成员,纯粹只是型别定义,所有继承它不会招致任何额外负担。设计适当的相应型别是迭代器的责任,设计适当的迭代器则是容器的责任。
iterator_traits负责萃取迭代器的特性,__type_traits负责萃取型别的特性。如果class内含指针成员,并且对它进行内存动态配置,那么这个class就需要实现自己的non_trivial-xxx。
第四、五章:
序列式容器:array,vector,heap,priority_queue,list,slist,deque,stack,queue
并联式容器:RB-tree,set,map,multiset,multimap,hashtable,hash_set,hash_map,hash_multiset,hash_multimap
heap内含一个vector,priority_queue内含一个heap,stack和queue都内含一个deque,set/map/multiset/multimap都内含一个RB-tree,hash_x都内含一个hashtable。
vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。vector支持随机存取,使用的Random Access Iterators。为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端要求量更大一些,以备将来可能的扩充,vector的容量大小拥有大于等于其大小。所谓动态增加大小,并不是在原空间之后接连续新空间,而是以原大小的两倍另外配置一块较大空间,然后将原来内容拷贝过来,然后才开始在原内容之后构造元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。
list提供的是Bidirectional Iterators。
对deque进行的排序操作,为了最高效率,可将deque先完整复制到一个vector身上,将vector排序后,再复制回deque。
heap使用完全二叉树实现
缺省情况下priority_queue是利用一个max-heap完成
树:路径所经过的边数,称为路径长度(length);根节点至任一节点的路径长度,即所谓该点的深度(depth);某节点至其最深子节点的路径长度,称为该节点的高度(height);任何节点的大小是指其所有子代的节点总数。
平衡二叉树,一般而言其搜寻时间可节省25%左右
AVL-tree要求任何节点的左右子树高度相差最多1
RB-tree(红黑树)
1.每个节点不是红色就是黑色
2.根节点为黑色
3.如果节点为红,其子节点必须为黑
4.任一节点至NULL(树尾端)的任何路径,所含的黑节点数必须相同
insert_unique()表示被插入节点的键值在整棵树中必须独一无二,insert_equal()表示被插入节点的键值在整棵树中可以重复。
面对并联式容器,应该使用其所提供的find函数来搜寻元素p.find()。
map不允许两个元素拥有相同的键值,insert插入操作返回一个pair,由一个迭代器和一个bool值组成,后者表示插入成功与否。
hash_table可提供对任何有名项的存取操作和删除操作,所以hash_table也可被视为一种字典结构。
负载系数,意指元素个数除以表格大小,负载系数永远在0~1之间——除非采用开链策略。
线性探测:平均插入成本的增长幅度,远高于负载系数的成长幅度,这样的现象在hashing过程中称为主集团。
二次探测:如果hash function计算出新元素的位置为H,而该位置实际上已被使用,那么我们就依序尝试H+1²,H+2²,H+3²,...,H+i²,而不是像线性探测那样依序尝试H+1,H+2,H+3,...,H+i。如果我们假设表格大小为质数,而且永远保持负载系数在0.5以下,那么就可以确定每插入一个新元素所需要的探测次数不多于2。二次探测可以消除主集团,却可能造成次集团。
使用开链手法,表格的负载系数将大于1,SGT STL的hash_table便是采用这种做法。bucker维护hash_table_node,buckets聚合体用vector完成。hash_table的迭代器没有后退操作,也没有定义所谓的逆向迭代器。
第六章:
算法,问题之解法也。
首先提出了时间复杂度和空间复杂度,肯定是时间花的越少越好,使用的空间越小越好,这也是算法的初衷。
介绍了几十种算法,就不一一累赘了。
第七章:
讲了一下仿函数的概念,STL仿函数的分类,以操作数的个数划分,可分为一元和二元仿函数;以功能划分,可分为算术运算、关系运算、逻辑运算三大类。
第八章:
改变仿函数接口着,称为function adapter;改变容器接口者,我们称为container adapter,改变迭代器接口者,我们称为iterator adapter。
insert iterators可以将一般迭代器的赋值操作转变为插入操作,专司尾端插入操作的back_insert_iterator,专司头端插入操作的front_insert_iterator,可以任意位置执行插入操作的insert_iterator。STL提供三个相应函数:back_inserter()、front_inserter()、inserter()。
iostream iterators可以将迭代器绑定到某个iostream对象身上。绑定到istream对象身上的,称为istream_iterator,拥有输入功能;绑定到ostream对象身上的,称为ostream_iterator,拥有输出功能。
感想:这本书刚看的时候真的觉得很难,前面那一堆泛型操作什么鬼嘛,硬着头皮看(虽然看不懂,哈哈),花了12天粗略的把书过了一遍,花了两个小时写这篇博客,字有点多就不检查了,有错别字的话就将就着看吧。。。