【C++】vector的模拟实现 (上)

简介: 【C++】vector的模拟实现(上)

👉前言👈


我们接下来要模拟实现的 vector,主要参照源码来写。但也不是完全照抄源码,主要是实现 vector 核心的函数接口,知道 vector 的底层实现逻辑。


👉如何看源码👈


源码并不是每一行都要看懂,而是抓住源码的核心。那对于一个类,我们需要关注的是这个类的成员变量和核心的成员函数。而 vector 的成员函数我们不用太关心,因为 STL 要实现哪些函数接口是有规定的。


以下为 vector 源码的成员变量。

78200a4d0d954e459376a9bf0f1a2085.png

注:源码中的 vector 的成员变量都是迭代器,而 vector 的迭代器是指针。而我们之前写的 string 和顺序表都是T* a; size_t _size; size_t _capacity,那为什么源码要这么定义成员变量呢?见下图:

ae632ed306824bcd9a5b555efe4a1946.png

其实这两种定义成员变量的方式本质上是一致的,只是换了种玩法而已。


👉vector 的模拟实现👈


vector 的主要框架


namespace Joy
{
  template<class T>
  class  vector
  {
  public:
    typedef T* iterator;
    typedef const T* const_iterator;
  private:
    iterator _start;
    iterator _finish;
    iterator _endofstorage;
  };
}


为了避免跟库里的 vector 冲突了,我们用命名空间将我们自己实现的 vector 封装起来。


无参的拷贝构造


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


注:_start 指向第一个位置,_finish 指向最后一个元素的下一个位置,_endofstorage 指向最大容量的下一个位置。


正向迭代器


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


size 和 capacity


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

4d201913cc3e4a8191787a62c2302982.png


指针相减为两个指针之间的元素个数,所以_finish - _startsize_endofstorage - _startcapacity。当_start == _finish时,size 为0。


reserve 和 resize


void reserve(size_t n)
{
  if (n > capacity())
  {
    size_t oldSize = size();
    T* tmp = new T[n];
    // _start为nullptr时,不需要拷贝数据
    if (_start)
    {
      memcpy(tmp, _start, sizeof(T) * oldSize);
      delete[] _start;
    }
    _start = tmp;
    _finish = tmp + oldSize;
    _endofstorage = tmp + n;
  }
}
void resize(size_t n, T x = T())
{
  if (n > capacity())
  {
    reserve(n);
  }
  if (n > size())
  {
    while (_finish < _start + n)
    {
      *_finish = x;
      ++_finish;
    }
  }
  else
  {
    _finish = _start + n;
  }
}


reserve 函数接口说明


注:使用 memcpy 函数来拷贝数据会带来一些问题,我们在后面的内容会讲解。


如果没有oldSize来记录原来 size 的大小,就需要考虑成员变量的更新顺序了。见下方代码:


// 错误的更新方式:先更新_start
_start = tmp;
_finish = tmp + size(); 
_endofstorage = tmp + n;
// 正确的更新方式:先更新_finish
_finish = tmp + size(); 
_start = tmp;
_endofstorage = tmp + n;


如果先更新_start的话,那么就无法通过size()来得到原来的size了,从而出现 BUG。所以我们可以用oldSize来记录size的大小,那么这样就没有更新成员变量的先后顺序问题了。当_start为nullstr时,,说明 vector 对象中没有数据,不需要拷贝数据;反之则通过 memcpy 函数来拷贝数据。最后更新相关成员变量的大小。


resize 函数接口说明


3bd1345479624e48a83c9fde0fd24ae6.png


只有当n > capacity()时,才会去调整capacity的大小(采取不缩容的原则)。如果n > size()的话,就利用 while 循环插入x,这个过程_finish也刚好更新了。如果n <= size()的话,就只需要修改_finish为_start + n。


push_back 和 pop_back


void push_back(const T& x)
{
  if (_finish == _endofstorage)
  {
    int newCapacity = capacity() == 0 ? 4 : capacity() * 2;
    reserve(newCapacity);
  }
  *_finish = x;
  ++_finish;
}
void pop_back()
{
  assert(!empty());
  --_finish;
}


push_back 函数接口说明


首先需要判断是否需要扩容,当_finish == _endofstorage时,容量已满,需要扩容。扩容之后,就插入数据,更新_finish


pop_back 函数接口说明


先断言 vector 类对象是否为空,如果为空,就直接报错;如果不为空,就删除数据。


[ ] 运算符重载


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


函数接口功能测试


void vectorTest1()
{
  vector<int> v;
  v.push_back(1);
  v.push_back(2);
  v.push_back(3);
  v.push_back(4);
  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;
  v.pop_back();
  v.pop_back();
  v.pop_back();
  for (auto e : v)
  {
    cout << e << " ";
  }
  cout << endl;
}
void vectorTest2()
{
  vector<int> v;
  v.resize(10, -1);
  for (auto e : v)
  {
    cout << e << " ";
  }
  cout << endl;
  v.resize(5);
  for (auto e : v)
  {
    cout << e << " ";
  }
  cout << endl;
}

02e3a5d9f26b4b4a825258cda3b6c331.png


84d6025cf5a5400bb7036879b3590d1d.png


insert 和 erase


