【C++】vector的模拟实现

简介: vector的模拟实现

一.vector介绍

vector是表示可变大小数组的序列容器

就像数组一样,vector也采用连续的存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。

二. vector的模拟实现

1. 基本框架

vector是个类模板,这样它就可以存储不同数据类型的变量,其迭代器是数据类型的指针。

template<class T>
class vector
{
public:
  typedef T* iterator;            //普通迭代器
  typedef const T* const_iterator;//const迭代器 
private:
  iterator _first;         //指向第一个有效数据的指针
  iterator _finish;        //指向有效数据的尾(尾不存储有效数据)
  iterator _endofstorage;  //指向存储容量的尾(尾是未开辟的空间)
};

301348a19c2342dab9638ca06325f00b.png

2. 迭代器相关接口

2.1 begin和cbegin

begin


原型:iterator begin() noexcept;

作用:返回一个指向第一个数据的普通迭代器


cbegin


原型:const_iterator cbegin() const noexcept;

作用:返回一个指向第一个数据的const迭代器


template<class T>
class vector
{
public:
  typedef T* iterator;            
  typedef const T* const_iterator;
  // begin
  iterator begin()
  {
  return _first;
  }
  // cbegin
  const_iterator cbegin()const
  {
  return _first;
  }
private:
  iterator _first;         
  iterator _finish;        
  iterator _endofstorage;  
};


2.2 end和cend

end


原型:iterator end();

作用:返回一个指向最后一个数据的下一个位置的普通迭代器


cend


原型:const_iterator cend() const noexcept;

作用:返回一个指向最后一个数据的下一个位置的const迭代器


template<class T>
class vector
{
public:
  typedef T* iterator;            
  typedef const T* const_iterator;
  //end
  iterator end()
  {
  return _finish;
  }
  //cend
  const_iterator cend()const
  {
  return _finish;
  }
private:
  iterator _first;         
  iterator _finish;        
  iterator _endofstorage;  
};


3. 容量操作接口

3.1 size和capacity

size


原型:size_type size() const;

作用:返回vector对象有效数据的个数


capacity


原型:size_type capacity() const;

作用:返回vector对象的容量大小


注意指针减指针的结果并不是单纯的数字减法的结果,编译器会根据指针类型把数字减法的结果除以其权重 sizeof(T),得到对应类型的有效数据的个数。


指针减指针运算:1.计算间隔的字节数,2、除以权重( sizeof(T))。


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


3.2 reserve

原型:void reserve (size_type n);

作用:调整vector对象的容量


n > 对象的容量大小:开新空间扩容(还要拷贝旧空间的数据)

n <= 对象的容量大小:啥事都不干

void reserve(size_t n)
{
  if (n > capacity())
  {
  size_t len = size();
  // 1.开新空间
  T* tmp = new T[n];
  // 2.拷贝数据
  for (size_t i = 0; i < len; ++i)
  {
    tmp[i] = _first[i];
  }
  // 3.释放旧空间,并更新成员变量
  delete[] _first;
  _first = tmp;
  _finish = tmp + len;
  _endofstorage = tmp + n;
  }
}


问:拷贝数据时能否用m92df3d1a26a04d05abcaf05bdcc5461a.pngemecpy?


在回答这个问题之前我们先来看看memcpy的拷贝原理,其实就是按一个字节一个字节这样的来完成深拷贝。


void* memcpy(void* dest, const void* src, size_t num)//传入数据可能是各种类型的,所以用void*接收
{
    //断言,判断指针的有效性,防止野指针
    assert(dest!=NULL);
    assert(src!=NULL);
  //void*
  //可以接收所有类型的指针
  //不可以进行解引用和加减的操作,但可以比较大小
  void* tmp = dest;
  while (num--)
  {
  //把指针类型转化为char*在解引用和+1/-1时可以访问一个字节
  *(char*)dest = *(char*)src;
  ((char*)dest)++;
  ((char*)src)++;
  }
  return tmp;
}



如果我们是内置类型还好,但是像string类对象那样有指向一块空间的指针的成员变量的话就会造成浅拷贝


