C++ vector深度剖析与模拟实现:探索模板的泛型应用


前引:在C++标准模板库STL中,vector是最常用的容器之一。它以动态数组的形式提供联系内存存储,支持随机访问和高效的尾部插入\删除操作。然而,其底层实现远非简单的数组封装,而是通过精妙的内存管理策略和数据结构设计,平衡了性能与灵活性。本文将深入探讨vector的底层实现原理,包括其核心数据结构、动态扩容机制、迭代器设计,以及实际应用中的注意事项~
在上一个容器 string 的模拟实现中,我们发现 string 的模拟实现有些单调,它仅仅操作字符串,通过划分空间+类实现它的各种接口功能即可,难度还比较正常,思维逻辑正确代码不是问题;今天的 vector 作为新学的一个容器,它比 string 要复杂一些,因为它可以接纳各种类型,这就需要用到我们之前学的模板,不仅仅是写一个类这么简单~下面开始今天的vector实现,正文开始!
目录
vector的核心数据结构
vector 的底层实现依赖于三个关键指针(或者迭代器),它们共同管理内存空间与元素布局:


_start:指向数组的起始位置,即第一个元素的内存地址
_finish:指向最后一个元素的下一个位置,用于标记已分配但未使用的空间边界
_end_of_storage:指向当前分配内存的末尾,标记整个连续内存块的结束

