模拟实现vector/迭代器失效问题

简介: ⭐模拟实现vector类⭐迭代器失效问题

对于STL,我们不仅学会使用STL,还要了解其底层原理,这样一来,我们就能知道什么时候用string好,什么时候用vector,什么时候用list,哪种方法效率高等等。其次了解了STL的底层原理,也助于我们的C++功力大增!

先来看看vector类的成员变量:下图是从《STL源码剖析》书中截取的

NEQMR_OISKVC8@9V$I[P[SS.png

vector类的成员变量有三个,分别是iterator start、iterator finish和iterator end_of_storage。我们可以用string类的成员变量来理解这三个变量:

string 类的成员变量有:T* a , size_t  _size , size_t  _capacity

start也就是a,finish也就是a+_size,end_of_storage也就是a+_capacity。

源码中的vector类:

template<classT, classAlloc=alloc>classvector {
public:
typedefTvalue_type;
typedefvalue_type*iterator;
typedefconstvalue_type*const_iterator;
//......protected:
iteratorstart;
iteratorfinish;
iteratorend_of_storage;
    }

image.gif

开始模拟实现->

为了避免冲突,先创建一个命名空间,然后在命名空间里面创建vector类:

namespacemy_vector{
template<classT>classvector {
public:
typedefT*iterator;//迭代器typedefconstT*const_iterator;//const迭代器private:
iterator_start;
iterator_finish;
iteratorend_of_storge;
    };
}

image.gif

1.构造函数:

①无参构造,将成员变量全部置为空即可

vector()
            :_start(nullptr)
            ,_finish(nullptr)
            ,_end_of_storge(nullptr)
        {}

image.gif

②迭代器区间构造:

template<classInputIterator>vector(InputIteratorfirst, InputIteratorlast)
            :_start(nullptr)
            , _finish(nullptr)
            , _end_of_storge(nullptr)
        {
while (first!=last)
            {
push_back(*first);
++first;
            }
        }

image.gif

其它的接口就先实现,方便后续使用:

交换接口:

voidswap(vector<T>&v)
        {
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storge, v._end_of_storge);
        }

image.gif

析构函数:

~vector()
        {
delete[] _start;
_start=_finish=_end_of_storge=nullptr;
        }

image.gif

清空数据:

voidclear()
        {
_finish=_start;
        }

image.gif

③拷贝构造

传统写法:

先初始化,然后开空间,最后是将被拷贝对象的值弄到拷贝对象中。在范围for循环中,最好是引用。

vector(constvector&v)
            :_start(nullptr)
            , _finish(nullptr)
            , _end_of_storge(nullptr)
        {
//先开辟空间reserve(v.capacity);
for (auto&e : v)
            {
push_back(e);
            }
        }

image.gif

现代写法:

vector(constvector&v)
            :_start(nullptr)
            , _finish(nullptr)
            , _end_of_storge(nullptr)
        {
vector<T>tmp(v.begin, v.end);//用迭代器区间构造,找个打工人swap(tmp);
        }

image.gif

2.插入数据的相关函数接口:

①reserve()的模拟实现:

因为在插入数据时,不管是最初状态还是空间满的时候,都得扩容,所以就先实现reserve()。因为reserve是不会缩容的,缩容和扩容是需要代价的,而扩容是不可避免的,但是缩容就不必要了,采用空间换时间的策略。

在最初状态,_start是指向空指针的,因此在扩容的时候需要判断一下。

voidreserve(size_tn)//不止是在插入数据的时候使用,也可以单独使用        {
if (n>capacity())//因为不缩容,所以判断一下,大的时候才处理            {
size_toldSize=size();
T*tmp=newT[n];
if (_start)//判断一下_start是否为空,因为如果一开始的capacity是空的,然后赋值为4,但是此时的_start是nullptr,给不了数据                 {
//不建议用memcpy,因为它是浅拷贝,如果T是string、vector等等,就会出错//memcpy(tmp, _start, sizeof(T) * size());for (size_ti=0; i<oldSize; i++)
                    {
tmp[i] =_start[i];
                    }
delete_start;
                }
_start=tmp;
_finish=tmp+oldSize;
_end_of_storge=_start+n;
            }
        }

image.gif

②size()和capacity():

实现size()和capacity(),方便操作,并且这也是需要实现的一部分。

size_tsize() const        {
return_finish-_start;
        }
size_tcapacity() const        {
return_end_of_storge-_start;
        }

image.gif

③push_back():

因为_finish表示的是有效元素的个数,指向的是最后一个元素的下一个位置,因此不管是否需要扩容,尾插的位置必然是_finish指向的位置。