6d863bc21f694746bdb8977faf236578.png

3.3 resize

原型:void resize (size_type n, value_type val = value_type());

作用:调整vector有效数据的大小


n > 有效元素个数:多出来的有效空间用val填补,如果不传那就填补对应数据类型默认的值value_type(),如果大于capacity()还会扩容

n < 等于有效元素个数:会截断多出来的有效数据

void resize(size_t n, const T& val = T())
{
  // 1.当n大于其有效数据长度的处理
  if (n > size())
  {
  if (n > capacity())
  {
    reserve(n);
  }
  iterator finish = _first + n*sizeof(T);
  while (_finish != finish)
  {
    *_finish = val;
    ++_finish;
  }
  }
  else// 2.当n小于等于其有效数据长度的处理
  {
  _finish = _first + n * sizeof(T);
  }
}


内置类型的构造函数


我们resize参数列表中有个形参const T& val = T(),这里的T是传入数据的类型,T()就是这个类型的一个匿名对象。如果T是自定义类型的话确实有它的默认的构造函数,但如果T是内置类型的话像int, double,char他们也有自己的默认构造函数吗?

20210506151909793.png


4. 默认成员函数

4.1 构造函数

类型一


原型:vector()

作用:构造一个空对象


什么都没传就是空对象。没有开辟空间,其成员变量都为nullptr

vector()
  :_first(nullptr)
  ,_finish(nullptr)
  ,_endofstorage(nullptr)

类型二


原型:vector(size_type n, const value_type& val = value_type())

作用:构造并初始化n个val

vector(size_t n, const T& val = T())
  // 1.先把成员变量初始化为空
  :_first(nullptr)
  ,_finish(nullptr)
  ,_endofstorage(nullptr)
{
  // 2.开空间
  reserve(n);
  // 3.给空间赋值
  while (n--)
  {
  *_finish = val;
  ++_finish;
  }
}


问:既然是初始化构造n个val,直接在函数体里构造就行了,在初始化列表把成员变量初始化为空有必要吗?


44aa31a548904bb890a89b7a3edef862.png

4.2 拷贝构造

vector(const vector& v)
  :_first(nullptr)
  ,_finish(nullptr)
  ,_endofstorage(nullptr)
{
  // 1.开和v一样大的空间
  reserve(v.capacity());
  // 2.遍历v,利用迭代器拷贝数据
  const_iterator it = v.begin();
  const_iterator finish = v.cend();
  while (it != finish)
  {
  *_finish++ = *it++;
  }
}

拷贝构造的几点说明


2074cb6066834da4aad5b987b37aa27b.png


4.3 赋值重载

vector<T>& operator=(vector<T> v)//注意这里的参数v是拷贝构造得来的
{
  swap(v);//这里复用了成员函数swap
  return *this;
}
//成员函数swap
void swap(vector<T>& v)
{
    //相互交换各自指向的地址
    //下面的swap是std的swap
  std::swap(_first, v._first);
  std::swap(_finish, v._finish);
  std::swap(_endofstorage, v._endofstorage);
}


成员函数swap和std::swap

3c3e18c10d904cdca102be86185b5316.png



赋值重载的几点说明


20210506203821563.png


5. 修改操作接口

5.1 insert

原型:iterator insert (iterator position, const value_type& val);

作用:在pos位置插入一个数据


iterator insert(iterator pos, const T& val)
{
  // 插入之前检查是否需要扩容
  if (_finish == _endofstorage)
  {
  size_t len = pos-_first;
  size_t newcapacity = capacity() == 0 ? 2 : capacity() * 2;
  reserve(newcapacity);
  // 更新迭代器(下面图里有说明)
  pos = _first + len;
  }
  // end为最后一个有效数据的迭代器
  iterator end = _finish-1;
  // 挪动数据
  while (pos <= end)
  {
  *(end + 1) = *end;
  --end;
  }
  // 插入数据
  *pos = val;
  ++_finish;
  return pos;
}

insert涉及到的迭代器失效问题

①增容导致外面传入的迭代器pos变成野指针

