vector模拟实现——从三个指针到对象生命周期管理
2026/8/8 9:30:12 网站建设 项目流程

本文代码已同步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(),说明startfinish两个指针相减,得到元素个数;
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、nval来初始化的构造函数
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;}

来测试一下

六、迭代器失效

我们再来测试一下inserterase

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项目持续更新

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询