《STL源码剖析》——容器概述
空间配置器
STL中的空间配置器分为两部分:内存分配和对象构造/析构
内存分配由可以分为一级配置器和二级配置器。
内存分配
当分配内存大于128字节时,启用一级配置器,直接调用malloc和free函数进行内存分配;
当分配内存小于128字节时,会先将请求内存大小提升为8的倍数,然后到free_list中找相应的内存块,如果找到,则返回当前内存块,并从链表断开,当释放内存时,则重新将该内存块连接到free_list上。如果没有找到相应的内存块,则需要到内存池中分配20个内存块,返回一块给应用程序,剩余的挂载到free_list上;如果内存池中不足20个内存块,则尽可能多地分配内存块,返回给应用程序。如果一块都分配不出来,内存池调用malloc分配40块内存块,20块挂载到free_list,20块留在内存池中。如果内存池分配失败,则前往free_list寻找更大的内存块分配给应用程序,剩余的留在内存池中。
元素构造
construct函数调用placement new运算符进行构造;
destory函数调用对象的析构函数;
uninitialized_copy函数对未初始化内存空间调用拷贝构造函数将原迭代器[first, last)指向的元素拷贝复制到[result, result + (last-first))的未初始化空间中;
uninitialized_fill函数会对[first, last)的未初始化空间复制x;
两个函数会使用到type_traits技巧,萃取出元素的特性,如果是POD类型,那么直接调用语言内置的复制函数,否则调用类的构造函数。
迭代器
迭代器是容器和算法良好的粘合剂,算法能够在不知道容器内部的情况下就能完成对容器的遍历。
iterator_traits
迭代器器萃取是指通过迭代器萃取机制能够萃取出迭代器所指对象的相应型别,包括值类型、引用类型、指针类型、距离类型、迭代器类型。迭代器类型包括:input_iterator,output_iterator, forward_iterator, bidirectional_iterator, randomaccess_iterator。
// iterator_category的定义
struct input_iterator_tag {}
struct output_iterator_tag {}
struct forward_iterator_tag : public input_iterator_tag {}
struct bidirectional_iterator_tag : public forward_iterator_tag {}
struct randomaccess_iterator_tag : public bidirectional_iterator_tag {}
template<class T>
struct iterator_trait{
typedef typename T::value_type value_type;
typedef typename T::difference_tpe difference_type;
typedef typename T::reference_type reference_type;
typedef typename T::pointer pointer;
typedef typename T::iterator_category iterator_vategory;
}
type_traits
type_trait是萃取出类型是否包含trivial default constructor, trivial copy constructor, trivial assignment operator, trivial deconstructor, 是否为POD类型。通常使用类型萃取,根据萃取出的不同类型调用不同的构造函数,提高效率。
template<class T>
struct type_trait {
typedef __false_type T::has_trivial_default_constructor;
typedef __false_type T::has_trivial_copy_constructor;
typedef __false_type T::has_trivial_assignment_operator;
typedef __false_type T::has_trivial_destructor;
typedef __false_type T::is_POD_type;
}
对C++内部写好了对内置类型char, int, unsigned int, shot等类型的type_trait。
序列容器
Vector
vector是STL中的动态数组,和数组一样在内存中呈连续分布,vector中有size和capacity变量,分别用来指示vector中的元素大小和可容纳元素大小。当size >= capacity时,vector会进行动态扩容,即分配一块2倍当前内存大小的内存,然后将数据复制到新内存中,销毁当前内存。vector是单向开口的数据结构,在尾部插入元素效率较高,在头部插入元素会调用insert,导致vector中的所有元素移动,效率较低。vector的迭代器就是普通指针,支持随机访问。
Deque
deque是STL中的双端队列,是双向开口的数据结构,能够以极高的效率在首尾两端插入元素。deque的内部存储结构分为两级,第一级是一段存放缓冲区指针的数据,第二级是缓存区,缓存区中存放数据,默认大小为512字节。deque使用map指针(指向指针的指针)指向第一级数组,数组中的元素指向各缓冲区。deque的迭代器由四部分组成:curr(指向缓存区中元素的指针)、first(指向缓存区头部的指针)、last(指向缓存区尾部的指针)、node(缓存区指针在第一级数组中存放的位置)。deque的迭代器支持随机访问,当跨缓存区时,需要更改node指针,向尾部方向移动时last递增,向头部方向移动时,first递减。
List
list是STL中的双向链表,可以在任意位置实现插入和删除,整体呈环状结构,支持双向迭代器,不支持随机访问,遍历链表的时间复杂度是O(N)。STL为list设置了和slist相似的迭代器,不过list的迭代器支持递减操作,list的节点包含next, prev, data等数据属性。
sList
slist是STL的单向链表,只支持forward_iterator,对slit的插入和删除操作都是在头部进行的,即只支持push_front和pop_front;slist的节点分为两部分,基类包含指向下一个节点的指针,派生类(list_node)继承至基类内含数据成员。slist的迭代器也分为两部分,基类迭代器内含指向基类节点的指针,派生类继承至基类,并实现了自增、取值、指针运算等。为了能在链表中的任意位置插入和删除节点,slist提供了insert_after和erase_after函数。slist内部包含有一个头指针,通过对头指针的操作可以在O(1)内完成节点的插入和删除操作。
Heap
heap是一颗完全二叉树,底层数据存储在vector或deque(要求使用RandomAccessIterator)中,键值最大(最小)的节点在首端;插入节点时,会将元素值append到vector尾部,然后通过上溯调整根节点与子节点,使其仍然满足堆特性;删除节点时,会先将尾部元素值取出,然后将首部元素值赋给尾部元素,执行下溯调整,将子节点中的较大值赋给父节点,直至树的底部,最后将取出的尾部元素插入到堆中,完成上溯调整。STL中的heap没有迭代器,不支持遍历访问,heap文件主要为priority_queue提供make_heap/sort_heap/pop_heap/push_heap等全局函数。heap还支持用户自定义的比较函数来构建堆(最大堆、最小堆)。
Priority_queue
priority_queue是STL中的一种容器适配器,底层数据结构使用的是vector,没有定义迭代器,不支持遍历访问,支持push/pop/top/empty/size等操作,使用make_heap构建堆,每插入一个元素时,先将元素插入到底层容器vector中,然后调用push_heap调整堆;每删除一个元素时,先调用pop_heap,将首端元素复制到尾端,然后调整堆,最后使用vector的pop_back删除元素。
Stack
Stack是STL中的容器适配器,实现先进后出的栈数据结构,底层容器采用deque实现,有top/push/pop等操作,没有定义迭代器,不支持遍历访问。
Queue
queue是STL的容器适配器,实现先进先出的队列数据结构,底层容器采用deque实现,有fron/back/push/pop等操作,没有定义迭代器,不支持遍历访问。
序列容器
RB_Tree
概述
红黑树是一种特殊的平衡二叉搜索树,红黑树满足以下条件:1) 红黑树的每个分为红色和黑色两种颜色;2)根节点必须为黑节点;3)红色节点的子节点必须为黑色,两个红色节点不能直接相连;4)任意路径下(从根节点到子节点)的黑色节点数目一定相同;
对红黑树的插入操作是先使用二叉搜索树性质,根据遇大往左,遇小往右的规则找到待插入位置,将插入节点置为红色,连接到父节点上,然后调整红黑树及其节点颜色,使其符合红黑树定义。调整的过程大致分为四种,根据伯父节点的颜色和插入节点的位置(外侧或内侧)来决定。《挖坑:详细过程以后描述》
迭代器设计
红黑树的迭代和slist的设计思路相似,都是由双层节点结构+双层迭代器组成。
双层节点结构由两部分组成:基类——指向父节点指针、指向左子节点指针、指向右子节点指针;派生类——存储数据(Value);
双层迭代器也是由两部分组成:基类——内部含有一个指向基类节点结构的指针,以及increment和decrement函数,它们由operator++和operator–运算符调用,并且定义类tb_tree迭代器的类型为双向迭代器bidirectional_iterator;派生类——内部包含迭代器的完整定义,包括**相应型别(迭代器所指对象的型别)**的定义,递增递减取址指针运算符等。
核心函数
- insert_unique: 插入不允许重复的元素,set, map中使用,底层调用__insert函数完成插入操作;
- insert_equal: 插入允许重复的元素,multiset, multimap中使用;
- insert: 涉及到寻找插入元素位置和调整红黑树的操作;
- header: STL设计中的特殊实现技巧,header的左子节点指向红黑树的最左节点,header的右子节点指向红黑树的最右节点,header的父节点指向root,root的父节点指向header。
Set/Map
set和map底层都是对rb_tree的包装,set的键值和实值都是一样的,且迭代器和指针都是const,不允许修改键值。set和map的键值会自动排序,调用find函数的查找效率较高O(logn)。map的ValueType是pair类型,键值和实值分开,实值允许修改,且rb_tree的value_type也被定义为pair类型。
multiset/multimap
multiset和multimap与set和map的唯一区别是,multi在插入时调用insert_equal函数,即允许键值重复。
Hash_Table
hash_table是实现hash_set(unordered_set),hash_map(unordered_map)的底层数据结构。它由vector buckets和list组成,buckets内的每个元素是一个list根节点,STL中通过开链法来解决hash碰撞问题。vector bucket的大小为最接近且大于等于设定长度的质数。STL中为内置类型和const char*提供了hash函数,对内置类型,直接返回原值,然后再和vector bucket的长度(质数)取余得到在buckets中的index,然后将节点插入到list中。
hash_table的迭代器是一个单向迭代器,只有递增操作,对于一个bucket中的list,通过next操作即可完成指向下一个节点,如果当前bucket的list遍历完成,则进入到下一个bucket的list进行遍历。
hash_set/hash_map
hash_set和hash_map的底层数据结构由hash_table支持,与set和map一样,hash_set的键值和实值均为valuetype,且键值不允许修改,hash_map的实值为pair类型,允许修改,但同样键值不允许修改。hash_map和map一样都支持operator[]操作,都是返回pair类型的second值的引用。如果想让自定义类型作为hash_set/hash_map的键值,需要自己定义自定义类型的hash函数和equal_to函数。
示例:
class Tag {
public:
int a;
Tag(int _a) : a(_a) {}
};
struct TagHasher{
size_t operator()(Tag t) const {
return std::hash<int>{} (t.a);
}
};
struct TagEqualTo {
bool operator()(const Tag &a, const Tag &b) const {
return a.a == b.a;
}
};
hash_map<Tag, int, TagHasher, TagEqualTo > aaa;