因为增容的过程是开新空间、拷贝数据、释放旧空间。在增容后外面的pos指向的空间被释,自己放变成了野指针。造成这个问题的原因是原空间被释放,而外面的pos不能及时更新。

c35dfebe01aa43df80b95bf99c6ed378.png

当然对于pos我们是传值进来的,虽然我们在函数里面更新了它,但是外面的pos其实还是失效的,它指向一块已经被释放的空间,变成了野指针。如果是传引用的话就可以解决,不过STL库并没有这样做。

10311985228c43708e6d3f6aa2d23d14.png


②插入导致外面传入的迭代器pos意义改变

这是在没有增容的情况发生的迭代器失效问题,插入后外面的迭代器pos指向的值变成了新插入的val。造成这个问题的原因是vector的存储空间是连续的,外面的pos永远指向那个位置,而insert就是要挪动pos以及pos位置以后的数据,最后在pos位置上插入新的数据val。

14324e5a540d4394b8657c43dd85a817.png


insert迭代器失效问题总结

在使用insert后,原来的pos的最好别用了,因为我们不确定是否增容导致它变成野指针还是,还是没增容但是它的意义改变了。要用的话就更新它,让它接收insert的返回值,这个返回值一定是有效的,但是我们要清楚返回的迭代器它的值是指向新插入那个数据的。6ad611be20b34e4eabb10bf09d57406f.png



问:插入一个数据,能否用memset?

20210506212306640.png

20210506223017819.png20210506222122839.png



memcpy和memset拷贝数据总结


拷贝数据时慎用memcpy和memset,前者如果拷贝的是涉及到内存管理的数据类型比如string会造成浅拷贝,后者只是按一个字节的内容来拷贝的。最保险的拷贝方式还是遍历一遍单独拿出每个数据以来赋值完成拷贝。


5.2 erase

iterator erase(iterator pos)
{
  //把pos后面的数据一个一个地往前挪
  iterator begin = pos + 1;
  while (begin != _finish)
  {
  *(begin - 1) = *begin;
  ++begin;
  }
  --_finish;
  return pos;
}


erase涉及到的迭代器失效问题

①缩容导致外面传入的迭代器pos变成野指针

有的编译器,如果你一直删除数据,而导致太多空间空不出不同,编译器会把你的容量缩小:开辟小空间、拷贝数据、释放旧的大空间。和insert一样外面的迭代器pos变成了野指针。

②:删除导致外面传入的迭代器pos意义改变

在未缩容的情况下,erase删除一个数据后,外面pos的值变成了那个被删除数据的后一个数据,这时因为vector存储空间是连续的而pos永远指向空间的那个位置,删除一个数据就是把后面位置的数据往前挪。

关于erase写法的几点说明


20210506234011646.png


相关文章
|
6月前
|
存储 编译器 C++
【C++】vector介绍+模拟实现
【C++】vector介绍+模拟实现
|
3天前
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
3月前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
136 4
|
2月前
|
存储 对象存储 C++
C++ 中 std::array<int, array_size> 与 std::vector<int> 的深入对比
本文深入对比了 C++ 标准库中的 `std::array` 和 `std::vector`,从内存管理、性能、功能特性、使用场景等方面详细分析了两者的差异。`std::array` 适合固定大小的数据和高性能需求,而 `std::vector` 则提供了动态调整大小的灵活性,适用于数据量不确定或需要频繁操作的场景。选择合适的容器可以提高代码的效率和可靠性。
101 0
|
2月前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
67 0
|
4月前
|
存储 C++ 索引
【C++打怪之路Lv9】-- vector
【C++打怪之路Lv9】-- vector
40 1
|
4月前
|
安全 测试技术 C++
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化2
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化
92 6
|
4月前
|
安全 测试技术 C++
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化1
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化
120 7
|
4月前
|
编译器 C++
【C++】—— vector模拟实现
【C++】—— vector模拟实现
|
4月前
|
编译器 C语言 C++
【C++篇】解密 STL 动态之魂:全面掌握 C++ vector 的高效与优雅
【C++篇】解密 STL 动态之魂:全面掌握 C++ vector 的高效与优雅
87 3