vector的模拟实现@STL

  • 1. (constructor) & (destructor)
  • 2. 一系列基本接口
    • 2.1 size & capacity
    • 2.2 []
    • 2.3 iterator
  • 3. 尾插尾删
    • 3.0 reserve & resize
    • 3.1 push_back & pop_back
  • 4. 构造 & 拷贝构造 & 赋值重载
  • 5. 迭代器失效问题
    • 5.1 insert
    • 5.2 erase
    • 5.3 小总结
  • 附:vector.h & vector.cpp

本文将按照前后依赖逻辑链完成vector的模拟实现,重点介绍迭代器失效问题。完整的.h文件附在最后(含测试代码)

正文开始@边通书

在源码中,vector中声明了如下成员变量 ——

template<class T, class Alloc = alloc>
class vector
{public:typedef T* iterator;
private:iterator start;iterator finish;iterator end_of_storage;
};

它们的作用等同于我们之前的 _a_size_capacity ——

1. (constructor) & (destructor)

❤️ 构造函数

  • 无参构造
  • 迭代器区间构造 (后面实现了push_back再写)

类似顺序表,初始化一定要都给空,不然push_back就会出问题。

    //1.无参构造vector(): _start(nullptr), _finish(nullptr), _endofstorage(nullptr){}

❤️ 析构函数

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

2. 一系列基本接口

2.1 size & capacity

    size_t size() const{return _finish - _start;}size_t capacity() const{return _endofstorage - _start;}

2.2 []

  • _start原生指针,直接[下标]访问
    T& operator[](size_t n){assert(n <= size());return _start[n];}const T& operator[](size_t n) const{assert(n <= size());return _start[n];}

2.3 iterator

普通迭代器 & const迭代器,const对象调用const成员函数返回const迭代器,老生常谈了

    iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}

支持迭代器就支持范围for.

3. 尾插尾删

3.0 reserve & resize

插入元素要考虑扩容问题。

❤️ reserve

同样需要深拷贝

    void reserve(size_t n){if (n > capacity()){T* tmp = new T[n];size_t sz = size();//若原来已有空间,则需要释放旧空间、拷贝数据if (_start){/*memcpy(tmp, _start, sizeof(T)*sz);delete[] _start;*/for (size_t i = 0; i < sz; i++){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + sz;_endofstorage = _start + n;}}

注:

  • 注意更新*_finish*时,要提前把sz保存起来。

    // 错误示范,请勿模仿
    _start = tmp;
    _finish = _start + size();
    _endofstorage = _start + n;
    

    这样是有问题的,因为size()是指针-指针,预期的是旧空间的_finish - _start(0),但是start已经更新了,减完就是负值。

  • 拷贝数据不能简单地memcpy ,这样虽然vector是深拷贝的,但其中的对象仍然是浅拷贝的。对于int没有问题,但是对于自定义类型就出问题了。因此我们需要更改 ——

    • 若T是int,一个一个拷贝,没问题;
    • 若T是自定义类型,一个一个拷贝,调用的是T的深拷贝赋值(建立在赋值重载的基础上),也没问题。

❤️ resize

resize经典的三种情况,老生常谈了

    void resize(size_t n,const T& val = T() ){if (n <= size()){_finish = _start + n;}else{if (n > capacity()){reserve(n);}while (_finish != _start + n){*_finish = val;_finish++;}}}

说明 ——

  • 匿名对象像临时对象一样,具有常属性不可修改,必须加const

  • 匿名对象T()(生命周期在当前行)被const引用,会延长它的生命周期。引用销毁后,随之销毁(可验证)。

  • resize默认填充的是T类型的缺省值,int类型就是0;指针类型就是空指针;若是自定义类型,比如vector,那么就是调用的vector的构造函数,构造vector的匿名对象

  • C++中为了迎合模板需要,内置类型int等可认为有构造函数

3.1 push_back & pop_back

❤️ push_back

    void push_back(const T& x){if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 :capacity()*2);}*_finish = x;_finish++;}

❤️ pop_back

    void pop_back(){assert(_finish > _start);_finish--;}

4. 构造 & 拷贝构造 & 赋值重载

❤️ 迭代器区间构造

    //2.迭代器区间构造template<class InputIterator>vector(InputIterator first, InputIterator last): _start(nullptr), _finish(nullptr), _endofstorage(nullptr){while (first != last){push_back(*first);first++;}}

说明 ——

  • 一个类模板的成员函数又可以是一个函数模板,这样,迭代器区间[first,last)可以是自己类型的迭代器,又可以是任意类型的迭代器。只要这个迭代器解引用的数据能和T匹配。

  • 同样的,一定要初始化,否则一会儿要push_back时候就会出问题(当初我写顺序表时候就吃过这个亏)。

  • 关于InputIterator名称的含义:函数模板的模板参数传迭代器区间是有命名规范

    迭代器分为四类,用来访问容器 ——

    学习继承后可以有更好的理解,子类满足父类的所有特征,同时又具有新的特征。子类的迭代器都可以给父类的,反过来不行。

    因此,虽然语法上模板参数可以是任意类型,但是又有一定要求。

❤️ 拷贝构造 - 现代写法

借助构造,实现过string类,这就是老生常谈了

    void swap(vector<T>& v){std::swap(_start,v._start);std::swap(_finish, v._finish);std::swap(_endofstorage, v._endofstorage);}// 拷贝构造2 - 现代写法 - 借助构造// v2(v1);vector(const vector<T>& v): _start(nullptr), _finish(nullptr), _endofstorage(nullptr){vector<T> tmp(v.begin(), v.end());swap(tmp);//this->swap(tmp);/*swap(_start, tmp._start);swap(_finish,tmp._finish);swap(_endofstorage, tmp._endofstorage);*/}
  • 必须初始化置空,否则换给tmp,出作用域调用析构函数销毁时,会出问题(不能free一块是随机值的空间)

❤️ 赋值重载 - 现代写法

  • 复用拷贝构造
  • 同时一石二鸟,送外卖帮扔垃圾哈哈。
    // 赋值重载// v1 = v3;vector<T>& operator=(vector<T> v){swap(v);return *this;}

这里是传值调用,v是v3拷贝构造来的。在此,我们再次重新理解拷贝构造不能传值必须传引用,因为拷贝构造要先传参,传参又是一次拷贝构造,这样下去无限递归了!

5. 迭代器失效问题

5.1 insert

我们按照惯常的逻辑实现insert:检查空间、挪动数据、插入数据(代码如下)。测试代码,发现insert插入第5个数据(即扩容时),出现了随机值

    // 错误示范,请勿模仿void insert(iterator pos, const T& x){assert(pos >= _start);assert(pos <= _finish);if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 : capacity() * 2);}iterator end = _finish -1;while (end >= pos){*(end +1) = *end;end--;}*pos = x;_finish++;}

测试代码 ——

 //测试insert - 迭代器失效问题void testVector4(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);vector<int>::iterator it = find(v.begin(), v.end(), 2);if (it != v.end()){v.insert(it, 20);}for (auto e : v){cout << e << " ";}cout << endl;}

这是因为若insert时发生了扩容,会导致it指向的空间被释放,此时it指向着已被释放的空间是野指针,这种问题,我们就叫做迭代器失效

⭐️ 因此我们通过,增容时,提前算出相对位置,更新pos来解决。然而此时外边的it仍然是失效的,则通过传返回值(An iterator that points to the first of the newly inserted elements.),之后还需要使用it,再根据需要接收返回值(不能传引用,传引用会引发其他问题)

