本文代码已同步Github
一、为什么STL使用三个迭代器
1、SGI STL中的vector设计
通过之前模拟实现的string,我们知道string的底层有str,size,capacity
_str指向字符串的起始位置:_size表示字符串的有效元素个数_capacity表示字符串容量
那么vector中是否也是这样的结构呢?
我们通过g++中SGI版本的vector来观察
在说明文档中,发现vector是通过一些头文件进行了封装;
核心文件便是<stl_vector.h>;
2、vector核心成员变量
首先发现vector实际上是一个模板,这也就解释了为什么vector不仅能存储内置类型也能存储自定义类型;
其次,发现类里面typedef了许多类型名,包括把模板参数T称为value_type等
下面,我们来看一下protected中的成员变量:
里面有三个迭代器(内存池相关内容先不管),并不是我们想象的一个指针加两个变量;
对于iterator的定义则是value_type*即T*
也就是说这三个迭代器其实是三个指针;
通过名字我们猜测:
start指向有效数据的起始位置;- end
指向有效数据最后一个元素的下一个位置; end_of_storage指向这块存储空间的末尾的下一个位置;
那猜测究竟对不对呢?我们来看看实现的迭代器成员函数
begin()返回的是start,表明start指向起始位置;end()返回的是finish,表明finish指向最后一个有效位置的下一个位置;size()返回的是end() - begin(),说明start和finish两个指针相减,得到元素个数;capacity()返回的的是end_of_storage - begin(),说明end_of_storage指向的就是这块空间的末尾的下一个位置;
经过对vector底层的简单观察,发现vector有着自己独特的结构,那么我们就根据底层结构来模拟实现vector
二、vector类模板框架搭建
vector本质上是一个存储任意类型对象的容器,因此需要使用类模板实现。
注意⚠️:vector采用的是模板参数!由于模板导致变量和声明不能分离,因此我们使用<vector.h>文件来实现
下面我们来完成模拟实现的前置工作
//<vector.h>#pragmaonce#include<iostream>namespacestl{//模板参数template<classT>classvector{typedefT*iterator;private:iterator _start=nullptr;iterator _finish=nullptr;iterator _end_of_storage=nullptr;};}三、基础接口实现
注意⚠️:文档中的顺序并不适合模拟实现,各个接口之间有一定的依赖!
我们先来看库里面的构造函数的参数类型
default(1)explicitvector(constallocator_type&alloc=allocator_type());fill(2)explicitvector(size_type n,constvalue_type&val=value_type(),constallocator_type&alloc=allocator_type());range(3)template<classInputIterator>vector(InputIterator first,InputIterator last,constallocator_type&alloc=allocator_type());copy(4)vector(constvector&x);总结一下:
1、无参的默认构造函数
2、用n个val来初始化的构造函数
3、用迭代器区间初始化的构造函数
4、拷贝构造函数
我们先实现无参的默认构造函数,剩下的在后续实现(便于复用代码)
对于无参的默认构造函数,我们直接给出成员变量的缺省值;直接走初始化列表即可;
//无参的默认构造函数//即使什么都不写,成员变量也会走初始化列表,使用缺省值vector(){}接下来我们实现一些简单接口:
iteratorbegin(){return_start;}iteratorend(){return_finish;}size_tsize()const{return_finish-_start;}size_tcapacity()const{return_end_of_storage-_start;}boolempty(){return_start==_finish;}这样就完成了前置工作;
这些函数都是一眼秒懂,我们不再测试!
四、空间管理
1、reserve()
经过上一篇对vector的接口介绍以及string类的经验,我们知道reserve本质上是用来开空间的;
先来看reserve的参数
voidreserve(size_t n);分析逻辑:
如果 n > capacity,那么就需要扩容;
否则,没有影响
实现时选择声明和定义分离
template<classT>voidvector<T>::reserve(size_t n){if(n>capacity()){//扩容iterator tmp=new[n]T;memcpy(tmp,_start,sizeof(size()*sizeof(T));delete[]_start;//更新_start=tmp;_finish=_start+size();_end_of_storage=_start+n;}}此时由于还未实现push_back等操作,我们先不着急测试;
2、resize()
接着来看resize的参数
解读一下核心逻辑:
如果n < size(),就把数据减少到n个;
如果n > size(),就把有效数据个数增加到n个,同时使用val来填充;
如果n > capacity(),就会重新分配空间,把有效数据个数增加到n
template<classT>voidvector<T>::resize(size_t n,constT&val){if(n<size()){_finish=_start+n;}else{reserve(n);//挪动数据for(size_t i=size();i<n;i++){_start[i]=val;}_finish=_start+n;}}先不着急测试,等修改操作实现完之后,一起进行测试;
五、元素操作
在实现修改操作之前,我们先实现一个打印函数,用来更方便的观察测试结果;
template<classT>voidprint_vector(vector<T>&v){for(auto&e:v){cout<<e<<" ";}cout<<endl;}1、operator[]
operator[]就是返回pos位置的引用即可
T&operator[](size_t pos){assert(pos<size());return_start[pos];}2、push_back()
push_back就是尾插,考虑扩容;
voidpush_back(constT&val){if(_finish==_end_of_storage){reserve(T);}*_finish=val;++_finish;}测试 + debug
程序没有正常运行;我们调试来看
此时,当程序运行到69行时,发现_finish是空指针;
说明上面的reserve并没有正常扩容;
此时我们着重来看reserve
在更新时,正常来说_finish = _start + n应该能使得_finish更新
说明问题就在这里;
我们画图来分析一下
此时,_start已经指向了新空间的起始位置;而_finish还在指向旧空间;
size() = _finish - _start,其中_start已经更新,而_finish却还没有;
代入到_finish = _start + _finish - _start,竟然成了自赋值导致一直为空指针;
找到了问题,该怎么解决呢?
方法一:先更新_finish再更新_start
voidvector<T>::reserve(size_t n){if(n>capacity()){//扩容iterator tmp=newT[n];memcpy(tmp,_start,size()*sizeof(T));delete[]_start;//方法一_finish=tmp+size();_start=tmp;_end_of_storage=_start+n;}}我们先来看一下结果是否正确
没有问题;
方法二:提前记录size()大小
voidvector<T>::reserve(size_t n){if(n>capacity()){size_t old_size=size();//扩容iterator tmp=newT[n];memcpy(tmp,_start,old_size*sizeof(T));delete[]_start;//方法一//_finish = _tmp + size();//_start = _tmp;//_end_of_storage = _tmp + n;//方法二_start=tmp;_finish=_start+old_size;_end_of_storage=_start+n;}}用old_size来记录有效数据个数即可正常更新;
来看运行结果
没有问题
我们顺便来测一下resize
3、 pop_back()
pop_back就是尾删,直接改变_finish即可
voidpop_back(){assert(!empty());--_finish;}来测试一下
再删一次,看是否会触发断言
4、 insert()
vector底层是连续空间,因此插入删除可能需要移动大量元素,降低效率;尽量少用
发现参数全部都是迭代器;因此我们也要采用迭代器参数
我们选择实现第一个参数类型的函数
vector<T>::iteratorvector<T>::insert(iterator pos,constT&val){assert(pos>=_start);assert(pos<=_finish);if(_finish==_end_of_storage){reserve(capacity()==0?4:2*capacity());}iterator end=_finish-1;while(end>=pos){*(end+1)=*end;--end;}*pos=val;++_finish;returnpos;}我们来测试一下
5、erase()
我们先来看参数
显然,参数扔是迭代器类型的;
在pos位置删除当前元素,挪动数据并更新_finish即可
vector<T>::iteratorvector<T>::erase(iterator pos){assert(pos>=_start);assert(pos<_finish);autobegin=pos+1;while(begin!=_finish){*(begin-1)=*begin;++begin;}--_finish;returnpos;}来测试一下
六、迭代器失效
我们再来测试一下insert和erase
1、野指针
我想在末尾插入一个5,但最终打印出来却是随机值?
我们通过调试来看
程序执行到这时,应该已经完成了赋值,但却并没有正确赋值!
我们来画个图
原因就是扩容后未更新pos的指向,导致pos成了类似野指针的迭代器
怎么解决呢?
先记录距离初始位置的相对大小;接着在扩容之后更新pos
vector<T>::iteratorvector<T>::insert(iterator pos,constT&val){assert(pos>=_start);assert(pos<=_finish);if(_finish==_end_of_storage){//更新possize_t old_pos=pos-_start;reserve(capacity()==0?4:2*capacity());pos=old_pos+_start;}iterator end=_finish-1;while(end>=pos){*(end+1)=*end;--end;}*pos=val;++_finish;returnpos;}没有问题
2、位置失效
即使是没有扩容,在pos位置插入值之后,数据挪动;pos指向的位置发生改变;
同样认为迭代器失效;
如果要访问;那就需要更新迭代器之后再进行访问;
七、对象构造与资源管理
1. n个val构造
下面我们来看构造函数的其他重载
fill(2)explicitvector(size_type n,constvalue_type&val=value_type(),constallocator_type&alloc=allocator_type());分析逻辑:
先开大小为n的空间;
然后依次填入val即可
//2.n个val构造vector(size_t n,constT&val=T()){reserve(n);for(size_t i=0;i<n;i++){push_back(val);}}我们来测试一下
2. 迭代器区间构造
我们先来看库里面是怎么设计的
range(3)template<classInputIterator>vector(InputIterator first,InputIterator last,constallocator_type&alloc=allocator_type());显然,是把迭代器区间构造函数设计成了函数模板
那我们也仿照这样的设计,按照函数模板形式来实现
template<classInputIterator>vector(InputIterator first,InputIterator last){//[first,last]size_t n=last-first;reserve(n);InputIterator begin=first;while(first!=last){push_back(*first);}}来测试一下
编译报错了:
1>------ 已启动生成: 项目: vector, 配置: Debug x64 ------ 1> Test.cpp 1>D:\DailyCode\09_cpp_vector\vector\vector\vector.h(47,15): error C2100: 无法取消引用类型为“InputIterator”的操作数 1>D:\DailyCode\09_cpp_vector\vector\vector\vector.h(47,15): error C2100: with 1>D:\DailyCode\09_cpp_vector\vector\vector\vector.h(47,15): error C2100: [ 1>D:\DailyCode\09_cpp_vector\vector\vector\vector.h(47,15): error C2100: InputIterator=int 1>D:\DailyCode\09_cpp_vector\vector\vector\vector.h(47,15): error C2100: ] 1> (编译源文件“Test.cpp”) 1> D:\DailyCode\09_cpp_vector\vector\vector\vector.h(47,15): 1> 模板实例化上下文(最早的实例化上下文)为 1> D:\DailyCode\09_cpp_vector\vector\vector\Test.cpp(366,17): 1> 查看对正在编译的函数 模板 实例化“stl::vector<int>::vector<int>(InputIterator,InputIterator)”的引用 1> with 1> [ 1> InputIterator=int 1> ] 1> D:\DailyCode\09_cpp_vector\vector\vector\Test.cpp(366,17): 1> 请参阅 "stl::test_constructor" 中对 "stl::vector<int>::vector" 的第一个引用
这个报错让人抓不到头脑;
我们直接说结论,答案是测试代码的vector<int> v1(10,1)的两个参数匹配上了迭代器区间构造
为什么会匹配上呢?
对于n个val的构造函数,第一个参数需要从int->size_t,第二个参数需要从int->const int&
而对于迭代器区间构造,直接将InputIterator推导为int,无需类型转换;
由于函数模板不需要类型准换,因此直接匹配到迭代器区间构造上了;
我们加上重载函数即可解决
//3.迭代器区间构造template<classInputIterator>vector(InputIterator first,InputIterator last){//[first,last]size_t n=last-first;reserve(n);while(first!=last){push_back(*first);++first;}}vector(intn,constT&val=T()){reserve(n);for(size_t i=0;i<n;i++){push_back(val);}}3. 拷贝构造
copy(4)vector(constvector&val);有了上面两个构造函数的经验,我们直接遍历+范围for即可
提前开好空间,避免多次扩容
//4.拷贝构造vector(constvector&val){reserve(val.size());for(auto&e:val){push_back(e);}}来测试一下
4. 析构函数
释放_start指向的空间即可
delete[]会依次调用T的析构函数
~vector(){if(_start){delete[]_start;_start=_finish=_end_of_storage=nullptr;}}5. 赋值运算符重载
首先清空原有内容,接着开空间,最后依次填入即可
voidclear(){_finish=_start;}T&operator=(constT&val){clear();reserve(val.size());for(auto&e:val){push_back(e);}}我们不妨来试一下现代写法
//现代写法voidswap(vector<T>&v){std::swap(_start,v._start);std::swap(_finish,v._finish);std::swap(_end_of_storage,v._end_of_storage);}vector<T>&operator=(vector<T>&val){swap(val);return*this;}来测试一下
八、经典再现
前面的实现对于int等内置类型没有问题,但是当vector存储自定义类型时,问题才真正出现
代码出现了随机值,我们调试来看
显然是reserve出了问题;
我们还是来画图分析:
首先来看原始内存分布图:
接着来看扩容后的内存分布图:
注意:memepy是浅拷贝
由于memcpy是浅拷贝;导致新空间的每个_str指向的还是原来的位置
而此时原来空间均已被销毁,导致最终打印成随机值;
并且程序结束时会调用两次string的析构函数!
关键在于:memcpy是浅拷贝,导致拷贝后的数据如果是自定义类型,那么仍会指向被释放的空间
该怎么解决呢?
我们选择不用memcpy,而是直接采用赋值运算符,把新空间的每个对象都用原空间的对象来完成对象复制;同时也会开好新空间**
这样对于自定义类型析构时就会调用其析构函数;
对于内置类型则不做处理;
template<classT>voidvector<T>::reserve(size_t n){if(n>capacity()){size_t old_size=size();//扩容iterator tmp=newT[n];//memcpy(tmp, _start, old_size * sizeof(T));//赋值重载for(size_t i=0;i<old_size;i++){tmp[i]=_start[i];}delete[]_start;//方法一//_finish = _tmp + size();//_start = _tmp;//_end_of_storage = _tmp + n;//方法二_start=tmp;_finish=_start+old_size;_end_of_storage=_start+n;}}此时我们再来看运行结果
程序正常运行!
九、从vector模拟实现理解STL设计思想
通过对vector的模拟实现,我们不仅了解了一个动态数组容器的底层结构,也进一步理解了 C++ STL 容器设计背后的思想。
在实现过程中,我们首先认识到vector的核心并不是简单的数组封装,而是通过三个迭代器_start、_finish、_end_of_storage管理一段连续空间,通过空间大小与有效元素数量的分离,实现动态扩容的能力。
在空间管理方面,vector需要在容量不足时重新申请空间,并将原有元素迁移到新的空间中。这一过程看似简单,但其中涉及指针更新、数据拷贝以及迭代器失效等问题。通过这些问题,我们更加深入地理解了连续空间容器在效率和使用限制之间的权衡。
同时,在实现vector存储自定义类型时,我们发现简单的内存拷贝并不能保证对象的正确性。对于像string这样的类对象,其内部可能管理着动态资源,如果只复制对象本身的内存,会导致资源重复释放等问题。因此,容器不能直接操作对象内部资源,而应该依赖对象自身提供的构造、拷贝、赋值和析构等接口完成生命周期管理。
这也体现了 C++ STL 的重要设计思想:
容器负责管理元素的位置和存储方式,而元素类型负责管理自身的资源和生命周期。
通过模拟实现vector,我们不仅学习了一个 STL 容器的实现方式,更重要的是理解了 C++ 中面向对象、泛型编程以及资源管理之间的联系。从最初的类和对象,到内存管理,再到 STL 容器设计,C++ 的核心思想始终围绕着:
让对象管理自己的资源,让代码拥有更好的复用性、安全性和扩展性。
这也是 STL 能够成为 C++ 标准库核心组成部分的重要原因。
下一篇将继续通过 list 模拟实现,进一步学习链式结构、迭代器设计以及 STL 容器的抽象思想。
img-YRdjfFGT-1785832729716)]
程序正常运行!
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2tjljf0sxdj
如果觉得有帮助,可以关注Github项目持续更新