C++入门第七篇--STL模板--vector模拟实现

简介: C++入门第七篇--STL模板--vector模拟实现

前言:

有了前面的string库的介绍,在这里我就不再介绍vector库了,而是直接模拟实现了。

vector库的概念和作用:

vector库是针对于数组的数据类型的容器,它有点类似我们曾经实现过的顺序表,你完全可以按照顺序表去理解vector,针对顺序表,我们自然少不了增删查改的功能,所以接下来让我们模拟实现一下vector库。

模拟实现过程:

1.私有成员变量的设置:

在这里,我们这样设置我们的私有成员变量,由于文档中C/C++库的函数大部分是用迭代器实现的,故我们模拟的时候也使用迭代器去操作,故成员如下:

private:
  iterator _start;
  iterator _finish;
  iterator _endofstorage;

其中_start指向顺序表开头,_finish指向顺序表的数字的结尾,而_endofstorage则控制容量,指向容量的结尾

但是我们C++中是没有所谓的iterator的,但是我们知道iterator的本质是指针,故我们对类型重命名如下:

typedef T* iterator;
typedef const T* const_iterator;

2.构造函数 析构函数 拷贝构造函数:

私有成员建立好后,我们下一个便是构建基本的三个函数:构造,析构,拷贝构造。

首先是构造函数:

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

这里就是常规的全让指针为空,因为我们会在调整容量的位置去为这三个成员变量赋值

析构函数:

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

这里需要注意的点就是:我们是需要在堆区上动态开辟空间的,故我们的析构函数是必须显式实例化的,要让析构函数释放掉我们的堆区空间。

拷贝构造函数:

vector(const vector<T>& s)
  :_start(nullptr)
  , _finish(nullptr)
  , _endofstorage(nullptr)
{
  for (auto& ch : s)
  {
    push_back(ch);
  }
}

再拷贝构造这里,我使用了遍历尾插的方式,或许你会说,直接memcpy不是更好么,但是我们的顺序表不仅仅要存储内置类型,有时也要存储自定义类型,而memcpy对应的是一种浅拷贝,一旦涉及到指针的问题,就会有多次释放的危险,故我们在这里采取尾插的方式,即自定义类型会调用其赋值运算符重载,内置类型则直接赋值,这样就很好的避免了多次释放的问题。

3.赋值运算符重载:

void swap( vector<T>& tmp)
{
  std::swap(_start, tmp._start);
  std::swap(_finish, tmp._finish);
  std::swap(_endofstorage, tmp._endofstorage);
}
vector<T>& operator=( vector<T> tmp)
{
  swap(tmp);
  return *this;
}

我在这里采取的现代写法,和string一样,采用一个变量tmp来打工的方式将值转给*this,大体的写法概念不变,我这里不过多赘述了。

4.size长度 capacity容量:

size用来返回顺序表的长度,而capacity用来返回顺序表的容量

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

5.首尾迭代器返回:

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

在这里我们写了两个版本,一个是可读可写版本,一个是可读不可写版本,分别返回不同的迭代器

6.下标访问操作符[]重载:

其基本和我们字符串的写法区别不大:

T& operator[](size_t pos)
{
  assert(pos < size());
  return _start[pos];
}
const T& operator[](size_t pos) const
{
  assert(pos < size());
  return _start[pos];
}

同样也是两个版本,可改与不可改

7.扩容!!!(本次实现的重点!)

扩容,是我们本次实现的重点,在这里牵扯到一个很关键的问题:迭代器失效。

何为迭代器失效呢?

先看下面的代码:

void reserve(size_t n)
{
  if (n > capacity())
  {
    T* tmp = new T[n];
    if (_start)
    {
      int i = 0;
      for (i = 0; i < size(); i++)
      {
        tmp[i] = _start[i];
      }
      delete _start;
    }
    _start = tmp;
    _finish = _start + size();
    _endofstorage = _start + capacirty();
  }
}

这段代码就涉及到严重的迭代器失效的问题,问题出在我们的delete被销毁后,我们对应的size()和capacity()本质上指向的是之前的被销毁的数组的地址,这样我们使用函数是得不到正确的长度的,因为迭代器此时的地址是无效的,这便是我们所谓的迭代器失效,你可以看这张图理解:

所以,我们可以这样去修改程序:

void reserve(size_t n)
{
  size_t sz = size();
  if (n > capacity())
  {
    T* tmp = new T[n];
    if (_start)
    {
      int i = 0;
      for (i = 0; i < size(); i++)
      {
        tmp[i] = _start[i];
      }
      delete _start;
    }
    _start = tmp;
    _finish = _start + sz;
    _endofstorage = _start + n;
  }
}

