C++STL库探究
STL即标准模板库,封装了一些常用的数据结构和算法
它的价值是从小来说,给用户直接提供了一套高效的算法接口,并且讲算法和数据容器分离,让算法变成不依赖数据类型,不依赖存储结构的逻辑代码。大的来说它将抽象概念通过泛型技术描述出来,对于庞大的系统利用组件分层设计。
六大组件
容器通过空间配置器获得存储空间,算法利用迭代器获取容器内容,仿函数协助算法完成不同策略变化,适配器可以修饰仿函数和容器。
容器:利用数据结构,线性树形哈希,来存储数据
算法:利用迭代器实现和数据结构无关的代码,或者依赖容器的算法
迭代器:容器和算法之间的胶水,算法看到统一的++, –遍历方法,容器有自己的遍历方式
空间配置器:提高零碎内存分配效率,解决内存碎片,防止空间浪费。
仿函数:比如在算法中需要更加普通化,sort函数需要传入定制的比较规则,把比较规则封装成类,再重载(),之后可以通过无名对象方式调用仿函数得到结果。
适配器:修饰仿函数时,根据语义需求创造出灵活的表达式供算法使用。修饰容器,直接封装其接口,像stack封装deque。
空间配置器
在容器背后默默奉献的角色,因为C++本身的动态内存分配机制不够高效,所以诞生了空间配置器。它不仅可以分配内存空间,甚至可以索取硬盘空间。
它把原先new和delete的两个阶段分开设计,分配空间是alloc,构造对象是construct,还有填充函数用于内存填充/拷贝/移动。
alloc分配内存采用两级设计,避免了内存碎片的问题。大于128字节直接用一级配置器向系统堆空间申请内存,小于128字节使用二级配置器,利用内存池管理内存。
内存池配合16个自由链表,8,16…128字节的内存块挂在链表上,类似哈希筒的方式管理回收内存。内存池主要注重的是水位线,水位线过低就要采取措施补充水源。
而不管是几级配置,给用户使用的都是同一的一个alloc接口,透明!
仿函数
仿函数可以实现智能指针的删除器,还可以实现算法的泛化。
bubble_sort
template<typename T>
class Great {
public:
bool operator()(const T& left, const T& right) {
return left > right;
}
};
template<typename T, typename Com>
void bubble_sort(T* arr, int size, Com cmp) {
bool flag = true;
for (int i = 0; i < size - 1; i++) {
flag = true;
for (int j = 0; j < size - 1 - i; j++) {
if (cmp(arr[j], arr[j + 1])) {
swap(arr[j], arr[j + 1]);
flag = false
}
}
if(flag){
return;
}
}
}
void test_bubble() {
int arr[] = { 5,6,8,7,9 };
//传入无名对象,在算法里可以直接当函数调用
bubble_sort(arr, sizeof(arr) / sizeof(arr[0]), Great<int>());
}
容器
C++有三个重要的顺序容器,vector,list和deque。顺序是指容器内的元素可以被按序排列。
关联容器主要为了查询元素是否存在,并且获取元素。有map和set。
选择vector ,list ,deque ?
在c语言中根据元素个数是否确定就能决定使用array或者list。但是对于顺序容器,都是动态增长的,主要准则是关注插入和访问特性。
vector 封装的是数组,是一段连续的内存空间,因此任意插入和删除效率很低,插入的话需要先把后续元素向后搬移,是On的复杂度。但是随机访问性很好。
list 封装了带头节点的双向循环链表,非连续地址空间,插入删除只需要调整前后结点指向,是O1的复杂度。但是不支持随机访问,每个结点还有额外的指针空间开销。
deque 是逻辑上的连续空间,但底层是许多连续空间拼接起来的,虽然支持随机访问,但是开销很大,只有在对容器头尾操作才有优势。
vector实现细节
关键技术是增容时机,SGI版本在插入元素之前先判断容量是不是满了,若满则开辟两倍大的空间,搬移元素到新空间,释放旧空间。
开辟新空间不一定在原来的地址上,因为不能保证原空间后有足够大的新空间,意味着原来的迭代器失效。
数据结构用三个迭代器,start ,finish,end分别指向已用空间,未用空间,空间末尾。迭代器是直接封装了数据类型的指针,因为指针完全胜任对连续空间的操作。
list实现细节
list封装的就是带头节点双向循环链表,所以它的实现都是基于链表的操作。
它的迭代器比较特殊,由于是非连续地址空间,迭代器必须支持++,–的操作。这需要对指针再次封装,定义它的++–操作。
deque和vector的差异
- deque在头端操作时间复杂度可以达到O1
- deque没有vector的容量概念,随时拼接上连续的空间,没有原空间不足搬移元素三步骤,扩容性比vector好,代价是迭代器复杂了
- 随机访问的性能要比vector差
排序deque元素可以先将元素复制到vector中,在vector中排序后复制回deque。
关联容器
关联容器是为了提高元素查询效率诞生的,存放结构变成了key-value。一种基于红黑树,一种基于哈希。
map,set,multi系列,由于底层是红黑树,所以是有序的,但是不能修改结点元素会破坏树的性质。
unorder系列基于哈希,为了进一步提高查询效率,哈希结构决定了它的无序性。
关联容器和序列容器的差异
- 查询的效率,逐个比较——key和地址绑定
- 无序存放,空间浪费
- 哈希筒方式的哈希冲突处理