// 迭代器失效:野指针问题
iterator insert(iterator pos, const T& x)
{
  assert(pos >= _start);
  assert(pos < _finish);
  if (_finish == _endofstorage)
  {
    // 保存pos和_start的相对位置,避免迭代器失效问题
    size_t len = pos - _start;
    size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
    reserve(newCapacity);
    // 扩容会导致pos迭代器失效,需要更新处理一下
    pos = _start + len;
  }
  // 挪动数据
  iterator end = _finish - 1;
  while (end >= pos)
  {
    *(end + 1) = *end;
    --end;
  }
  *pos = x;
  ++_finish;
  return pos;
}
// STL规定erase返回删除位置下一个位置迭代器
iterator erase(iterator pos)
{
  assert(!empty());
  assert(pos >= _start);
  assert(pos < _finish);
  iterator begin = pos + 1;
  while (begin < _finish)
  {
    *(begin - 1) = *begin;
    ++begin;
  }
  --_finish;
  return pos;
}


insert 函数接口说明


注:insert 的返回值为新插入元素的位置。


首先要判断pos位置是否合法,然后插入数据。插入数据时,需要判断容量是否已满,是否需要扩容。如果需要扩容的话,则需要将pos和_start的相对距离保存为len。如果保存它们的相对距离的话,扩容将会造成迭代器失效,从而导致野指针问题。那为什么扩容就会带来迭代器失效的问题呢?因为扩容有可能是异地扩容的,异地扩容的话,那么pos就不会在_start和_finish之间。


0a194990dfb8464d8efed9a31eb73972.png

所以,保存了pos和_start的相对距离,就可以在扩容后更新pos的值了,从而避免迭代器失效造成野指针问题。解决这个问题后,就移动数据,插入数据,更新_finish就行了。


3d5bd052e4214c68ae908a21fd14229f.png


现在,我们就写好了 insert 函数接口。那我想问大家一个问题,在我们插入数组后,find 函数返回的迭代器是否还可以再用,也就是说是否还可以对 it 进行读写操作。不能,因为也有可能带来野指针问题。那为什么会这样呢?因为插入数据时会发生异地扩容,然后迭代器 it 就会失效。有小伙伴就会说了,我们刚才不是已经解决了这个问题了吗。其实,我们只是解决了 insert 函数内部的迭代器失效问题,并未解决外部的迭代器失效问题,也就是说 it 不在_start和_finish之间了。


e8912ee83049460ba652b1f015fe1e33.png


通过上图就可以清楚地看到,it 已经不在_start和_finish之间。这时候,对 it 进行读写操作就是纯纯的野指针问题。


erase 函数接口说明


进行相关的断言,挪动数据,更新_finish


96fa9b3df66541dd97e0746d3a70914d.png

6c90eb25178b45dd8e09630c01c5a48e.png


那我再想问大家一个问题,这里的迭代器 it 会不会失效呢?我们先来看一下 VS 对这一问题的判定。

84dbed87154a468bbb8f2580c66d2f1b.png


可以看到,程序没有报错直接就崩掉了。所以 VS 认为这样是不行的。那么我们再去 Linux 下跑一下这段代码。


f496f9a284af473484bf3ea1d6ef95f7.png


可以发现,这段代码是可以在 Linux 下跑过的。那我们再来试一下写能不能跑过。可以发现,写也没什么问题。


bf96ace9ddae49c3ab8d789e83419641.png


注:不同的编译器跑的结果一般不一样。


那迭代器 it 究竟是失效还是不生效呢?其实,我个人认为是失效的。


2b8f958367ac4b48879fe095a57dc487.png

如果删除最后一个位置的元素,那么 it 就是v.end()。其实如果我们将要删除的元素改成5,后,代码在 Linux 下也能跑过。为什么呢?因为我们现在模拟实现的 vector 就是 Linux 下的 vector 源码。删除数据就只是指针减减一下,而 it 此时指向的位置不再是有效数据的位置,但是操作系统无法做到对每个位置的越界访问都进行检查,所以上面的代码可以跑过。


那我们再来验证一下,我们自己实现的 vector 是不是也可以在 VS 下跑过。

a54630798f0d4c1683d9d36b7097894f.png


是不是可以跑过啊!!!那为什么用 VS 下的 vector 就不能跑过呢?原因就是 VS 的 vector 原码的迭代器并不是原生指针,而是函数调用(以后会讲解)。所以,我们应该认为 it 会失效,不要对其进行读写操作。









相关文章
|
4月前
|
存储 编译器 C++
【C++】vector介绍+模拟实现
【C++】vector介绍+模拟实现
|
2月前
|
存储 C++ 索引
【C++打怪之路Lv9】-- vector
【C++打怪之路Lv9】-- vector
23 1
|
2月前
|
安全 测试技术 C++
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化2
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化
65 6
|
2月前
|
安全 测试技术 C++
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化1
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化
66 7
|
2月前
|
编译器 C++
【C++】—— vector模拟实现
【C++】—— vector模拟实现
|
2月前
|
编译器 C语言 C++
【C++篇】解密 STL 动态之魂:全面掌握 C++ vector 的高效与优雅
【C++篇】解密 STL 动态之魂:全面掌握 C++ vector 的高效与优雅
51 3
|
2月前
|
C++
【C++】C++ STL探索:Vector使用与背后底层逻辑(三)
【C++】C++ STL探索:Vector使用与背后底层逻辑
|
2月前
|
编译器 Linux C++
【C++】C++ STL探索:Vector使用与背后底层逻辑(二)
【C++】C++ STL探索:Vector使用与背后底层逻辑
|
2月前
|
编译器 C++ 容器
【C++】C++ STL探索:Vector使用与背后底层逻辑(一)
【C++】C++ STL探索:Vector使用与背后底层逻辑
|
2月前
|
算法 C++ 容器
C++之打造my vector篇(下)
C++之打造my vector篇(下)
29 0