voidpush_back(constT&x)
        {
if (_finish==_end_of_storge)//判断空间是否满了            {
size_tnewCapacity=capacity() ==0?4 : capacity() *2;
reserve(newCapacity);
            }
*_finish=x;//直接在_finish这个位置插入数据++_finish;
        }

image.gif

④insert接口/迭代器失效问题

void insert(iterator pos, const T& val);

这部分很重要,因为涉及了迭代器失效问题!我们都知道,在插入数据前,我们需要进行一次判断,判断容器的容量是否满了,如果满了,则需要扩容,而问题也就发生在这里,扩容会导致迭代器失效的问题!(当然,迭代器失效的问题不仅仅会出现在这)

在扩容的时候,是重新开辟一块大的空间,然后释放原来的空间,看下图:

ELZ9322DT@EX_`}[1UVW9Q5.png

这样就导致了插入数据失败。解决问题呢,就是更新pos,让pos指向新空间的同一块位置:在扩容前记录一下pos距离_start的长度,在扩容后,让pos重新指向新空间的_start+len处。

//迭代器失效问题voidinsert(iteratorpos, constT&val)
        {
assert(pos>=_start);
assert(pos<_finish);
if (_finish==_end_of_storge)
            {
size_tlen=pos-_start;//算出pos距离_start的长度size_tnewCapacity=capacity() ==0?4 : capacity() *2;
reserve(newCapacity);
//待空间更新,就更新pos的位置,解决pos失效的问题pos=_start+len;
            }
iteratorend=_finish-1;
//挪动数据while (end>=pos)
            {
*(end+1) =*end;
--end;
            }
*pos=val++_finish;
        }

image.gif

当然,不扩容的话,就可能不会导致失效的问题了。其实迭代器失效,也就是野指针的问题。

解决迭代器哦失效,便是

3.实现迭代器

普通对象迭代器:

刚好,迭代器的begin刚好就是_start,end也刚好是_finish。

iteratorbegin()
        {
return_start;
        }
iteratorend()
        {
return_finish;
        }

image.gif

const迭代器:

const_iteratorbegin() const        {
return_start;
        }
const_iteratorend() const        {
return_finish;
        }

image.gif

4.访问接口[]:

T&operator[](size_tpos)
        {
assert(pos<size());
return_start[pos];
        }
constT&operator[](size_tpos) const        {
assert(pos<size());
return_start[pos];
        }

image.gif

5.判空

当_finish等于_start的时候,说明此时容器是空的,没有空间,没有数据。

boolempty() const        {
return_finish==_start;
        }

image.gif

6.resize()

对于resize(size_t n,T val = T()),是针对有效元素个数的。当n是大于容器当前的容量,则代表着是扩容+插入数据,当n小于容器的当前容量,大于当前有效元素个数,那么代表着不扩容,但是插入数据,当n小于当前容量,那么就是相当于删除数据。

那么插入的数据的话,缺省值是T(),即匿名对象,因为T有可能是string类型,是Date类型,是各种各样的类型,因此需要它的构造函数去初始化。

voidresize(size_tn, Tval=T())//T()是匿名对象,初始化resize的空间        {
if (n>capacity())//扩容            {
reserve(n);
            }
if (n>size())//不扩容,但是插入数据            {
//从_finish开始填数据while (_finish<_start+n)
                {
*_finish=val;
++_finish;
                }
            }
else//删除数据            {
_finish=_start+n;//相当于把有效元素的个数减少到n个            }
        }

image.gif

7.删除数据的接口:

①pop_back();

voidpop_back()
        {
assert(empty());//如果容器已经空, 再次调用的话,直接报错--_finish;
        }

image.gif

②erase()接口以及其引起的迭代器失效

删除任意位置,即挪动要删除的数据的后面的位置,将他们往前挪即可。

EH`@NBXV7((NXYPUQD%7[VQ.png

voiderase(iteratorpos)
        {
//pos的位置要合理assert(pos>=_start);
assert(pos<_finish);
iteratorbegin=pos+1;
while (begin<_finish)
            {
*(begin-1) =*begin;
++begin;
            }
--——finish;
        }

image.gif

删除操作,会让pos迭代器会失效,但注意,在Linux下g++中不会报错,不会失效,因为g++采用的是模拟实现,它做不到识别失效问题,pj版能够识别,是因为它不是一个原生指针。但不管怎么样,一般来说统一认为它会失效!

而解决失效问题,可以将代码改成如下:

iteratorerase(iteratorpos)
        {
//pos的位置要合理assert(pos>=_start);
assert(pos<_finish);
iteratorbegin=pos+ ;
while (begin<_finish)
            {
*(begin-1) =*begin;
++begin;
            }
--_finish;
returnpos;
        }

image.gif

删除后,更新pos位置即可。

8.find导致的迭代器失效问题

my_vector::vector<int>::iteratorit=find(arr.begin(), arr.end(), 3);
if (it!=arr.end())
    {
arr.insert(it, 30);
    }
//可能发生迭代器失效    (*it)++;

image.gif

如上代码,在insert之和,it会发生迭代器失效。其原因是:即使我们在insert后,pos位置更新,使得pos没有失效,但是对于reserve接口,传参是传值传参,pos的改变不会影响it,而it作为参数传到insert接口后,由于原本指向的空间已经释放了,it就变成了野指针,也就是迭代器失效了。当然了,如果没有发生扩容,就可能不会发生失效。

在C++官方库里面,insert也没有引用作参数,也是传值传参的。简单地解释一下就就是,有时候传进去的是一些具有常性的临时变量,不可传,比如insert(arr.begin(),1);,其中的arr.begin()就是临时变量。

9.赋值操作

vector<T>&operator=(vector<T>v)
        {
swap(v);
return*this;
        }

image.gif


相关文章
|
存储 Linux 测试技术
vector迭代器失效与深浅拷贝问题
上文我们写了insert的模拟实现,最开始的版本是有许多Bug的,比如迭代器失效,最后经过优化修改实现了insert,这里我们以最初的版本为例,分析并解决迭代器失效问题。如下:
|
算法 区块链 C++
【C++要笑着学】vector 核心框架接口的模拟实现 | 基于STL3.0版本的简化vector | 浅谈迭代器失效问题(二)
STL 的源代码整体考虑的东西比较多,还要考虑和其他地方的结合,因此整体的设计是比较复杂的。基于这一系列原因,我们会以简单的形式去实现其核心框架接口,方便去学习 vector。
123 1
【C++要笑着学】vector 核心框架接口的模拟实现 | 基于STL3.0版本的简化vector | 浅谈迭代器失效问题(二)
|
存储 测试技术 区块链
【C++要笑着学】vector 核心框架接口的模拟实现 | 基于STL3.0版本的简化vector | 浅谈迭代器失效问题(一)
STL 的源代码整体考虑的东西比较多,还要考虑和其他地方的结合,因此整体的设计是比较复杂的。基于这一系列原因,我们会以简单的形式去实现其核心框架接口,方便去学习 vector。
114 1
【C++要笑着学】vector 核心框架接口的模拟实现 | 基于STL3.0版本的简化vector | 浅谈迭代器失效问题(一)
|
C++ 容器
【C++初阶:STL —— vector】vector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现 下
【C++初阶:STL —— vector】vector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现
181 0
【C++初阶:STL —— vector】vector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现 下
|
存储 算法 Linux
【C++初阶:STL —— vector】vector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现 上
【C++初阶:STL —— vector】vector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现
459 0
【C++初阶:STL —— vector】vector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现 上
|
存储 容器
容器大小的改变以及容器操作可能使迭代器失效、vector对象的容量变化
1 改变容器的大小 我们可以使用resize来增加或缩小容器,与往常一样,array不支持resize。如果当前大小大于所要求的大小,容器后面的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部:  list ilist(10,42);   //10个int:每个的值都是42 ilist.
1061 0
|
7月前
|
Shell Android开发
Android系统 adb shell push/pull 禁止特定文件
Android系统 adb shell push/pull 禁止特定文件
603 1
|
7月前
|
Android开发 Python
Python封装ADB获取Android设备wifi地址的方法
Python封装ADB获取Android设备wifi地址的方法
167 0
|
开发工具 Android开发
Mac 安卓(Android) 配置adb路径
Mac 安卓(Android) 配置adb路径
876 0
|
4月前
|
Shell Linux 开发工具
"开发者的救星:揭秘如何用adb神器征服Android设备,开启高效调试之旅!"
【8月更文挑战第20天】Android Debug Bridge (adb) 是 Android 开发者必备工具,用于实现计算机与 Android 设备间通讯,执行调试及命令操作。adb 提供了丰富的命令行接口,覆盖从基础设备管理到复杂系统操作的需求。本文详细介绍 adb 的安装配置流程,并列举实用命令示例,包括设备连接管理、应用安装调试、文件系统访问等基础功能,以及端口转发、日志查看等高级技巧。此外,还提供了常见问题的故障排除指南,帮助开发者快速解决问题。掌握 adb 将极大提升 Android 开发效率,助力项目顺利推进。
114 0