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之间的区别
- 底层数据结构:动态开辟的数组 vs 动态开辟的二维数组
- 前中后插入删除元素的复杂度:deque在前的插入和删除是O(1)
- 对内存的使用效率:vector随着扩容所需要的内存空间是整块连续的,使用效率低;而deque可以分块进行数据存储
- 在中间insert或erase,谁的效率高:vector的内存完全连续,发生插入或删除时,后续元素的挪动很简单;而deque的元素移动涉及到了分块内存的移动
-
vector和list之间的区别
- 底层数据结构: 动态数组 vs 双向循环链表
- 它们的差别就是数组与链表的差别:数组的增加删除O(n)查询O(n)随机访问O(1),链表的增加删除O(1)但定位需要O(n)
容器适配器
适配器:底层没有自己的数据结构,是另外一个容器的封装,它的方法全部由底层依赖的容器实现(比如stack就是依赖container实现的);没有实现自己的迭代器
stack和queue依赖于deque,原因如下:
- vector初始内存使用效率太低了,它需要两倍两倍扩容,而deque初始就为第二维数组开辟了4096/sizeof(x)的空间
- 对于queue来说,需要支持尾部插入和头部删除,用deque更快
- 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{}