这三个指针的动态调整是 vector 实现高效内存管理的核心。例如:当插入元素空间不足时,_finish会触发扩容逻辑,分配更大的内存块并迁移数据!下面我们来通过2倍扩容来实现 vector
模板框架搭建
既然 vector 的实现是依靠模板来的,那么推理出来就是:在自定义空间中用模板实现类
namespace Seek { //vector模板 template<class T> class vector { public: //实现 private: T* _start; T* _finish; T* _end_of_storage; }; }类模板的实例化:空间声明+模板类类型
Seek::vector<int> S1;构造初始化
观察库里面的 Vector 我们发现在没有任何参数时capacity也是0,例如:

那么我们开始只需要将三个指针全部初始化为空就行了
//构造初始化 vector() :_start(nullptr) ,_finish(nullptr) ,_end_of_storage(nullptr) { }析构函数
析构不能释放空,因此需要先判断指针是否开辟了空间,然后再置空
//析构 ~vector() { //判断 assert(_start); //释放空间 delete[]_start; //置空 _start = _finish = _end_of_storage = nullptr; }尾插数据
首次学习 vector 实现我们以整形为主进行学习
在尾插时我们可能需要更改三个指针的位置,因此需要先计算一下:size()、capacity
原理:指针-指针=中间的元素个数(通过这样我们可以控制三个指针的移动)
//size size_t size()const { return _finish - _start; } //capacity size_t capacity()const { return _end_of_storage - _start; }在尾插时需要考虑:如果空间已满 或者 _start为空。如果_start为空,那么无法使用memcpy
//尾插 void push_back(const T pc) { if (_finish == _end_of_storage) { //说明此时空间已满 或者 空间为空 size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //如果_start为空,那么无法使用memcpy if (_start == nullptr) { _start = new T[newcapacity]; _finish = _start; _end_of_storage = _start + newcapacity; } else //开空间 reserve(newcapacity); } //存 *_finish = pc; _finish++; }扩容
扩容就是 reserve ,传入指定数量的空间,reserve负责开辟,然后更新三个指针指向新空间
//扩容 void reserve(size_t newcapacity) { assert(newcapacity > 0); //记录 size_t size_tmp = size(); //开辟空间 T* tmp = new T[newcapacity]; //拷贝内容 memcpy(tmp, _start, sizeof(T) * newcapacity); //释放空间 delete[]_start; //更新指针 _start = tmp; _finish = _start + size_tmp; _end_of_storage = _start + newcapacity; }注意:我们开辟新空间后,很容易忘记_finish的指向,而在释放原空间之前,需要标记元素个数
扩容+初始化



如果resize小于_finish,那么就保留resize及其以前的数据
如果resize大于_finish,那么就需要扩容+初始化(如果没有给初始值,调用构造函数)
//扩容+初始化 void resize(size_t n, const T& val=T()) { if (n < size()) { //直接删 while (size() > n) { _finish--; } } else { //扩容 reserve(n); while (n != size()) { *_finish = T(); _finish++; } } }效果展示:

打印
如果实现流提取,虽然可以用友元解决,因为是模板无法识别变量类型,所以在成员函数内完成
而vector的类型太多了,如果去为它实现一个流提取是没有必要的,不向string是确定的
数组属于连续存储的,因此直接解引用\下标就可以拿到数组元素,连续的内存存储也支持加加的
//打印 void Print()const { assert(_start); for (T* tmp = _start ; tmp != _finish ; tmp++) { cout << *tmp << " "; } cout << endl; }效果展示:

迭代器
迭代器应该是返回有效元素的这个区间的指针指向,而不是放大到容量
这里数据的开始刚好是_start
末尾的下一个位置刚好是_finish \ _start+元素个数
//迭代器 iterator begin()const { return _start; } iterator end()const { return _start + size(); }删除指定位置元素

可以看到删除指定位置元素的参数是迭代器类型:iterator,这里需要注意的是_finish的位置

//删除指定位置元素 void erase(iterator pos) { //判断有效性 assert(pos >= _start && pos < _finish); //挪动元素 while (pos + 1 < _finish) { *pos = *(pos + 1); pos++; } _finish--; }效果展示:

插入元素在指定位置之前

可看到它的位置也是根据迭代器去做参数的
//插入元素在指定位置之前 void insert(iterator pos, T tmp) { //检查范围 assert(pos > _start && pos <= _finish); //看是否需要扩容 if (_finish == _end_of_storage) { size_t newcapacity = capacity() * 2; reserve(newcapacity); } //挪动元素 iterator it = _finish; while (it >= pos) { *it = *(it - 1); it--; } *(pos - 1) = tmp; _finish++; }
迭代器失效
迭代器失效的场景:
(1)insert 插入触发内存扩容,所有原有迭代器、指针、引用失效
(2)insert 插入未触发内存扩容,但元素位置移动,之后的迭代器失效
(3)erase 删除元素之后,被删除元素及其之后的所有迭代器失效
(4)clear 清理元素之后,所有迭代器失效
迭代器失效的原因(以下两种都会使原来的迭代器全部失效):
(1)内存的重新分配,vector是基于动态数组实现的容器,当元素超过容量时实现扩容,vector 重新分配内存(比如 insert )
(2)元素移动:即使没有内存重新分配,insert 会使插入的元素后移,erase 会使删除的元素前移
具有代表的就是 insert 和 erase 接口,迭代器失效与内部实现有关,比如STL里面的:

如果要解决可以接收 erase 的返回值获取下一个有效的迭代器,例如:


拷贝构造
拷贝构造是用一个已经存在的对象去创建+初始化另一个对象
注意:不能使用memcpy,因为memcpy是按字节拷贝,如果是自定义类型会发生浅拷贝情况
虽然这里的 T 是int类型,但是为了泛化使用,避免自定义类型发生浅拷贝
对于连续的地址,可以使用下标[ ] 或者 直接解引用访问内容
//拷贝构造 vector(const vector<T>& V) { _start = _finish = _end_of_storage = nullptr; //开空间 _start = new T[V.capacity()]; //拷贝 iterator tmp = V._start; iterator it = _start; while (tmp != V._finish) { *it = *tmp; it++ ; tmp++; } _finish = _start + V.size(); _end_of_storage = _start + V.capacity(); }效果展示:

赋值重载
我们可以通过拷贝构造出中间变量,再去交换临时变量的指向来完成初始化
//赋值运算符重载 vector<T>& operator=(const vector<T>& V1) { swap(V1); return *this; } void swap(const vector<T>& V1) { //V2是临时的,出了函数会销毁 vector<T> V2(V1); std::swap(_start, V2._start); std::swap(_finish, V2._finish); std::swap(_end_of_storage, V2._end_of_storage); }注意:开始我们构造出的*this是空的,我们利用另一个对象V1去深拷贝出V2,再将V2的内容给交 换给*this,当出了swap函数去释放V2的时候,V2已经全部是空了,所以不要使用断言
【雾非雾】期待与你的下次相遇!