记录一下《C++ STL源码剖析》中的要点。
vector
, list
, deque
, set
, map
;sort
, search
, copy
, erase
;*
, ->
, ++
, --
;queue
, stack
;这里是指SGI版本的STL实现,即GCC中的STL实现。
Foo *pf = new Foo;
operator new
配置内存;Foo::Foo()
构建对象内容;delete pf;
Foo::~Foo()
将对象析构;operator delete
释放内存;主要涉及如下四个函数:
construct()
函数;destroy()
函数;allocate()
函数;deallocate()
函数;construct()
函数new
,同时完成空间的分配和根据值对对象的构造;destroy()
函数:有两种方式:
删除的元素范围是[first, last)
;
但也不是范围内的元素都会被删除,删除之前还要看一下这些元素的析构函数是不是无所谓的(trivial destructor),如果是无所谓的就不逐个执行,这样可以提高运行效率;
allocate()
函数SGI的STL版本将内存的分配和对象的构造两个步骤拆分开来了,其实是为了能够在更细的粒度上管理内存空间;
allocate()
函数只负责内存的分配环节,设计的动机如下:
为此,SGI设计了双层级配置器,避免空间分配过程中可能出现的大量内存破碎问题。
第一层配置器的要点如下:
第二级配置器要点如下:
只处理申请128 bytes
及以下空间的操作;
维护16个free-lists,每个list空间大小分别8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 (16*8);
如果申请的空间不是8的倍数,则会自动将申请空间上调到8的倍数,这是对应了free-lists的空间;
(2) 如果free-lists中对应大小的区块不足,则用refill()
函数向内存池中获取20个(默认)新区块,再返回其中一个区块; refill()
是通过调用chunk_alloc()
函数从内存池中获取新区块的;
(3) 如果内存池中不足20个新区块,则chunk_alloc()
函数会取尽量多的新区块;
(4) 如果内存池中连1个新区块都凑不齐,则将内存池中剩余的空间组成一个区块(大小不一定对应8的倍数),放入free-lists中;
此时start_free
是内存池空余内存的起始地址,my_free_list
是free-lists数组中某个节点的地址,以start_free
为起点的空间不足my_free_list
所代表的区块list的常规空间大小,但仍放入其中,避免浪费空间;
然后还要向heap空间申请空间补充内存池,申请空间使用malloc
函数;
(5) 如果heap空间不足,则反过来再查free-lists是否还有空间,查询从当前的free-lists[size]
开始,一直查到free-lists[15]
;
如果free-lists中也没有更大的区块可以用了,则调用第一级配置器,从而抛出异常;
整个过程归纳如下:
总之,通过划分两级配置器,可以尽最大的努力获得空间的最大利用率;
参考:
deallocate()
函数allocate()
函数一样,deallocate()
函数的实现也是依赖于两级配置器;按照前面的逻辑,SGI将对象的空间分配和对象的值构造两者分开了,因此在allocate()
函数的基础上,还需要有一些配套的函数来实现对象值的构造,这些函数是:
uninitialized_copy()
函数;uninitialized_fill()
函数;uninitialized_fill_n()
函数;uninitialized_copy()
函数commit or rollback
原则,即要么全部拷贝成功,要么全部不要拷贝;construct()
函数;[result, result+(last-first))
是输出的范围,也是要复制的空间;i
遍历输入的序列,也就是要复制的原始对象;construct(&*(result+(i-first)), *i)
执行复制;uninitialized_fill()
函数commit or rollback
原则,即要么全部拷贝成功,要么全部不要拷贝;construct()
函数;[first, last)
是要填充的范围;i
遍历输出的序列,也就是要填充的对象;construct(&*i, x)
执行填充;uninitialized_fill_n()
函数;n
个元素填充传入的参数;commit or rollback
原则,即要么全部拷贝成功,要么全部不要拷贝;construct()
函数;[first, first+n)
是要填充的范围;i
遍历输出的序列,也就是要填充的对象;construct(&*i, x)
执行填充;作用是提供一种方法,使它能够依次遍历某个容器所含的各个元素,又不用暴露该容器的内部细节。
operator*
和operator->
进行重载;auto_ptr
对象;auto_ptr
的基础上扩展,从封装原生指针扩展到为不同容器封装指针功能;auto_ptr<指向对象的类型> ptr_name(new 指向的对象);
template <class _Tp> class auto_ptr {
private:
// 成员变量,即原生指针
_Tp* _M_ptr;
public:
// 绑定类型
typedef _Tp element_type;
// 构造函数 [1] => 初始化为空指针
explicit auto_ptr(_Tp* __p = 0) __STL_NOTHROW : _M_ptr(__p) {}
// 构造函数 [2] => 初始化为一个现有的指针
auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {}
// 构造函数 [3] => 初始化为一个现有的泛型指针
#ifdef __STL_MEMBER_TEMPLATES
template <class _Tp1> auto_ptr(auto_ptr<_Tp1>& __a) __STL_NOTHROW
: _M_ptr(__a.release()) {}
#endif /* __STL_MEMBER_TEMPLATES */
// 析构函数
// 删除指针指向的内存空间
~auto_ptr() { delete _M_ptr; }
// ...
};
operator=
: // 重载operator= [1] => 复制一个现有的指针
auto_ptr& operator=(auto_ptr& __a) __STL_NOTHROW {
if (&__a != this) {
delete _M_ptr;
_M_ptr = __a.release();
}
return *this;
}
// 重载operator= [2] => 复制一个现有的泛型指针
#ifdef __STL_MEMBER_TEMPLATES
template <class _Tp1>
auto_ptr& operator=(auto_ptr<_Tp1>& __a) __STL_NOTHROW {
if (__a.get() != this->get()) {
delete _M_ptr;
_M_ptr = __a.release();
}
return *this;
}
#endif /* __STL_MEMBER_TEMPLATES */
operator*
: // 取自身指针指向内存地址的值
_Tp& operator*() const __STL_NOTHROW {
return *_M_ptr;
}
operator->
: // 返回自身指针
_Tp* operator->() const __STL_NOTHROW {
return _M_ptr;
}
_Tp* get() const __STL_NOTHROW {
return _M_ptr;
}
release
和reset
函数: // 把自身指针置空,同时返回自身指针
_Tp* release() __STL_NOTHROW {
_Tp* __tmp = _M_ptr;
_M_ptr = 0;
return __tmp;
}
// 修改指针的指向
void reset(_Tp* __p = 0) __STL_NOTHROW {
if (__p != _M_ptr) {
delete _M_ptr;
_M_ptr = __p;
}
}
operator*
和析构的时候才会操作指针指向内存地址的值,其他操作均是操作指针本身的值;构造函数;
operator*()
函数;
Item&
即引用时,该返回值可以充当左值和右值;Item
时,该返回值只能充当右值,返回时是值复制返回,会产生临时变量;*ptr = xxx;
的形式,右值是xxx = *ptr
的形式;operator->()
函数;
operator++()
函数;
operator==()
函数和operator!=()
函数;
value_type
;difference_type
;pointer
;reference
;iterator_category
;iterator_category
是和容器类型相关,其他四种信息均用于表明迭代器指向的空间所存放的数据的类型信息;partial specialization
偏特化;
/*第一种用法:只能获取泛型本身的信息*/
template<class T>
struct MyIter {
typedef T value_type; // 将泛型类型直接绑定到别名
T* ptr;
MyIter(T* p=0):ptr(p) {}
// ...
};
/*第二种用法:可以通过取泛型的成员变量获取更多的信息*/
template<class T>
struct MyIter {
typedef typename T::value_type value_type; // 将依赖于泛型的类型绑定到别名
T* ptr;
MyIter(T* p=0):ptr(p) {}
// ...
};
/*第一种用法:只能返回泛型本身的类型*/
template<class I>
I func(I ite) {
return ite;
}
/*第二种用法:可以通过取泛型的成员变量以返回更多的类型*/
template<class I>
typename I::value_type func(I ite) {
return *ite;
}
iterator_traits
泛型来进行萃取的功能;value_tyoe
difference_type
reference
reference_type
作为返回值类型,可以做左值和右值;value_type
作为返回值类型是值传递,需要进行对象复制,产生临时变量,只能做右值;pointer
iterator_category
即对应的容器所属类型;
是对迭代器对应容器的存储方式的说明;
目的是增强算法的效率,因为算法可以根据迭代器的类型来进行不同的数据访问操作实现;
总共有5种类型;
input_iterator
;
output_iterator
;
forward_iterator
;
bidirectional_iterator
;
random_access_iterator
;
高层级的类型是对低层级类型的特殊化,高层级类型一定是低层级类型;高层级类型的泛化能力下降,但是效率会提高,所以能够用高层级类型就一定会用高层级类型;
因为5种类型之间有泛化和特殊化的关系,所以定义的时候用了继承,如下:
继承实现的好处在于,只要实现了基类的处理方法,如果高层级所对应的方法找不到,会自动调用低层级的方法,这样越高层级的可兜底的方法就越多,虽然使用兜底的方法会降低了调用时的效率,但大大提高了调用时的容错率;
迭代器中的Traits实现如下:
使用iterator_category
的一个例子如下,这个例子实现了advance()
,用于迭代器的前后移动,针对不同类型的迭代器,实现了不同的泛型方法;
advance()
上层对外接口实现如下,通过iterator_category
调用上面实现的不同泛型方法;
...class InputIterator, ...
中的InputIterator
是遵循了STL算法的命名规则的写法,即以算法所能接受的最初级类型来为其迭代器类型参数命名;定义的方式为xxx
,需要说明容器类的具体类型;
xxx.begin()
和xxx.end()
返回的是迭代器类型;
通常是作为STL的算法调用参数使用;
start
指向vector
的首元素;
finish
指向vector
的尾元素之后的第一个元素,即size()
返回容量外的第一个元素,相当于是已用空间的边界(不含);
end_of_storage
指向capacity()
返回最大容量后的第一个元素,相当于是最大可用空间边界(不含);
vector
的迭代器用类型对应的普通指针即可,定义如下:vector::iterator
的本质是int *
; random_access_iterator
,参看三、3.3.3部分;uninitialized_fill_n
函数填充值;capacity()
就是申请的n
,和finish
指向相同,并没有自动增加可用空间;push_back()
函数:vector
的末尾增加一个元素;finish
处插入元素,同时++finish
;start
到finish
和finish
到end_of_storage
的元素都复制到新空间中;push_back()
每次仅增加一个元素;push_back()
函数的时候一定要注意原来的迭代器的指向问题;finish
前的新增元素个数【2】的元素挪到最后面,同时finish
后移;position
后面的元素【1】往后压,空出位置给插入的元素;finish
后的元素,让它的个数等于新增元素个数【3】,同时finish
后移;position
后的元素【2】挪到最后面,空出位置给剩下的要插入元素,同时finish
后移;finish
后的备用空间【2】小于插入元素的空间【3】,则:
finish
指针;fill()
函数和copy()
函数;vector
赋值始终保持连续;是一个双向链表;
此时的list_node
是节点类型,link_type
是指向节点的指针类型;
node
指针指向的是环状双向链表的最后一个节点list.end()
(不含),是一个空白节点,即伪头节点(亦是伪尾节点);
注意:
front
元素是node->next
;back
元素是node->pre
;node
指针指向;link_type
或者说是list_node*
的封装;迭代器类型是bidirectional_iterator
,可以双向移动,但不能进行随机读取;
增加元素、删除元素的操作不会令原来的迭代器失效;
自增和自减函数重载:
self& operator++()
相当于++i
,先自增再返回;
其他的一些获取信息的函数如下:
node
指针绑定,不会赋值;node->next
出发,一直遍历并销毁到node
;node
节点;position
之前;first
到last
(不含)的链表和原链表断开;first
到last
(不含)的链表放到position
之前;vector
增加了双向空间增长和删除,相比list
实现了随机访问,而且使用的是连续的空间,因此在实现上远比vector
或者list
复杂;map
,如下:也就是定义一个指针数组,数组中的各个指针均指向一块缓冲区;
然后用变量map
指向这个指针数组的头节点;
(2) 指向缓冲区的头尾指针:
迭代器的结构见下节;
cur
指针指向的那个元素,是某个实际数据元素,而不是整个缓冲区;start
和finish
迭代器的含义是start.cur
和finish.cur
,分别指向deque
的头元素和尾元素;deque
的start
(起始点),一个是finish
(终止点);map
指针数组的缓冲区指针是有序的,即左侧指针指向的缓冲区一定在右侧指针指向的缓冲区前面;map
的缓冲区使用从数组中央开始,向两边展开使用,而不是从头开始使用;map
数组的两头,则需要重新分配一个更大的map
数组,并把原map
拷贝过去;cur
指针是指向真正的当前访问元素;set_node()
函数用于迭代器在不同的缓冲区之间跳转;set_node()
函数是实现迭代器运算符重载的关键,它通过对map
数组指针的遍历,实现跳转到下一指针指向的缓冲区(*new_node
)的功能,其实现如下:fill_initialize()
函数用于分配空间并设置元素初值,包括:
deque
申请空间;create_map_and_nodes()
函数用于为deque
申请空间,包括:
map
数组大小,即需要节点数+2,最少为8;map
数组中间的指针,然后为每个指针申请缓冲区内存;nstart
指针和nfinish
指针赋予迭代器start
和finish
;finish
所在的缓冲区仍有一个以上空间,则直接增加即可;finish
迭代器跳到该缓冲区中;map
数组是否需要扩增;reallocate_map()
函数用于扩增:
map
数组比较空(因为是从中间向两边填充的,所以有可能出现仅用了一边的空间,导致此时map
数组的节点利用率不高),则将已经使用的节点(即map
数组中的指针元素)往数组中间挪动即可,无需申请新的空间;start
和finish
;finish.cur == finish.first
,(也就是说移除之前缓冲区就为空),则需要消除缓冲区,并回退到上一个缓冲区,再进行元素的移除,此时移除的是上一个缓冲区的最后一个元素;push_back()
相吻合的,也就是说允许有完全空的缓冲区出现,而且两端无论何时都需要有空余的空间,实现如下:start.node+1
);copy()
和copy_backward()
函数来完成复杂的支离破碎的缓冲区之间的移动,是因为迭代器已经定义和实现好迭代器前后移动的代码了,因此在高层算法的使用上,deque
和其他的容器无异;pos
实际上是指pos.cur
指针所指的元素;position.cur
处插入一个元素;erase()
函数一样,之所以不需要显式给出复杂的缓冲区之间的操作,是因为使用了copy()
和copy_backward()
函数,因此在逻辑上可以完全按照完整的双向连续空间来处理,无需理会繁杂的中控器和缓冲区管理;deque
的基础上进一步封装;deque
,而是拥有一个deque
的成员变量,这样确实能够让这个封装关系不那么复杂而且合乎逻辑,因为从逻辑来说,stack
和deque
是同级的而不是父子关系;deque
的某些功能,而且均进行了二次封装;deque
的基础上进一步封装;deque
,而是拥有一个deque
的成员变量;deque
的某些功能,而且均进行了二次封装;堆的结构是一个完全二叉树;
完全二叉树可以用数组来表示;
root
节点是最值,且位于数组的第一个元素;
因此,priority_queue
封装了一个vector
成员变量,如下所示:
vector
末尾的新加入元素上浮,以满足最大堆的性质;first
,新插入的元素在last
;vector
的最尾端(并未取走);root
下沉,直到满足大顶堆的性质;vector
最后一个元素外,其余元素满足大顶堆;pop_heap()
函数,因为每次执行都会将最大值放到vector
表示最大堆部分的末尾;vector
变成符合大顶堆性质的完全二叉树;pop_heap()
的节点下沉操作,使得以当前节点为根节点的子树满足大顶堆性质;priority_queue
的函数实现基本就是直接封装和heap
相关的函数,来实现一个完整的大顶堆的功能;push()
函数:往堆中添加元素,先用vector.push_back()
往数组中添加元素,再调用的是push_heap()
函数;pop()
函数:往堆中删除最大值元素,调用的是pop_heap()
函数,然后再用vector.pop_back()
弹出末尾的元素;slist
,这里的标题是采用了更为常用的C++11标准容器命名;__slist_node_base
,里面仅包含一个next
指针,定义如下:__slist_node_base
类,也就是包含一个next
指针;->pre
;->pre->new
;slist
的迭代器可以看作是对指向节点的指针的指针,即指向__slist_node_base
的指针;由于__slist_node
是__slist_node_base
的子类,因此通过迭代器的node
指针取值的时候需要将父类指针强制转换为子类指针,才能调用子类对象的成员变量;
迭代器的一些函数实现如下:
stack
的底层容器的,相当于是一个倒转的stack
;0
或者NULL
)进行比较;end()
函数实际上是返回了用空指针初始化的迭代器,指向空指针;key
和value
;是平衡二叉搜索树;
特点:
STL中实现了红黑树的完整容器,但并不暴露,而是作为其他关联式容器的底层容器;
使用的是指针形式二叉树来实现红黑树;
由于红黑树真的很复杂,无论是原理还是实现都很复杂(究极麻烦的分类讨论和指针操作),这里就无力再赘述了(>﹏<),可以参考原书中的内容;
key
和value
是统一,就保存一个值,既是键(可被索引和排序)也是值(可被取出使用);re_tree
的成员变量;bidirectional_iterator
,参看三、3.3.3部分;find()
函数是比较常用的函数:key
和value
是分开的,键用于被索引和排序,值用于被取出使用;pair
数据结构实现键值对;first
作为键,second
作为值;re_tree
的成员变量;set
容器的少;value
,但不能修改key
;bidirectional_iterator
,参看三、3.3.3部分;set
和map
的基础上增加了允许键值重复;insert_equal()
而不是insert_unique()
,因此可以实现键值的重复;key
的大数映射成小数,然后就可以放到一个小索引里面了;hashtable
,但并不向外暴露,而是作为其他关联式容器的底层容器使用;key
)被映射到同一个索引数组位置上;key
了,就从当前的位置开始,一直往下找,直到找到一个空闲的位置来放key
;i = i + step
,step = step + 1
;key
,则往下循环查找,直至找到;i
已有元素发生碰撞,则探查的下一个位置不是i+1
,而是i+1^2, i+2^2, i+3^2
,直到找到空闲的位置;i = i + step^2
,step = step + 1
;i = i + step*hash(key)
,step = step + 1
;hash(key)
是另外一个hash函数;key
对应的索引数组位置;i
已有元素发生碰撞,则用下一个hash函数计算位置,再探测是否有碰撞;(1) 桶数组的底层是用vector
实现;
(2) 链表用另外定义的__hashtable_node
实现,如下:
forward_iterator
类型,只能前进,不能后退;cur
和指向整个哈希表对象的指针ht
;bucket
开始往后遍历,直到找到一个非空的桶,然后把cur
指针指向这个桶链表的第一个节点;bkt_num()
函数用于计算某个key
的哈希值并返回它在指针数组中的位置,实现如下:初始的时候用一个质数初始化指针数组的大小;
如果感觉指针数组不够,就会扩增指针数组的空间,过程仍然是:申请新空间,复制原有数据,释放旧空间;
判断指针数组不够的标准是:如果已放入哈希表中的元素个数大于指针数组的大小,则扩增指针数组;
复制原有数据的操作如下:
其余的关于hashtable
的详细定义这里就不再赘述了,实现是比较像deque
的实现的;
只不过hashtable
是引入了哈希函数作为指针数组的定位,而不是用下标定义,在实现上还比deque
简单一点点,因为指向的区域是链表而不是固定连续的空间(即deque
的缓冲区),可以动态增长;
但hashtable
在扩增指针数组的时候需要rehashing重新分配每个桶内的链表,而不仅仅是修改头部指针即可;
set
的功能基本相同;set
更高,可达常数时间复杂度;hash_set
,这里的标题是采用了更为常用的C++11标准容器命名;hashtable
的成员变量;hashtable
的函数;100
,最近的质数为193
,也就是说最多可以放193个元素到哈希表中,之后就需要扩增指针数组了;map
的功能基本相同;map
更高,可达常数时间复杂度;hash_map
,这里的标题是采用了更为常用的C++11标准容器命名;hashtable
的成员变量;hashtable
的函数;100
,最近的质数为193
,可以放入的元素个数也是193;unordered_set
和unordered_map
的基础上增加了允许键值重复;hashtable
实现的insert_equal()
而不是insert_unique()
,因此可以实现键值的重复;hash_multiset
和hash_multimap
,这里的标题是采用了更为常用的C++11标准容器命名;class
或者struct
;
operator()
函数,而且必须要重载operator()
函数;cmp()
的时候;operator()
时使用;struct
即可;operator()
的重载;用于进行排序;
传入的迭代器类型:
实现机制:
16
或者32
时,则数据量较小;16
或者32
时,则数据量较小,改用直接插入排序;2*logN
仍未改用直接插入排序时,改用堆排序;参考: