Learning C++ No.14【STL No.4】

简介: Learning C++ No.14【STL No.4】

引言:

北京时间:2023/3/9/12:58,下午两点有课,现在先把引言给搞定,这样就能激励我更早的把这篇博客给写完了,万事开头难这句话还是很有道理的,刚好利用现在昏昏欲睡的时候,把这个没什么干货的引言写完,并且刚刚刷了一下知乎,我发现现在的知乎有两个字很合适形容(掘金),例,我刚刚随意看了一个有关大学生4年应该干什么的文章,写这种文章的,我不多做评价,我深信人无完人这一理念,我深知自己并不优秀,但我知道,我的三观还行,例如:我并不觉得送外卖……(可能有人说……),我反而时常会想象自己熬夜送外卖的场景,我觉得这样还挺好的,凭自己的双手赚钱;什么叫上好大学……,什么叫高考……,什么叫富二代……,我只想好好看电视,好好吃饭,找一个能聊的来的朋友,所以像什么吹牛比自卑好,我不能理解,反正我宁愿自卑我都不愿意吹牛,做人不是为了给别人增加压力,开玩笑的吹牛叫搞笑,认真的吹牛叫说谎,所以咱还是继续自卑吧!做不到的事情、未可知的事情,咱不干,咱只谈实际,咱做好自己,例,美剧……,ok,这篇博客,我们就继续深入学习迭代器失效的问题,和vector类知识的收尾,然后把list(带头双向循环链表)给开个头(下篇博客就是自我实现list),So,gogogo!


6.png

前提:此时的我,因为上午写完了一篇博客,现在心情非常的好,只是有点困,并且以我的情商,舍友之间关系还是非常的ok的(只是有点可惜,没能找到能谈心的),校园人际关系也还行(毕竟我没有什么认识的人,哈哈哈!),上述只是对知乎上的某些清华级别文章做出的感想!并且通过文字证明:耍嘴我也行,哈哈哈!好在这里没有吹牛!写(耍嘴我才是祖师爷!),哈哈哈!其实我也只会哈哈哈,别的我也不是很在行,哈哈哈!看到这么多哈哈哈,这里刚好想起来,明天5哈(综艺)就更新了哦!又不会无聊啦!(不过谈到5哈,这里我对当今的综艺情况也是略有一点见解的),这里就不多做赘述了,此处省略一万字……


再谈迭代器失效问题

上篇博客,我们已经搞明白了插入函数迭代器失效的原因和解决的方法,这里我们不多加深入,这里我们就通过删除函数(erase),来再次进入到迭代器失效的问题上,如下图:

7.png


此时按照上述代码的写法,我们就可以发现,在删除数据的时候,会导致_finish(end)的位置减减到pos(it)位置的前面,这样就会导致地址不匹配(pos再也找不到迭代器区间的地址),这个问题也就是另一个经典的迭代器失效问题;

所以当我们按照上述代码的写法,只要有两个偶数同时在一起或者结尾是一个偶数的话,就会导致跳过一个偶数或者导致_finish(end)的位置减减到pos(it)位置的前面,这些情况都是迭代器失效的问题;


本质就是pos(it)_finish(end)不能匹配成功,会发生跳跃和穿越的问题

解决方法

上篇博客,我们知道,解决这种问题就是使用返回值就行了(目的就是为了让地址连续),所以此时解决这个删除函数中迭代器失效问题,最好的方式还是使用返回值,如下代码:


8.png


以上就是使用返回值的方式解决迭代器问题,所以以后碰到删除和插入,我们都应该要考虑一下是否涉及到迭代器失效的问题(前提我们使用的遍历方式是迭代器或者范围for),如果碰到这些问题,导致程序崩溃,此时我们也不要慌张,第一时间就应该要想到使用返回值的范式就可以很好的解决这个问题,迭代器实现问题,So,So啦!


总:只要我们使用了迭代器来遍历数据,那么就要考虑到迭代器失效的问题哦!


从模板再谈匿名对象


在之前学习类和对象的时候,我们了解了匿名对象的概念,知道它就是从 vector<int> v; v.vector();直接写成vector<int>().vector()的一个简单省略写法,并且此时如果写成模板参数的形式,就可以写成 T().push_back(),表示的就是:使用T(int)类型去创建一个匿名对象,然后使用这个对象去调用push_back(尾插函数),也就可以写成T(),表示的就是使用默认构造函数,生成一个该默认构造函数类型的匿名对象,并且最后为了更好的配合模板初始化问题的使用,C++的大佬,是允许这样写的:const T& x = T()使用给匿名对象起别名的方式,来延长匿名对象的生命周期(否匿名对象的生命周期只在该行代码,出了作用域就失效,所以以后看到这种写法,我们不要感到奇怪,当然更重要的是,在以后我们遇到有关模板类初始化的时候,我们也可以使用这样的写法。注意:一定要加上const,不可以写成T& x = T(),因为调用默认构造函数的时候,都是通过临时变量的形式,临时变量是具有常属性的,所以如果没有const,语法上就是有问题的。


如下就是使用匿名对象去构造我的模板类自我实现构造函数:


9.png


迭代器特性

从源码中的构造函数再看迭代器,如下图:

10.png


我们可以发现,vector类的构造函数中,有一个构造函数是支持传两个随机迭代器参数调用构造函数的(本质就是允许传两个指针(地址)给给构造函数中的随机迭代器函数),所以有了这个迭代器构造函数,那么此时调用构造函数的时候,就可以进行如下的调用:

10.png


可以发现,只要我们有了一个迭代器类型参数的构造函数,那么此时不仅可以对vector类型的数据调用构造函数、对string类型的数据调用构造函数,而且还可以对数组调用构造函数,所以总的来说,迭代器本上就是调用地址,只要我们传递的参数是地址类型(指针),并且迭代器类型符合(双向迭代器、单向迭代器、随机迭代器),那么就可以使用迭代器来访问我们的数据。


总:此时无论是迭代器失效问题还是迭代器使用问题,我们都进行了进一步的理解,所以迭代器的学习,我们先告一段落,不过在以后的链表和二叉树学习过程中,我们肯定会再和它打交道了,并且更加深入的了解它。


再谈深浅拷贝

搞定了迭代器这个重要的话题,此时我们就再来看一个重要的问题,深浅拷贝问题,再谈原因:当我们遇到了深拷贝之后,新空间的数据还是一个类(也就是还指向了一块空间,还需要进行深拷贝),此时就会因为,如果只进行一次深拷贝的话,那么导致旧空间中的数据指向的那块空间,会被两个不同空间(新空间、旧空间)中的数据类型(指针)一起存储,最后就会导致该空间被析构两次,导致程序崩溃。 有了这个问题,那么此时=我们就需要再进一步的了解一下深浅拷贝问题了!


第一个涉及这种场景:调用拷贝构造的时候

第二个涉及这种场景:扩容的时候


在谈这两个场景之前之前,我们浅浅复习一下之前的知识:this指针和赋值运算符重载


this指针

学习类和对象,this指针是非常的重要的,因为this指针是C++大佬专门发明出来在类和对象中使用的,目的就是为了可以让我们在调用类中的公有成员函数的时候,可以少传一个参数,默认this指针代表的就是该类对象(并且this指针可以省略),所以就导致我们可以不需要传参(类对象),就可以使用某个类对象,如下图:


11.png



我们可以发现,我们的函数中,只有三个参数,但是此时我们却可以使用该类的私有成员变量,原因就是,我们的类对象(d1),去调用了该公有成员函数(Init),所以此时编译器就默认该对象(d1)就是this指针,并且默认它的地址被传给了该公有成员函数(Init),并且默认这个地址参数是被this指针接收,所以就导致,在类中的公有成员函数都有一个默认的参数存在,就是this指针,有了这个默认的指针,此时就允许该类的任意对象直接来调用该类中的公有成员函数。

注意:此时小心空指针对象成为this指针的情况(容易导致空指针解引用问题)


赋值运算符重载

首先第一点,赋值运算符重载是一个默认成员函数,它是六大天选之子之一,你不写编译器会自己调用(无论是内置类型还是自定义类型(但是只是浅拷贝(如果涉及到深拷贝问题,就需要自己去实现一个深拷贝的拷贝构造函数(因为拷贝构造函数由于使用了赋值运算符,所以也可以直接对内置类型和自定义类型进行初始化)))),所以如果涉及到了深拷贝问题,那么你就需要自己去实现赋值运算符重载和一系列的构造函数、拷贝构造函数,(因为编译器会优先调用我们自己实现的函数,其次才是对自定义类型调用相应的默认成员函数)。


六大默认成员函数和自定义类型、内置类型的关系总的来说就是两句话:


默认构造函数和析构函数,对内置类型不处理,自定义类型调用其对应的函数;

拷贝构造函数和赋值运算符重载,不仅对自定义类型进行处理,对内置类型也会处理(但只是浅拷贝);


从拷贝构造看深浅拷贝:

复习完上述的知识,我们就可以开始新知识的学习了,问题如上描述(深拷贝出来的新空间中,还有深拷贝),如下图:

12.png


根据上图,我们就可以充分意识到,遇到类中类问题的解决方法和基本情形了,所以以后想要使用memcpy函数,就一定要考虑是否涉及深层次的深拷贝问题,如果有,就不可以使用memcpy函数,而是去循环调用赋值运算符重载(前提是该类具有赋值运算符重载)。


从扩容函数看深浅拷贝

搞定了上述拷贝构造函数中的深层次深拷贝问题,发现原因是因为memcpy只能进行浅拷贝的问题,此时就想到,我们的扩容函数中,也使用了memcpy函数,所以此时我们就应该要想到,扩容函数是否也会涉及深层次的拷贝问题,相信我,答案是会的,如图:


13.png


因为道理都差不多,这里不多做讲解

总:我们对奇怪知识的理解又多了一点点,深层次的深拷贝问题So、So!


vector较完整代码如下

#include<iostream>
#include<string>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<algorithm>//包含所有算法的头文件,例如:sort
#include<functional>//排降序的时候用到
using namespace std;
namespace wwx2
{
  template<class T>
  class my_vector
  {
  public://可以看出上述是把T给直接定义成了一个value_type,然后使用value_type定义了const指针和普通指针(所以本质上:都只是T类型而已)
    typedef T* iterator;//有普通版本的迭代器,此时无论是因为模仿STL还是真的会用到,我们都应该要给一个const类型的迭代器
    typedef const T* const_iterator;
    typedef T& value_type;
    typedef size_t size_type;
    my_vector()
      :_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)//初始化列表初始化
    {}
    my_vector(size_type n, const T& val = T()) //和下述的那个resize同理(使用T匿名对象调用默认构造函数,然后增长匿名对象的生命周期)
      : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
    {//所以无论这个类是什么类型,在初始化的时候,我们就是给给它的默认构造就行了
      reserve(n);
      for (size_type i = 0; i < n; ++i)
      {
        push_back(val);
      }
    }
    //此处可以添加一个上述构造函数的int重载版本(省略)
    //使用迭代器区间的一个构造函数(实现了这个函数,此时就会导致下面的那个调用构造函数初始化时,不知道调用那个了,因为此时有两个构造函数),就会导致类型不匹配问题,因为使用迭代器的构造函数,是要求传指针参数的,不可以是整形参数,所以需要改进一下(萝卜青菜区分开来)
    template <class InputIterator>//此时就是一个模板类中有模板函数的写法(本质上是因为迭代器的多样性,不可以把迭代器的类型给写死,要是可变的,所以就使用模板的方式)
    my_vector(InputIterator first, InputIterator last)//就是一个允许类模板的成员函数是一个函数模板
      : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
    {
      while (first != last)//迭代器区间:[first,last)
      {
        push_back(*first);
        ++first;
      }
    }
    //上面的两个构造函数对比,如果,下面传上来的参数是两个整形,那么此时因为第一个构造函数的第一个参数是一个无符号数(所以此时需要进行类型的转换)
    //而第二个构造函数的两个参数由于都是指针,所以就可以直接接收,不需要什么类型转换(所以第二个构造函数就是下述传参更爱吃的萝卜)
    //所以如果是两个整形使用了第二个构造函数,那么此时就会导致对整形就行解引用的情况发生,所以就有问题了
    //这个位置还可以加一个给拷贝构造使用的构造函数(如果没有这个拷贝构造函数,而是去调用构造函数的话,这边就会导致经典的浅拷贝问题)
    //my_vector(const my_vector<T>& v)//因为调用拷贝构造的时候,传参传上来的是一个vector的对象,所以这边的参数也要写成vector,不可以像上述一样写成别的类型
    //{//因为上述已经有了一个有缺省参数的构造函数,所以这里不需要再给(总的来说,还是拷贝构造的特性)
    //  reserve(v.capacity());//深拷贝,不然就会导致同一块空间析构两次
    //  for (auto e : v)
    //  {
    //    push_back(e);//这个是因为复用了reserve和push_back(不传统)
    //  }
    //}
    //my_vector(const my_vector<T>& v)//传统一些的深拷贝写法
    //{
    //  reserve(v.capacity());
    //  memcpy(_start, v._start, sizeof(T) * v.size());//这步需要仔细研究一下(本质还是那个道理,反正三指针问题一定要给它搞定)
    //  _finish = _start + v.size();//这边因为_finish是由数据个数决定的,所以新开辟的空间之中有多少个数据是会改变的,所以这边的_finish是需要自己更新一下的,不可以依靠reserve中的_finish(但是reserve中的容量是可以使用的,因为开辟空间的大小是不会改变的)
    //}
    my_vector(const my_vector<T>& v)//最传统的深拷贝写法
    {
      _start = new T[v.capacity()];
      //memcpy(_start, v._start, sizeof(T) * size());//所以为了解决深深拷贝的问题,此时就不可以使用memcpy函数,而是去循环调用赋值(因为赋值运算符本质上就是深拷贝实现的)
      for (size_type i = 0; i < v.size(); ++i)//调用赋值运算符,然后通过复制运算符再去调用拷贝构造
      { 
        _start[i] = v._start[i];//此时这样写,我们就解决了深拷贝中还要深拷贝的问题(本质:就是构造函数中嵌套构造函数)
      }//并且因为按照上述那样写,对内置类型并没有影响,因为复制对内置类型来说也是可以用的(赋值重载本来就是六大默认函数之一,编译器会自己调用)
      _finish = _start + v.size();
      _end_of_storage = _start + v.capacity();//reserve是会处理容量的,所以没有使用reserve的时候,就要自己把容量的大小给处理一下
    }
    ~my_vector()
    {
      delete[] _start;
      _start = _finish = _end_of_storage = nullptr;
    }
    iterator begin()
    {
      return _start;//通过临时变量返回,就是类型传值返回
    }
    iterator end()
    {
      return _finish;//同理
    }
    const_iterator begin()const
    {
      return _start;
    }
    const_iterator end()const
    {
      return _finish;
    }
    size_type capacity()const//像这种函数一定要记得加上我们的const
    {
      return _end_of_storage - _start;
    }
    size_type size()const
    {
      return _finish - _start;
    }
    void resize(size_type n, T val = T())//实现了reserve,此时resize复用就行,只是初始化的细节注意一下和size的大小注意一下就行 (所以此时我们就需要多给一个缺省参数,来进行默认的初始化新开辟的空间),就是防止你没有给第二个参数,只给了第一个参数,那此时我也可以把空间初始化
    {//上面那个缺省值的初始化是非常的神奇的(目的:因为此时的vector是一个泛型编程,是没有具体类型的,所以不可以用0来初始化,所以就要用一个模板参数,然后就是通过一个匿名对象的方式去调用默认构造函数)
      if (n < size())//这个不是缩容(这个属于删除数据,因为size是和数据紧紧挂钩的)
      {
        _finish = _start + n;//这个条件在尾插的时候,扩容,是不可能存在缩容删除数据的(唯一可能的就是我们直接调用这个函数,然后进行传参的时候,只有这种情况才有可能导致删除)
      }
      else
      {
        if (n > capacity()) //这个条件就是传说中的只有你比我小,我才扩容 
        {
          reserve(n);//开空间
        }
        while (_finish != _start + n)//加 初始化
        {
          *_finish = val;//此时已经有空间了,所以就不需要使用定位new的方法,直接赋值就行
          ++_finish;
        }
      }
    }
    value_type operator[](size_type pos)//并且此时返回的是一个引用,就是返回别名,目的:节省构造,防止临时变量的常属性
    {
      assert(pos < size());
      return _start[pos];//就是返回这个pos位置在_statr中的那个下标位置(因为此时是指针,所以准确的来说,应该是一个地址位置)
    }
    const value_type operator[](size_type pos)const//就是多重载一个const类型的,给给const对象使用(萝卜青菜给有所爱)
    {
      assert(pos < size());
      return _start[pos];
    }
    void reserve(size_type n)
    {
      //这边肯定是要涉及深浅拷贝问题的(并且为了防止缩容问题,这边还要进行判断)
      if (n > capacity())
      {
        //使用下面调换两个指针的顺序是可以解决的,但是不是很好,所以我们这边直接先把size()的值给接收起来就行
        size_type sz = size();//这样直接使用sz就行了,不需要再使用size(),也就是不需要再考虑finish和start的位置(随便它去变,跟我都没关系)
        iterator tmp = new T[n];//此时这里不需要考虑加1的问题(只有像string这种,需要存一个\0的这种,我们才需要多开一个空间给给它使用)
        if (_start != nullptr)//此时的new是不需要想malloc一样就行开辟成功和失败的判断的,这个只是为了单纯判断一下_start中是否有数据
        {
        //  memcpy(tmp, _start, sizeof(T) * size());//由于自定义类型不适用,所以改进一下
          for (size_type i = 0; i < sz; ++i)//深深拷贝的专业写法,间接多调用一次拷贝构造
          {
            tmp[i] = _start[i];
          }
          delete[] _start;//这个就是经典的防止空间太大太小问题,直接开空间,然后拷贝,然后直接把原空间给删除
        }
        //_start = tmp;//注意:因为此时是重新开空间,释放旧空间,所以此时的tmp就是我们的start
        //注意:此时下述的size()是不会因为finish和start的地址改变而改变的 (不会因为重复调用而改变),一直都是同一个值,也就导致可以直接使用tmp+size()
        //_finish = tmp + size(); //如果此时是先把tmp给给_start,然后再加加size(),此时就会导致finish还是0,而start已经是tmp,然后又因为size就是finish-start,就会导致size是一个负值,也就是-start,然后再用start+size,那么刚好就是0,最后赋值给finish,此时finish就还是0,所以就会导致后面push_back的时候,对空指针解引用的问题,所以此时为了解决这个问题,此时就不敢直接先给给start
        _start = tmp;//先给给finish,再给给start就行,很好的解决finish为空的问题
        _finish = tmp + sz;//提前记录size的好处
        _end_of_storage = tmp + n;//此时的这个是通过tmp指针的首地址,然后加上16个字节,就是首地址向后走走16个字节,得到的就是此时的容量
      }
    }
    void push_back(const T& x)
    {//因为使用的是模板类,所以这里的参数类型都是直接给一个T参数类型就可以了
      //从刚刚的STL源码中,我们可以发现的是,它的push_back使用的是内存池开空间的形式(就是定义new加定义构造)
      //我们这里使用不了,我们就直接使用正常的形式就行(如果想要使用的话,就要在定义模板参数的位置给一个内存池的参数)
      if (_finish == _end_of_storage)
      {
        reserve(capacity() == 0 ? 4 : capacity() * 2);//此时的reserve函数中使用的是赋值运算符重载而不是memcpy,可以放心大胆的使用
      }
      *_finish = x;//这步就是尾插的经典步骤,上述的判断只是为了防止空间不足而已(但是由于我们的_end是一个原生指针,所以这里想要直接在尾部赋值,就需要对这个尾部指针进行解引用)
      ++_finish;
    }
    void pop_back()
    {
      assert(!empty());//切记assert给的是真
      --_finish;//这种如果直接用,不检查的话,就会导致删多了的话有越界问题(并且因为此时是迭代器的写法,地址问题),就会导致循环找地址,然后就导致无限打印地址,直到地址匹配到
    }
    iterator insert(iterator pos, const T& val)
    {
      assert(pos >= _start);
      assert(pos <= _finish);//还是那个道理,assert中的条件是真条件
      //并且由于这边,我们使用了_finish这个位置,就会导致,如果容量刚好等于_finish的时候,越界,所以这个位置也要进行一个容量的检查
      if (_finish == _end_of_storage)
      {
        size_type len = pos - _start;//先记录(len的作用就是解决迭代器失效问题,目的:更新pos指针)
        reserve(capacity() == 0 ? 4 : capacity() * 2);
        pos = _start + len;//扩容后再更新(解决迭代器失效问题)
      }
      iterator end = _finish - 1;
      while (end >= pos)//此时因为pos的类型是地址,所以不存在-1变成无符号(size_t),所以不存在死循环,没有问题
      {
        *(end + 1) = *end;//此时的意思就是把最后一个数据赋值给0,然后把刚刚那个位置腾出来,然后循环
        --end;
      }
      *pos = val;
      ++_finish;
      return pos;
    }
    iterator erase(iterator pos)
    {
      assert(pos >= _start);
      assert(pos < _finish);
      iterator start = pos + 1;
      while (start != _finish)
      {
        *(start - 1) = *start;
        ++start;
      }
      --_finish;
      return pos;//这个位置不敢给成start,因为本质就是为了更新pos
    }
    bool empty()
    {
      return _start == _finish;//当_finish--到_start的时候,就是空,就是不允许的
    }
  private:
    iterator _start;//因为STL源码中是直接使用三个原生指针,所以这边我们模仿它,我们也直接使用三个原生指针就行
    iterator _finish;
    iterator _end_of_storage;
/*    iterator _start = nullptr;
    iterator _finish = nullptr;
    iterator _end_of_storage = nullptr;*///这样写就可以不需要在构造函数的初始化列表位置写初始化了,因为到时候他自己会调用这下面的缺省参数
  };
  void test_my_vector1()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    for (size_t i = 0; i < v.size(); ++i)
    {
      cout << v[i] << " ";
    }
    cout << endl;
    my_vector<int>::iterator it = v.begin();//所以本质,我发现,使用三指针的写法,好像就是为了可以更好的使用迭代器(因为迭代器的本质就是地址的加加减减,前提:地址要连续)
    while (it != v.end())//不连续就会导致迭代器失效问题
    {
      cout << *it << " ";
      ++it;
    }
    cout << endl;
    for (auto e : v)//有了迭代器,就有迭代器的小儿子范围for
    {
      cout << e << " ";
    }
    cout << endl;
  }
  void test_my_vector2()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    v.pop_back();
    v.pop_back();
    my_vector<int>::iterator it = v.begin();
    my_vector<int>::const_iterator cit = v.begin();//反正就是萝卜青菜给有所爱,数据类型是什么就调用什么,不怕没有,想要什么就有什么
    while (it != v.end())
    {
      cout << *it << " ";
      ++it;
    }
    cout << endl;
  }
  template<class T>
  void Function()
  { 
    T x = T();//这个就直接用一个int类型来辅助理解就行(int x = int();) 可以看出此时的int();就是我们之前学的匿名对象;其中int 空表示的就是匿名对象,而int()后面的括号其实表示的是,此时拿着这个int空,匿名对象去调用某一个函数,()括号中存放的本就应该是你要调用的这个函数需要的参数
    cout << x << endl;//总而言之:目的就是为了让任意类型都可以调用默认构造,然后任意类型都可以初始化
  }
  void test_my_vector3()
  {
    //int i = int();//这个的意思就是:使用int(),匿名对象,然后去调用构造函数,然后延长匿名对象的声明周期(把它给给新的对象)
    //int j = int(1);//支持这种写法,本质上还是为了支持模板可以使用(也就是上述resize的缺省参数的写法;很好的解决模板初始化问题)
    //int* p = int*();//指针类型不支持直接把匿名对象给给指针类型
    Function<int>();
    Function<int*>();
    Function<double>();
  }
  void test_my_vector4()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    cout << v.size() << endl;
    cout << v.capacity() << endl;
    v.resize(10);
    for (auto e : v)
    {
      cout << e << " ";
    }
    cout << endl;
  }
  void test_my_vector5()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);//切记push_back也是会调用扩容函数的
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);//此时就很神奇的会涉及到,insert是否扩容问题,如果数据是在push_back的过程,insert就不涉及扩容,如果,insert涉及扩容,那么此时就有迭代器失效的问题,本质就是地址不连续
    //v.insert(3, 30);//聪明的我,居然写成了这样,真的是函数都调不明白啊(但是也刚刚好,可以引出我们的find函数)
    //当然此时也不一定一定要使用find,因为此时可以直接使用迭代器(充分表明我的不聪明)
    v.insert(v.begin(), 0);
    v.insert(v.end(), 6);
    v.insert(v.begin() + 3, 30);//充分表明迭代器是真的好用(促进我们通过变量看地址的好习惯)
    auto pos = find(v.begin(), v.end(), 3);//此时就是把我的迭代器区间传给了find函数,这样就可以使用find函数在迭代器区间中寻找3这个元素
    if (pos != v.end())//这里这样写的目的:主要是为了防止越界
    {
      pos = v.insert(pos, 30);//像这种一直插一直插这种,就会导致一个问题(经典的迭代器失效问题),本质就是:地址不连续了
    }//并且此时如果上述这样写,那么pos在内存中的位置是不会改变的,但是由于insert因为内存不足开空间,导致start和finish的位置发生了改变,间接导致begin和end(迭代器)发生了改变,导致迭代器失效
    (*pos)++;//意思就是:拿更新后的pos地址中的元素加加一下(但是本质上是想要在,3的地方加加,最后会变成在30的地方加加),原因就是pos指向的那个地址是不变(但是地址中的数据在挪动),并且此时因为insert中的pos是个形参,所以不会影响我们外部的pos,所以此时最好使用返回值,直接把pos返回给我们
    //并且此时由于使用引用返回需要有临时变量(常属性),所以不推荐使用,所以这边,我们是通过返回值的方式解决这个问题的
    //总:pos失效后,我们都不推荐使用(*pos)++;所以不敢这样写
    for (auto e : v)//迭代器一失效(也就是begin和end的地址发生了改变),那么此时就会导致pos地址虽然不改变(但是它指向的空间发生了改变(从原来的begin和end的地址变成了一块未知的地址(因为begin和end已经因为扩容拥有了新地址,并且把原来的地址给释放了))),所以pos指向的地址就是一个未知地址,此时pos指针就是一个野指针
    {//总:开空间之后,导致pos指向的迭代器地址被释放,导致野指针问题(这也就是第一种经典的迭代器失效问题)
      cout << e << " ";//所以明白了原理之后,此时的解决方法就是:更新一下pos指针指向的空间(依据pos和start的相对位置不改变来更新)
    } 
    cout << endl;
  }
  void test_my_vector6()
  {
    wwx2::my_vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4); 
    v.push_back(5);
    auto pos = find(v.begin(), v.end(), 2);
    if (pos != v.end())
    {
      v.erase(pos);
    }
    (*pos)++;//此时这个不是解引用的意思,可以算是对*这个运算符重载的意思(本质上*的运算符重载就是在获取一下我想要的功能或者是数据)
    for (auto e : v)//并且此时这种删除之后再访问的方式是很不好的,因为如果按照erase代码来说,删除最后一个及时把finish的位置往前挪动一个,此时如果再去访问刚刚4的pos位置,就会导致对一个空地址解引用,此时就出问题了
    {               //所以总的来说:删除数据后,是不允许进行数据访问的(并且此时数据删除也是伴随着迭代器失效的问题的),因为位置关系发生了改变,也就是导致地址不连续(迭代器失效的本质)
      cout << e << " ";
    }
    cout << endl;
  }
  void test_my_vector7()
  {
    wwx2::my_vector<int> v;
    v.push_back(10);
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);
    v.push_back(50);
    for (auto e : v)
    {
      cout << e << " ";
    }
    cout << endl;
    //此时要求删除所有的偶数
    //std::vector<int>::iterator it = v.begin();
    wwx2::my_vector<int>::iterator it = v.begin();
    while (it != v.end())
    {
      if (*it % 2 == 0)
      {
        it = v.erase(it);//此时因为我们已经通过返回值可以直接拿到it的位置了,所以就不需要每次都++,因为数据覆盖之后,我的it就已经是被覆盖成了新的数据,此时只要再判断一下这个数据是不是偶数就行
      }
      else//因为可以连续拿到it位置的数据,所以it++只有在不是奇数的时候需要使用
      {
        ++it;
      }
    }
    for (auto e : v)
    {
      cout << e << " ";
    }
    cout << endl;
  }
  void test_my_vector8()
  {
    //wwx2::my_vector<int> v(10, 5);
    //wwx2::my_vector<int> v(10u, 5);//解决方案一:在整形后面加个u,表示无符号的意思,本质就是让它去找那个无符号的参数匹配
    wwx2::my_vector<int> v(10u, 5);//解决方案二:就是把上面那个函数提供一个int类型的重载函数(这个似乎有点傻傻的样子)
    //迭代器构造函数的正确使用方法:
    wwx2::my_vector<int> v2(v.begin(), v.end());
    //wwx2::my_vector<int> v(++v.begin(), --v.end());//此时这个一开始报错的原因(也就是不让我加加减减的原因)就是:涉及临时变量(常属性问题)
        //因为我们一开始的时候写的begin和end函数使用的是传值返回(解决方法就是:使用传引用返回,但是没什么必要(避免风险,导致first和last被间接改变))
    wwx2::my_vector<int> v3(v.begin() + 1, v.end() - 1);//避免临时变量常属性问题 ,先返回,返回之后再加加减减
    //因为我们的构造函数是使用随机迭代器实现的,所以是可以接收任何类型的迭代器的,例如:
    std::string s1("hello world");
    wwx2::my_vector<int> v4(s1.begin(), s1.end());//所以传什么类型的迭代器都是没什么问题的(迭代器就是牛),只要类型匹配就行(可以强转)
    //或者可以这样玩(本质就是在玩地址)
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };//随机迭代器,爱咋玩咋玩
    wwx2::my_vector<int> v5(arr, arr + 10);
    for (auto e : v)
    {
      cout << e << " ";
    }
    cout << endl;
    for (auto e : v2)
    {
      cout << e << " ";
    }
    cout << endl;
    for (auto e : v3)
    {
      cout << e << " ";
    }
    cout << endl;
    for (auto e : v4)
    {
      cout << e << " ";
    }
    cout << endl;
    for (auto e : v5)
    {
      cout << e << " ";
    }
    cout << endl;
  }
  void test_my_vector9()
  {
    int arr[] = { 0,10,4,3,2,6,9,8,1,7,5 };
    my_vector<int> v(arr, arr + 10);
    for (auto e : v)
    {
      cout << e << " ";
    }
    cout << endl;
    cout << "排序之后的情况:" << endl;
    sort(v.begin(), v.end());//总的来说:就是算法库里面存在着一个迭代器形式的sort函数(反正迭代器就是神),本质:指针(地址)yyds
    for (auto e : v)
    {
      cout << e << " ";
    }
    cout << endl;
    sort(arr, arr + sizeof(arr) / sizeof(arr[0]));//总归有了迭代器,函数变得更加的好用了,更加方便了(可爱!),并且此时是 默认是升序(降序需要控制一下)
    for (auto e : v)
    {
      cout << e << " ";
    }
    //排降序的写法
    cout << "降序:" << endl;
    //greater<int> g;//这个东西叫仿函数(具体以后学习)
    //sort(arr, arr + sizeof(arr) / sizeof(arr[0]),g);//想要排降序,就需要多传一个参数,具体原理不了解,先看看怎么使用就行
    //这种写法,很适合直接写成匿名对象的形式
    sort(arr, arr + sizeof(arr) / sizeof(arr[0]), greater<int>());
    for (auto e : v)
    {
      cout << e << " ";
    }
  }
  //此时这边有一个很重要的知识点:我们使用sort,排序的是我们自己的容器(vector),原理是因为我们包含了头文件,去调用了别的头文件中的函数,这里好像埋了一个炸弹没有解决
  void test_my_vector10()
  {
    my_vector<int> v(10u, 5);//调用自己实现的构造函数
    for (auto e : v)
    {
      cout << e << " ";
    }
    cout << endl;
    my_vector<int> v1(v);//调用拷贝构造(本质还是在调用构造函数,只是此时是通过this指针,用v这个已经初始化的对象去初始化v1这个对象而已),注意和赋值的区别,区别就是赋值是两个都已经实例化的对象,而拷贝构造是一个已经实例化,一个还没有
    for (auto e : v1)
    {
      cout << e << " ";
    }
    cout << endl;
    my_vector<std::string> v2(3, "1111111111111111");
    for (auto e : v2)
    {
      cout << e << " ";
    }
    cout << endl;
    my_vector<std::string> v3(v2);//这句代码的意思就是使用一个string类的vector模板类来拷贝构造另一个新对象(v2)
    //此时因为使用v2去初始化v3的时候回去调用reserve函数(防止空间太大太小问题),都是直接开空间,然后memcpy,最后delete原空间
    //这样写的好处有两点:一个是解决深浅拷贝问题(前提是内置类型),一个就是解决空间浪费和不足问题
    //但是此时如上述一样,我们不再是内置类型,而是自定义类型,并且此时这个自定义类型中也涉及到了深浅拷贝问题(就是也有一块空间)
    //那么此时就是变成是深深拷贝问题,需要进行两次的开辟空间
    //所以如上述我们这样写,只调用了一次reserve(new)函数,那么就会导致, 原本应该有两次的深拷贝变成了一次,就又会导致同一块空间被析构两次的问题了
    for (auto e : v3)
    {
      cout << e << " ";
    }
    cout << endl;
    //所以此时我们按照这个写法,我们就可以发现,我们的程序因为同一块空间释放了两次而导致崩溃了
    //本质的原因:就是我们使用了memcpy,它只会进行浅拷贝,把原空间中的地址拷贝到新空间中,不会进行深拷贝
    //所以解决的方法就是不能使用memcpy,而是自己再去实现一个深拷贝出来(因为memcpy是库里面规定的写法(不会进行深拷贝))
    //所以memcpy只有在进行内置类型深拷贝的时候可以使用,而在拷贝自定义类型的时候,最好就不使用memcpy函数
    //解决原理是上述所说的那样,但是因为我们已经实现了一个深拷贝了,模板类中的自定义类型的数据的深拷贝问题,我们是摸不到的,所以,此时我们并不能自己去实现深拷贝,(例:string类中的私有对象我们是访问不了的)
    //此时根据这个拷贝构造的问题,此时我们就还可以发现,我们在扩容的时候,复制数据的时候,使用的也是memcpy函数
    //所以扩容(使用memcpy)对于普通的内置类型处理是没有什么问题的,但是如果是自定义类型,那么此时就连扩容本质上也是有问题的(因为扩容也是需要把旧空间中的数据拷贝到新空间的)
    // 并且由于扩容的时候,会把原来的空间释放掉(那么就会导致你的空间中所有的自定义类型上的指针类型,都是野指针)
    //所以同理,无论是扩容还是构造函数,只要需要对自定义类型进行拷贝,那么此时就不仅要涉及到外部空间的深拷贝,还要注意到空间内的数据的深拷贝问题
    //所以解决方法和拷贝构造函数是一样的,就是使用赋值运算符重载,然后通过它间接的再去调用一次拷贝构造(例:string类中的私有对象我们是访问不了的)
    //但是此时由于不是所有的类都会去重载赋值,string有重载赋值,但是vector类却没有重载赋值,所以此时导致vector类中类此时就算是使用了赋值运算符重载
    //也不可以进行深深拷贝问题,还是解决不了问题,还是一个深层次的浅拷贝问题,所以还是会崩溃(string可以,但vector类不可以)
    //本质上还是没有两次的深拷贝构造函数的实现(没有重载赋值运算符)
    //并且此时我们是要注意到,我们的构造函数是可以使用现代化的写法的
  }
}
int main()
{
  wwx::vector<int>::iterator it;
  //cout << typeid(it).name() << endl;//typeid函数就是为了获得这个变量的类型是什么而已
  wwx2::test_my_vector1();//测试尾插
  wwx2::test_my_vector2();//测试尾删
  wwx2::test_my_vector3();//测试匿名对象
  wwx2::test_my_vector4();//测试resize
  wwx2::test_my_vector5();//测试insert和erase(本质:扩容导致迭代器失效问题)
  wwx2::test_my_vector6();//测试插入函数中迭代器失效
  wwx2::test_my_vector7();//测试编译器对erase的控制(std是直接报错(只要删除了数据,就是报错))
    wwx2::test_my_vector8();//测试构造函数
    wwx2::test_my_vector9();//测试随机迭代器
  wwx2::test_my_vector10();//测试深浅拷贝问题
  return 0;
}
代码注释都比较的详细

image.png


总结:星期五就该发的文章留到了今天,充分表明,摆烂,我也是专业的。

相关文章
|
4天前
|
设计模式 算法 Java
【c++】STL之stack和queue详解
【c++】STL之stack和queue详解
6 1
|
10天前
|
存储 算法 C++
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
17 0
|
12天前
|
存储 算法 数据处理
【C++】STL简介
**STL是C++标准库的关键部分,源于Alexander Stepanov的泛型编程研究。它提供了数据结构(如vector、list)和算法,是高效、通用的软件框架。STL始于惠普,后由SGI发展,现已成为C++1998标准的一部分并不断进化。它包括容器、迭代器、算法、仿函数、配接器和分配器六大组件,带来高效性、通用性和可扩展性,但也存在性能开销和学习难度。学习STL涉及理解底层数据结构、用法、实现和实践。推荐[cplusplus.com](https://cplusplus.com)作为学习资源。**
|
12天前
|
存储 算法 程序员
C++基础知识(八:STL标准库(Vectors和list))
C++ STL (Standard Template Library标准模板库) 是通用类模板和算法的集合,它提供给程序员一些标准的数据结构的实现如 queues(队列), lists(链表), 和 stacks(栈)等. STL容器的提供是为了让开发者可以更高效率的去开发,同时我们应该也需要知道他们的底层实现,这样在出现错误的时候我们才知道一些原因,才可以更好的去解决问题。
|
12天前
|
算法 前端开发 C++
C++基础知识(八:STL标准库 deque )
deque在C++的STL(Standard Template Library)中是一个非常强大的容器,它的全称是“Double-Ended Queue”,即双端队列。deque结合了数组和链表的优点,提供了在两端进行高效插入和删除操作的能力,同时保持了随机访问的特性。
|
12天前
|
存储 C++ 索引
C++基础知识(八:STL标准库 Map和multimap )
C++ 标准模板库(STL)中的 map 容器是一种非常有用的关联容器,用于存储键值对(key-value pairs)。在 map 中,每个元素都由一个键和一个值组成,其中键是唯一的,而值则可以重复。
|
12天前
|
存储 算法 数据处理
|
14天前
|
存储 算法 C语言
【C++】详解STL的适配器容器之一:优先级队列 priority_queue
【C++】详解STL的适配器容器之一:优先级队列 priority_queue
|
14天前
|
设计模式 存储 缓存
【C++】详解STL容器之一的deque和适配器stack,queue
【C++】详解STL容器之一的deque和适配器stack,queue
|
14天前
|
存储 算法 C++
【C++】详解STL容器之一的 vector
【C++】详解STL容器之一的 vector