即先用一个变量将长度存储起来,而不是再用失效的迭代器返回长度和容量,在这里就是sz来存储,这样,我们就不会出现我们的长度是错误的问题了。

7.改变数组长度:

void resize(size_t n, const T& x = T())//改变数组长度
  {              //注意,内置类型是可以调用构造函数的,在模板这个章节是支持的,在这里别忘了加一个const,因为我们的缺省值是常量,不加const引用权限会放大
    if (n <= capacity())
    {
      _finish = _start + n;
    }
    else
    {
      reserve(n);
      //剩下来填数据:
      while (_finish < _start + n)
      {
        *_finish = x;
        _finish++;
      }
    }
  }

解决了扩容问题,我们其他的都很好解决了,这里也是一样,我们考虑两种情况即可,但是注意依旧用赋值不要用memcpy,因为涉及到浅拷贝的问题。

8.尾插 尾删:

尾插:

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

尾删:

void push_pop()
{
  _finish--;
}

没什么好说的,顺手就应该写出来。

9.任意插 任意删:

任意删:

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

任意删的思路和我们顺序表的任意删差不多,直接覆盖即可,强调一下别忘了对我们传入的迭代器进行检验就好。

但是任意删有一个细节就是,我们会涉及到迭代器失效的问题,即这个位置被删除后再想针对这个位置删除就是出现问题,所以我们返回pos,即删除后的下一个位置的指针,这样就可以一直删,不会删除一次就失效了。

任意插:

void insert(iterator pos, const T& x)//任意插
{
  assert(pos >= _start);
  assert(pos <= _finish);//这里可以等于,方便尾插
  if (_finish == _endofstorage)
  {
    size_t range = pos - _start;//在这里先存储一个长度变量方便后续迭代器失效时重新指定位置
    reserve(capacity() == 0 ? 4 : 2 * capacity());
    pos = _start + range;//由于扩容之后pos失效,那样的话pos不在新数组上,故我们要存储一个整型,方便扩容之后把pos重新带到新数组上来,别忘了任意插也存在扩容后迭代器失效的问题,我们的pos也会停留在之前的数组上被销毁之后就丢失了,要重新给到新的数组上
  }
  iterator end = _finish - 1;
  while (end >= pos)//这里要等于,保证其能在pos的位置之前插而不是正好插入pos位置
  {
    *(end + 1) = *end;
    end--;
  }
  *pos = x;
  _finish++;
}

在任意插这里,我们需要注意一个扩容的问题,凡是涉及到扩容和删除的问题,当我们使用迭代器去操作的时候,就要最好看一看是否涉及到迭代器失效的问题,在任意删这里就涉及到了,po针对的是被删除的数组的地址,但我们扩容后,pos的原位置直接失效了,故我们需要在扩容后调整pos到新数组的对应位置上,即:

if (_finish == _endofstorage)
  {
    size_t range = pos - _start;//在这里先存储一个长度变量方便后续迭代器失效时重新指定位置
    reserve(capacity() == 0 ? 4 : 2 * capacity());
    pos = _start + range;//由于扩容之后pos失效,那样的话pos不在新数组上,故我们要存储一个整型,方便扩容之后把pos重新带到新数组上来,别忘了任意插也存在扩容后迭代器失效的问题,我们的pos也会停留在之前的数组上被销毁之后就丢失了,要重新给到新的数组上
  }

然后一个常规的插入pos即可,这里最关键的便是针对迭代器失效我们应该如何处理。

总结:

以上便是我们vector模拟实现的全部内容,和string一样,我们模拟实现vector最关键的目的是学会一些思路,以及熟练的去使用vector,这是最关键的。
补充一句,WBG加油!!!!

目录
相关文章
|
10天前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
12天前
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
1天前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
9 1
|
12天前
|
编译器 C++
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
|
1月前
|
C++ 容器
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
60 21
|
10天前
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
10天前
|
存储 安全 算法
深入理解C++模板编程:从基础到进阶
在C++编程中,模板是实现泛型编程的关键工具。模板使得代码能够适用于不同的数据类型,极大地提升了代码复用性、灵活性和可维护性。本文将深入探讨模板编程的基础知识,包括函数模板和类模板的定义、使用、以及它们的实例化和匹配规则。
|
10天前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
12天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
10天前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。