C++ STL学习笔记(5) Vector容器, array容器,deque容器
动态增长的数组vector,当它放入的元素满了的时候,会自动的扩充内存,但是,在计算机中内存不能够实现原地扩充,因为在申请了一块固定大小的内存之后,这块内存不管有没有用完,他后面的内存都有可能别的内容被占用。所以扩充的过程是在内存中别的地方找到一块足够大的内存,把原来的东西搬过去。
vector的内部示意图:
vector关键部分的实现代码: finish指向的是最后一个元素的后一个位置
可以先看一下vector定义的插入元素的方法,因为内存的扩容是发生在插入元素而内存不足的时候:
可以看到,在插入元素的时候,如果内存不足,会调用insert_aux的函数:
可以看到,insert_aux函数会开始的时候检查是否空间可用(很奇怪为什么到这里了还要检查空间是否够用,如果够用,则将元素继续插入到容器中,否则再次重新扩充内存,实际上,insert_aux函数不是是在这里被调用,它在其他地方也被调用,作为插入元素的函数,所以就会在刚开始的时候检查内存时候够用,这主要是针对别的地方调用它插入元素的时候使用的),可以看到,具体的内存扩充的代码在else语句里面,具体的实现如下所示:
所以在使用vector的时候,应该考虑到,在每次扩充内存的时候,会涉及到大量元素的拷贝动作,此时会调用拷贝构造函数,当拷贝完成后,释放掉原来的内存,需要同样调用析构函数,这是非常损耗性能的。
Vector的迭代器:
vector中迭代器的设计如下:
vector中迭代器的实现与前面讲的基本一样,也需要满足Iterator的一些基本的特征。
Array容器:
array本身就是C++种的数组,将它包装成容器,它就可以C++种容器的规律,比如可以提供迭代器,以便于让算法进行相应的操作,如果不将它数据进行这样的包装,数组就会被摈弃在STL的六大部件之外,失去了和这些部件之间的联系。
array的具体实现:
dequeue容器:
双向队列,内部的结构:
deque采用分段的方式(buffer),map指向的元素是一个指针,而这个指针指向的是buffer,所以map是一个只想指针的指针,存储map所指向的元素的容器是一个vector,。每一段buffer的元素满了的时候,deque会继续申请一块同样大小的内存,然后将内存的指针向前或者向后放入到map中,从而实现deque向前或者向后扩充。
deque的迭代器:
deque的迭代器有四个元素,cur, first,last, node, first指向内存段buffer的起始位置,last指向内存段buffer中最后一个元素的后一个位置(超尾),cur指向当前的元素,node指向当前buffer的指针, 也就是说指向vector中对应的指针。
可以看到dequeue的定义如下所示:
deque类中的定义的成员,可以看到map的类型,确实是指向指针的指针,还有迭代器start, finish.,以及对应的begin(),end()方法。
下面可以看一下迭代器的具体实现,有助于理解上面的原理图:
可以看到迭代其中的四个元素,node的类型也可以知道,即node是指向指针的。
deque<T>::insert( )方法:
在deque中插入元素,deque会智能的通过插入的元素的位置而决定调用的方法:如果靠近头部,则会将插入元素到头部,如果靠近尾部,则会将元素插入尾部,否则,会采取另一种插入方法。
deque如何模拟连续空间:通过 deque iterator实现,可以看到deque内部定义的方法:
可以看到,size方法中,出现了两个迭代器相减,所以这个减法是一个重载的运算符,两个迭代器相减,返回荣光期内元素的个数,所以减号运算符的重载方法是:finish-start是map中元素的个数,而每个map中元素指向的buffer中有固定个数的元素(buffer size),所以可以根据这些值计算出容器中元素的个数。它的具体的实现如下所示:
其中,node是map种元素的指针(map种元素的内存是连续的)。所以(node - x.node - 1)就是map_size.
cur-first是当前迭代器种多出来的元素(没有放满一个buffer)
x.last-x.cur是另一个参与运算的迭代器中多出来的元素(没有放满一个buffer)
迭代器中++,--的实现:
// 前缀加 self& operator++() { ++cur; // 切换到下一个元素 if (cur == last) // 当前的buffer已经放满了 { set_node(node + 1); // 跳到下一个node 节点 map中的下一个元素 } return *this; } // 后缀加 self& operator++(int dummy) { self tmp = *this; ++*this; // 在后缀加中调用前缀加 return tmp; }
还有其他一些方法的实现,+=,-=等运算符的重载,
例如,在重载迭代器的+=运算符的时候,需要考虑到,每次加了之后,是否会超出当前的buffer,如果从超出,就需要切换到下一个node,具体的实现思路如下所示:
deque容器其他方法的实现也与此类似,都与要考虑到节点的切换问题:
容器,queue, stack
从上图可以看出queue,stack和deque的关系,在实现queue,stack的时候,不需要再重写一个queue和stack,我们只需让他们内含一个deque,然后再禁止deque中的某些方法即可,实现方法如下所示:
stack的实现:
-------------------------------------------------------end---------------------------------------------------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)