C++的STL容器解析

STL容器

空间配置器与迭代器

空间配置器

在容器中,对象的构造析构、内存的开辟与释放都是通过容器的空间配置器allocator实现的

为什么需要allocator?:我们想要的效果是,内存开辟和对象构造分离,而析构的时候也只是删除有效元素但保留开辟的空间

如果用new与free的话,new的时候就会构造默认对象,free的时候直接将空间释放掉了,在这种情况下,使用vec.push_back(t1)实际上是对new出来的默认对象进行赋值;而使用vec.push_back()的时候,实际上只是将last指针--,也不能用free,因为那样会直接释放掉空间

所以需要一个空间配置器去管理模板类、容器中的内存开辟与释放、对象构造与析构

c++标准库中的allocator实现如下:

arduino 复制代码
template<typename T>
class Allocator{
    T* allocate(size_t size){
        //使用malloc,只开辟内存空间
        return (T*) malloc(sizeof(T)*size);
    }
    void deallocate(void *p){
        free(p);
    }
    void construct(T* p, const T &val){
        //定位new,在指定地址空间进行对象拷贝构造
        new(p) T(val);
    }
    void destory(T *p){
        //使用析构函数,只析构对象而不释放空间
        p->~T();
    }
}

迭代器

迭代器提供了一种统一的方式去访问容器中的元素,每种容器都有自己的迭代器类型,通常定义在函数类的内部,作为嵌套类型,通常提供了operator*,->,++,--,==,!=,begin()与end()这些常用的成员函数与操作符重载

在不同的容器中,迭代器的实现各不相同,对于顺序容器,迭代器通常为一个指针;而对于关联容器,迭代器可能是一个包含指向键和值的指针的复合对象

以下是c++中vector迭代器实现的简化版本:

kotlin 复制代码
template<typename T>
class Vector{
private:
    T* _first;
    T* _last;
    T* _end;
public:
    class Iterator{
    private:
        T* ptr;
    
    public:
        Iterator(T* p): ptr(p){}
        
        T& opreator*() const{ return *ptr; }
        T opeartor->() const{ return ptr; }
        
        Iterator& operator++(){
            ++ptr;
            return *this;
        }
        Iterator operator++(int){
            Iteartor temp = *this;
            ++ptr;
            return temp;
        }
        
        bool opeartor==(const Iterator& other){ return ptr == other.ptr; }
        bool opeartor!=(const Iterator& other){ return ptr != other.ptr; }
    }
    Iterator begin(){   return Iteartor(_first); }
    Iterator end(){ return Iterator(_last); }
}

但在使用迭代器的时候,需要注意迭代器失效的问题

什么是迭代器失效问题?:当使用erase删除或insert插入一次元素后,从it到迭代器末尾的所有迭代器都失效了,因为涉及到了后面所有元素位置的更改(同理使用扩容操作,会使得所有迭代器都全部失效)

所以在底层会将所有的迭代器设置为链表,每当插入或删除一个元素,会检查该位置到末尾的迭代器,如果链表里的迭代器落在这个范围里面,会将该迭代器设置为失效,令其所对应的容器元素为null

为了解决这个问题,底层会对删除或插入点的迭代器进行更新操作,也就是insert或erase(it)会返回一个更新后的该位置的迭代器

比如说,不可以这么写:

scss 复制代码
auto it = vec.begin();
for(; it != vec.end(); ++it){//继续使用it这个迭代器触发错误
    if(*it % 2 == 0){
        vec.erase(it);
        //当第一次执行该代码之后,it这个迭代器就失效了
    }
}

需要这么写:

scss 复制代码
while(it != vec.end()){
    if(*it % 2 == 0){
        it = vec.erase(it);
        //当删除一个元素之后,底层的空间会进行重排,在删除元素后的所有元素都会往前挪动一位,所以此时的it更新后指向的是下一位元素
        //如果是插入操作的话,所有的元素都会往后挪一位,该语句返回的更新的it值指向的是新插入的元素,而++it指向的是原来该位置的元素,所以需要再++一次才能指向下一个
    }
    else{
        ++it;
    }
}

标准容器

顺序容器

vector:向量容器,底层数据结构是:动态开辟的数组,每次以原来空间的2倍进行扩容,内存是连续的

常用操作如下:

vec.reserve(20);:为vector预留20的空间,不会添加新元素,可以提高后续代码的执行效率

一开始默认定义的vec的大小是0,它的内存空间是随着插入的执行动态开辟的,这个过程会涉及到对象的构造析构拷贝构造等等操作,效率比较低

