C++STL库探究

C++STL库探究

STL即标准模板库,封装了一些常用的数据结构和算法

它的价值是从小来说,给用户直接提供了一套高效的算法接口,并且让算法和数据容器分离,让算法变成不依赖数据类型,不依赖存储结构的逻辑代码。大的来说它将抽象概念通过泛型技术描述出来,对于庞大的系统利用组件分层设计。

六大组件

容器通过空间配置器获得存储空间,算法利用迭代器获取容器内容,仿函数协助算法完成不同策略变化,适配器可以修饰仿函数和容器。

容器:利用数据结构,线性树形哈希,来存储数据

算法:利用迭代器实现和数据结构无关的代码,或者依赖容器的算法

迭代器:容器和算法之间的胶水,算法看到统一的++, –遍历方法,容器有自己的遍历方式

空间配置器:提高零碎内存分配效率,解决内存碎片,防止空间浪费。

仿函数:比如在算法中需要更加普通化,sort函数需要传入定制的比较规则,把比较规则封装成类,再重载(),之后可以通过无名对象方式调用仿函数得到结果。

适配器:修饰仿函数时,根据语义需求创造出灵活的表达式供算法使用。修饰容器,直接封装其接口,像stack封装deque。

空间配置器

在容器背后默默奉献的角色,因为C++本身的动态内存分配机制不够高效并且有内存碎片问题,所以诞生了空间配置器。它不仅可以分配内存空间,甚至可以索取硬盘空间。

它把原先new和delete的两个阶段分开设计,分配空间是alloc,构造对象是construct,还有填充函数用于内存填充/拷贝/移动。

重点研究的是分配空间的alloc模块。

alloc分配内存采用两级设计,避免了内存碎片的问题。大于128字节直接用一级配置器向系统堆空间申请内存,小于128字节使用二级配置器,利用内存池和自由链表管理内存。

内存池配合16个自由链表,8,16…128字节的内存块挂在链表上,类似哈希筒的方式管理回收内存。

模型:
这里写图片描述

分配一块空间的流程:
这里写图片描述

回收内存?

用户申请时可以用定位new,用完了空间不是直接malloc返回给系统,而是调用Deallocate,把用完的空间挂在对应了freelist链表底下。

这样在频繁申请释放的场景下,不需要再去向系统申请空间,而是直接取链表底下挂的空间,提高了效率。在release下大约有10倍时间上的差距。

线程安全?

模板参数列表有开启线程的开关,若定义了多线程使用,就在向操纵某一个freelist下标的链表之前加锁,当其他线程取不同字节的空间是不会触发锁的,因此能减小锁竞争力度,释放也一样。在操纵内存池时加入单独的互斥锁。

不管是几级配置,给用户使用的都是同一的一个alloc接口,透明!

迭代器

迭代器是实现容器,算法泛化的重要组件。在容器内部提供迭代器的实现,在算法层面直接去应用迭代器,算法看到的是统一的 ++, – , == 这些操作。而本质上是在容器里对指针进行了运算符重载。

vector底层是连续的地址空间,所以原生指针就可以胜任迭代器。

list的迭代器把链表的遍历方式封装为++,–。

迭代器失效?

vector只要引起空间重新配置,原有迭代器都设置无效。

/*例如执行这样的代码*/
vector<int> v = {1, 35,54,2,3};
for(auto it = v.begin(); it != v.end(); it++){
    if(i++ < 2)
        v.erase(it);
}
//抛出迭代器失效的异常,it在删除指向元素之后就无效了
//erase的实现是这样的,利用erase的返回值重新复制it
iterator erase(iterator position){
    if(position +1 != end()){
        copy(position +1,  finish, position);//拷贝
    }
    --finish;
    destory(finish);
    return position;
}

//正确的姿势
for (auto it = v.begin(); it != v.end(); ) {
    if(i++ < 2)
        it = v.erase(it);
    else{
        it++;
    }
}

总结:迭代器失效分三种情况考虑,分别为数组型,链表型,树型数据结构。

数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效。

解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);

链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).

树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。

仿函数

仿函数就是重载了类的(),让其使用着像函数一样。一般是通过无名对象传参,或者函数指针也是狭义的仿函数。

仿函数可以实现智能指针的删除器,可以实现算法的灵活性。

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分别指向已用空间,未用空间,空间末尾。迭代器是直接封装了数据类型的指针,因为指针完全胜任对连续空间的操作。

vector没有提供方法使容量减小,可以通过创建tmp对象,swap的小手段。

list实现细节

list封装的就是带头节点双向循环链表,所以它的实现都是基于链表的操作。

它的迭代器比较特殊,由于是非连续地址空间,迭代器必须支持++,–的操作。这需要对指针再次封装,定义它的++–操作。

deque和vector的差异

  1. deque在头端操作时间复杂度可以达到O1
  2. deque没有vector的容量概念,随时拼接上连续的空间,没有原空间不足搬移元素三步骤,扩容性比vector好,代价是迭代器复杂了
  3. 随机访问的性能要比vector差

排序deque元素可以先将元素复制到vector中,在vector中排序后复制回deque。

关联容器

关联容器是为了提高元素查询效率诞生的,存放结构变成了key-value。一种基于红黑树,一种基于哈希。

map,set,multi系列,由于底层是红黑树,所以是有序的,但是不能修改结点元素会破坏树的性质。键值必须是可比较,不可变,可拷贝的对象,这是创建红黑树结点的要求。

unorder系列基于哈希,为了进一步提高查询效率,哈希结构决定了它的无序性。

关联容器和序列容器的差异

  1. 查询的效率,逐个比较——key和地址绑定
  2. 无序存放,空间浪费
  3. 哈希筒方式的哈希冲突处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值