❤️ 终版insert

    iterator insert(iterator pos, const T& x){assert(pos >= _start);assert(pos <= _finish);if (_finish == _endofstorage){// 扩容会导致迭代器失效,需要提前计算更新possize_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;}iterator end = _finish -1;while (end >= pos){*(end +1) = *end;end--;}*pos = x;_finish++;return pos;}

对应测试代码 ——

 //测试insert - 迭代器失效问题void testVector4(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);vector<int>::iterator it = find(v.begin(), v.end(), 2);if (it != v.end()){it = v.insert(it, 20);// 若insert时发生了扩容,会导致it指向的空间被释放// it本质上是野指针,这种问题,我们就叫做迭代器失效}v.insert(it, 10);for (auto e : v){cout << e << " ";}cout << endl;}

运行结果 ——

5.2 erase

我们按照惯常的逻辑实现erase:挪动数据覆盖。因为不涉及重新开空间深拷贝,那这块儿确实没出现野指针的问题。

    // 错误示范,请勿模仿void erase(iterator pos){assert(pos >= _start);assert(pos < _finish);assert(_start < _finish);iterator begin = pos +1;while (begin < _finish){*(begin - 1) = *begin;begin++;}_finish--;}

现在,要求实现删除v中所有偶数,测试代码 ——

 //测试erase - 迭代器失效问题void testVector5(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);//v.push_back(5);//删除数组中的偶数vector<int>::iterator it = v.begin();while (it != v.end()){if (*it % 2 == 0){v.erase(it);}it++;}for (auto e : v){cout << e << " ";}cout << endl;}

测试如下三种场景,却发现 ——

1 2 3 4 5 -> 正常(其实是巧合)
1 2 4 5   -> 没删利索
1 2 3 4   -> 崩了
  • 1245为例 ,这代表的是相连偶数的情况。删完2(挪动数据)后it++,导致后一个偶数没有判断,4刚好就逃过去了;

  • 再分析1234,这代表的是最后一个数是偶数的情况。erase最后一个偶数,it++,此时it和_finish刚好错过,it如脱缰野马,越界访问(这时候你就说了,把!=改成<不行吗,这不符合迭代器的一般使用)

总结,erase(it)后,it指向的位置意义已经变了,直接++,会导致一些意料之外的结果,这也是迭代器失效的问题。

⭐️ 文档中写道,函数调用后会返回刚删除位置的下一个位置,即这个意义已经改变的pos (An iterator pointing to the new location of the element that followed the last element erased by the function call. This is the container end if the operation erased the last element in the sequence.)

❤️ 终版insert,添加返回值

    iterator erase(iterator pos){assert(pos >= _start);assert(pos < _finish);assert(_start < _finish);iterator begin = pos +1;while (begin < _finish){*(begin - 1) = *begin;begin++;}_finish--;return pos;}

删除v中所有偶数的正确代码 ——

 //测试erase - 迭代器失效问题void testVector5(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);//v.push_back(5);vector<int>::iterator it = v.begin();while (it != v.end()){if (*it % 2 == 0){v.erase(it);}else{it++;}}for (auto e : v){cout << e << " ";}cout << endl;}

5.3 小总结

  1. 迭代器失效主要发生在insert和erase中,用了迭代器改变了底层的数据结构。迭代器失效了,就不要再去访问;若还需要访问,可先接收返回值更新。
  2. 只要是使用迭代器访问的容器,都可能涉及迭代器失效问题。string类同样也会失效,什么时候失效参考vector即可(但是一般很少涉及迭代器失效,因为string的插入删除不太用迭代器,常用的是下标)。

附:vector.h & vector.cpp

这里要特别说明一下testVector6,是为了测试vector中是自定义类型的情况,比如string。这涉及深浅拷贝问题,reserve时若用memcpy就是浅拷贝,会有问题,就需要一个一个对象进行深拷贝赋值

如果用memcpy,会出现长字符串是乱码,短字符串能正常打印的情况。

这是因为vs做了一些优化, 如果字符串较短(16),不会去堆上申请空间了,直接在对象中在栈上放一个小数组,用空间换取时间,所以幸存了。在Linux运行,全部都是乱码 ——

#include<iostream>
#include<assert.h>
#include<algorithm>
#include<string>using namespace std;namespace beatles
{template<class T>class vector{public:typedef T* iterator;typedef const T* const_iterator;//1.无参构造vector(): _start(nullptr), _finish(nullptr), _endofstorage(nullptr){}//2.迭代器区间构造template<class InputIterator>vector(InputIterator first, InputIterator last): _start(nullptr), _finish(nullptr), _endofstorage(nullptr){while (first != last){push_back(*first);first++;}}// 拷贝构造1 - 传统写法// 注:这里的memcpy同样是有问题的,要和reserve一样一个一个拷贝//vector(const vector<T>& v)//{//  _start = new T[v.capacity()];//    _finish = _start + v.size();//    _endofstorage = _start + v.capacity();//  memcpy(_start, v._start, sizeof(T)*v.size());//}void swap(vector<T>& v){std::swap(_start,v._start);std::swap(_finish, v._finish);std::swap(_endofstorage, v._endofstorage);}// 拷贝构造2 - 现代写法 - 借助构造vector(const vector<T>& v): _start(nullptr), _finish(nullptr), _endofstorage(nullptr){vector<T> tmp(v.begin(), v.end());swap(tmp);/*swap(_start, tmp._start);swap(_finish,tmp._finish);swap(_endofstorage, tmp._endofstorage);*/}// 赋值重载vector<T>& operator=(vector<T> v){swap(v);return *this;}~vector(){if (_start){delete[] _start;_start = _finish = _endofstorage = nullptr;}}size_t size() const{return _finish - _start;}size_t capacity() const{return _endofstorage - _start;}iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}T& operator[](size_t n){assert(n <= size());return _start[n];}const T& operator[](size_t n) const{assert(n <= size());return _start[n];}void resize(size_t n, const T& val = T() ){if (n <= size()){_finish = _start + n;}else{if (n > capacity()){reserve(n);}while (_finish != _start + n){*_finish = val;_finish++;}}}void reserve(size_t n){if (n > capacity()){T* tmp = new T[n];size_t sz = size();if (_start){/*memcpy(tmp, _start, sizeof(T)*sz);delete[] _start;*/for (size_t i = 0; i < sz; i++){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + sz;_endofstorage = _start + n;}}void push_back(const T& x){if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 :capacity()*2);}*_finish = x;_finish++;}void pop_back(){assert(_finish > _start);_finish--;}iterator insert(iterator pos, const T& x){assert(pos >= _start);assert(pos <= _finish);if (_finish == _endofstorage){// 扩容会导致迭代器失效,需要提前计算更新possize_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;}iterator end = _finish -1;while (end >= pos){*(end +1) = *end;end--;}*pos = x;_finish++;return pos;}iterator erase(iterator pos){assert(pos >= _start);assert(pos < _finish);assert(_start < _finish);iterator begin = pos +1;while (begin < _finish){*(begin - 1) = *begin;begin++;}_finish--;return pos;}private:iterator _start;iterator _finish;iterator _endofstorage;};// 测试vector中的对象是自定义类型// 若按照memecpy这种错误写法会出现乱码void testVector6(){vector<string> v;v.push_back("1111111111111111111111");v.push_back("1111111111111111111111");v.push_back("1111111");v.push_back("1111111");v.push_back("1111111");for (auto& e : v){cout << e << " ";}cout << endl;}//测试erase - 迭代器失效问题void testVector5(){vector<int> v;v.push_back(1);v.push_back(2);//v.push_back(3);v.push_back(4);v.push_back(5);//删除数组中的偶数//测试三种场景//1 2 3 4 5 -> 正常(巧合)//1 2 4 5    -> 没删利索//1 2 3 4 -> 崩了//vector<int>::iterator it = v.begin();//while (it != v.end())//{// if (*it % 2 == 0)//   {//     v.erase(it);//  }// it++;//}vector<int>::iterator it = v.begin();while (it != v.end()){if (*it % 2 == 0){v.erase(it);}else{it++;}}for (auto e : v){cout << e << " ";}cout << endl;}//测试insert - 迭代器失效问题void testVector4(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);vector<int>::iterator it = find(v.begin(), v.end(), 2);if (it != v.end()){it = v.insert(it, 20);// 若insert时发生了扩容,会导致it指向的空间被释放// it本质上是野指针,这种问题,我们就叫做迭代器失效}v.insert(it, 10);for (auto e : v){cout << e << " ";}cout << endl;}// 测试拷贝构造&赋值重载,//同时测试了迭代器区间构造void testVector3(){vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(5);vector<int> v2(v1);for (auto e : v2){cout << e << " ";}cout << endl;vector<int> v3;v3.push_back(10);v3.push_back(20);v3.push_back(30);v1 = v3;for (auto e : v1){cout << e << " ";}cout << endl;}//测试resizevoid testVector2(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);//1.v.resize(6, 6);for (auto e : v){cout << e << " ";}cout << endl;//2.v.resize(10);for (auto e : v){cout << e << " ";}cout << endl;//3.v.resize(2);for (auto e : v){cout << e << " ";}cout << endl;}//测试尾插尾删,同时测试reserve//遍历测试 [] & 迭代器void testVector1(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);v.pop_back();for (size_t i = 0; i < v.size(); i++){cout << v[i] << " ";}cout << endl;vector<int>::iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++;}cout << endl;for (auto e : v){cout << e << " ";}cout << endl;}
}

vector.cpp

#define _CRT_SECURE_NO_WARNINGS 1#include"vector.h"int main()
{//beatles::testVector1();//beatles::testVector2();//beatles::testVector3();//beatles::testVector4();//beatles::testVector5();beatles::testVector6();return 0;
}

【C++】vector的模拟实现@STL —— 迭代器失效问题相关推荐

  1. 面试中常被问到(九)STL迭代器失效问题

    STL容器中迭代器失效的场景: 以vector为例进行分析 vector进行数据插入 如果空间充足那组,插入数据后,原有数据向后移动,导致迭代器失效 如果空间不足,插入新数据需要扩容,则发生拷贝,导致 ...

  2. C++ - STL迭代器失效

    迭代器失效就是说,对容器进行了一些操作后,先前的迭代器无法进行解引用操作去访问容器的元素.迭代器失效可能会造成程序崩溃,如下图 各类迭代器失效总括 Vector insert 如果插入元素导致vect ...

  3. [转载] c++ iterator 失效_C++ STL迭代器失效问题

    参考链接: C++ Vector元素改变时Iterator失效 一.迭代器失效的类型 a.由于插入元素,使得容器元素整体"迁移"导致存放原容器元素的空间不再有效,从而使得指向原空间 ...

  4. C++---STL中迭代器失效的总结

    我们在使用STL底层给我们提供的一些容器的时候,当我们给一个容器中增加(insert)或删除(erase),最有可能出现迭代器失效. 什么是迭代器失效 在对容器进行操作的时候,由于一些操作,使得元素保 ...

  5. C++知识点18——使用C++标准库(vector的增长与迭代器失效)

    描述vector容器对象大小的方法有以下几个 1.size():返回vector容器对象中实际的元素个数 2.empty():当size()返回0时,该函数返回true,否则返回false,判断容器对 ...

  6. STL中容器vector迭代器失效的相关问题

    迭代器失效,有两个层面的意思: 无法通过迭代器++,–操作遍历整个stl容器.记作: 第一层失效. 无法通过迭代器存取迭代器所指向的内存. 记作: 第二层失效. vector是个连续内存存储的容器,如 ...

  7. C++ STL : 模拟实现STL中的vector类

    文章目录 vector vector的介绍 vector的优缺点 实现时需要注意的细节问题 1. Capacity增长问题 2. memset等函数来带的按字节拷贝问题 3. 深浅拷贝问题 4. 迭代 ...

  8. 【C++】STL——vector的模拟实现

    vector的模拟实现 文章目录 vector的模拟实现 一.vector模拟实现总览 二.模拟实现vector函数接口 1. 默认成员函数 1.1. 构造函数 1.2. 析构函数 1.3.拷贝构造函 ...

  9. STL的erase()陷阱-迭代器失效总结

    下面材料整理自Internet&著作. STL中的容器按存储方式分为两类,一类是按以数组形式存储的容器(如:vector .deque):另一类是以不连续的节点形式存储的容器(如:list.s ...

最新文章

  1. 使用Git上传项目到码云仓库
  2. 对AI领域的会议的评点
  3. 二级c语言作答文件不存在,全国计算机等级考试二级C语言上机考试题库及答案...
  4. [设计模式] ------ 单例模式
  5. 三点估算pmp_【每日一练】PMP项目管理专业资格认证考试练习题(十)
  6. c#水晶报表连接mysql,如何将数据库绑定到水晶报表?
  7. ado.net mysql 转义_ADO.NET数据库查询
  8. 抓包神器之Charles,常用功能都在这里了
  9. 将网页发布到远程windows server
  10. sm4 的s盒_国密SM4算法
  11. Mac墨刀怎么导出HTML,mockingbot墨刀中文使用说明.pdf
  12. Ubuntu下截图工具推荐
  13. centos8 配置DNS服务
  14. 2019年小学计算机室管理制度,2019年小学信息技术教师工作计划表
  15. 两条平行线之间的距离
  16. fabric ca 使用案例
  17. 计算机网络(三)——数据链路层
  18. Flutter问题记录 - Unable to find bundled Java version
  19. HUAWEI华为MateBook 13 2020款 锐龙版 R7 集显 触屏 16GB+512GB (HNL-WFP9Q)原装出厂系统恢复原厂系统
  20. SQL语言基础及数据定义功能

热门文章

  1. Centos 8使用yum或者dnf安装时报错解决
  2. 一篇文章搞懂JavaScript解构赋值
  3. 怎么分析竞争对手网站
  4. jquery点击事件重复/叠加
  5. 计算机移动存储设备是,移动存储设备使用时的注意事项有哪些?
  6. 小程序源码:王者荣耀改名神器-多玩法安装简单
  7. Python基础之lambda表达式
  8. Mac的Windows支持软件
  9. Day3:《蜘蛛侠:平行宇宙》体验报告:这是你从没看过的全新版本
  10. 影响电商发展的重要因素及电商未来的发展可能