vec.resize(20);为vector扩容容器,不仅给容器底层开辟指定大小的内存空间,而且还会添加新元素

vec.push_back()vec.pop_back()

deque:双端队列容器,底层数据结构是:动态开辟的二维数组,一维上从两行开始,以2倍的方式扩容,二维上的数组是固定长度的数组空间;每次扩容后,原来第二维的数据,从中间开始存放,上下都预留相同行数的空行,方便支持deque的首位元素添加;而对于二维的数据来说,first和last一开始都指向中间位置(因为是双端队列)

所以它的底层内存不是连续的,每一行(二维上)是连续的,但每一列之间是不一定连续的

常用操作如下:

dep.push_back(20); deq.push_front(20); 都是O(1)的

对比vector想要首部添加只能vec.insert(vec.begin(), 20),时间是O(1)

deq.insert(it,20); O(n)

deq.pop_back(); deq.pop_front(); deq.erase(it);

list:链表容器,底层数据结构是:双向的循环链表,有pre、data、next域存储对应的指针或数据

常用操作如下:

使用和deque一模一样,不同的是mylist.insert(it, 20);是O(1)的操作(但在插入前需要进行查询操作,要从头节点往后遍历)

顺序容器总结

  • vector和deque之间的区别

    1. 底层数据结构:动态开辟的数组 vs 动态开辟的二维数组
    2. 前中后插入删除元素的复杂度:deque在前的插入和删除是O(1)
    3. 对内存的使用效率:vector随着扩容所需要的内存空间是整块连续的,使用效率低;而deque可以分块进行数据存储
    4. 在中间insert或erase,谁的效率高:vector的内存完全连续,发生插入或删除时,后续元素的挪动很简单;而deque的元素移动涉及到了分块内存的移动
  • vector和list之间的区别

    1. 底层数据结构: 动态数组 vs 双向循环链表
    2. 它们的差别就是数组与链表的差别:数组的增加删除O(n)查询O(n)随机访问O(1),链表的增加删除O(1)但定位需要O(n)

容器适配器

适配器:底层没有自己的数据结构,是另外一个容器的封装,它的方法全部由底层依赖的容器实现(比如stack就是依赖container实现的);没有实现自己的迭代器

stack和queue依赖于deque,原因如下:

  1. vector初始内存使用效率太低了,它需要两倍两倍扩容,而deque初始就为第二维数组开辟了4096/sizeof(x)的空间
  2. 对于queue来说,需要支持尾部插入和头部删除,用deque更快
  3. deque对内存的利用率更好一些

priority_queue依赖于vector,原因如下:

priority_queue是大根堆,它父节点与子节点的关系是用下标计算出来的,所以需要在一个内存连续的数组上构建(内存不连续,那下标就没有意义)

无序关联容器

特点:增删查都为O(1),底层是链式哈希表

unordered_set:单重集合

unordered_multiset:多重集合

unordered_map:单重映射表

unordered_multimap:多重映射表

单重:不允许key重复, 多重:允许key重复

常用操作如下:

set.insert(); set.size(); set.count(x);//key为x的元素有几个 set.erase(x); set.erase(it); set.find(x)//返回key为x的迭代器

以及map的operator[],一是会查询该key,二是如果该key不存在,他会插入一对数据{key, xxx}

有序关联容器

set 和 map, 底层数据结构是红黑树,迭代器的顺序遍历其实就是对红黑树进行中序遍历,是有序的

如果想对自定义的类进行set或map,需要在自定义类中的public里定义一个小于运算符的比较bool operator<() const{}

相关推荐
BeyondESH3 分钟前
Linux线程同步—竞态条件和互斥锁(C语言)
linux·服务器·c++
豆浩宇12 分钟前
Halcon OCR检测 免训练版
c++·人工智能·opencv·算法·计算机视觉·ocr
WG_1720 分钟前
C++多态
开发语言·c++·面试
Charles Ray2 小时前
C++学习笔记 —— 内存分配 new
c++·笔记·学习
重生之我在20年代敲代码2 小时前
strncpy函数的使用和模拟实现
c语言·开发语言·c++·经验分享·笔记
迷迭所归处8 小时前
C++ —— 关于vector
开发语言·c++·算法
CV工程师小林8 小时前
【算法】BFS 系列之边权为 1 的最短路问题
数据结构·c++·算法·leetcode·宽度优先
white__ice9 小时前
2024.9.19
c++
天玑y9 小时前
算法设计与分析(背包问题
c++·经验分享·笔记·学习·算法·leetcode·蓝桥杯
姜太公钓鲸2339 小时前
c++ static(详解)
开发语言·c++