C++ vector深度剖析与模拟实现:探索模板的泛型应用
前引:在C++标准模板库STL中,vector是最常用的容器之一。它以动态数组的形式提供联系内存存储,支持随机访问和高效的尾部插入\删除操作。然而,其底层实现远非简单的数组封装,而是通过精妙的内存管理策略和数据结构设计,平衡了性能与灵活性。本文将深入探讨vector的底层实现原理,包括其核心数据结构、动态扩容机制、迭代器设计,以及实际应用中的注意事项~
在上一个容器 string 的模拟实现中,我们发现 string 的模拟实现有些单调,它仅仅操作字符串,通过划分空间+类实现它的各种接口功能即可,难度还比较正常,思维逻辑正确代码不是问题;今天的 vector 作为新学的一个容器,它比 string 要复杂一些,因为它可以接纳各种类型,这就需要用到我们之前学的模板,不仅仅是写一个类这么简单~下面开始今天的vector实现,正文开始!
目录
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为空,那么无法使用memcpyif (_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已经全部是空了,所以不要使用断言
【雾非雾】期待与你的下次相遇!