Vector
- 前言
- 一、了解vector的构造
- 二、进行模拟实现
-
- 成员变量及部分函数
- reserve()
- resize()
- 构造函数
-
- [1.`vector(size_t n, const T& val)`](#1.
vector(size_t n, const T& val)) - [2.template<class Inputiterator> vector(Inputiterator first, Inputiterator last)](#2.template<class Inputiterator> vector(Inputiterator first, Inputiterator last))
- [3.默认无参构造函数 vector()](#3.默认无参构造函数 vector())
- [4.拷贝构造函数 vector(const vector<T>& v)](#4.拷贝构造函数 vector(const vector<T>& v))
- [1.`vector(size_t n, const T& val)`](#1.
- 插入函数与删除函数
- 赋值重载函数
前言
本篇讲解stl库中的vector的模拟实现
一、了解vector的构造
我们有了之前学习过string的经验,学习vector自然是手到擒来,但是vector的内部构造和string不太一样,我们可以通过源码了解一下,如:

我们可以看到,在vector中,有三个成员变量,分别为start、finish、end_of_storage,且她们的类型均为iterator,这与string的char数组、size和capacity是不一样的,vector当然也可以使用string的构造来实现,但是源码采用了这种方式,自然也有自己的道理,那么这个iterator,我们之前说过它是迭代器,在vector中是怎么使用的呢?我们来细细分晓。

我们通过源码来看,可以发现iterator本质上就是模板类型的指针,这与string中迭代器的使用类似
二、进行模拟实现
对vector进行模拟实现,我们需要先猜测并确定vector各成员变量的功能,我们可以在C++官网上找到vector的各种成员函数和非成员函数,通过每个成员变量在不同函数中的作用来分析
成员变量及部分函数
cpp
namespace xx
{
template <class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstorage - _start;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
reserve()

cpp
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t _size = size();
if (_start)
{
for (int i = 0; i < size(); i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + _size;
_endofstorage = _start + n;
}
}
依据reserve函数为大家讲解几个自行模拟实现时的易错点
第一,为什么要使用_size来单独存储一次变量
在代码中,我们可以看到在_finish = _start + _szie时使用了一次_size,这是因为,我们的size()函数的运算逻辑就是_finish - _start,如:
cpp
size_t size() const
{
return _finish - _start;
}
当我们的_finish和_start初始都为nullptr时,经过扩容赋值之后,_start确实是指向了新的空间,但是_finish呢,他并没有改变,依旧是nullptr,因此我们直接写:
cpp
_start = tmp;
_finish = _start + size();
_endofstorage = _start + n;
那么就变成了_finish = _start + nullptr - _start;此时_finish依然是nullptr,这是不符合预期的,而使用一个单独的变量用于存储两者最初的距离便能解决这个问题,正如我们代码中所写
第二,为什么要使用for循环来进行赋值,为什么不使用strcpy或者memcpy?
这个问题非常好,如果我们使用memcpy进行拷贝的话,此处对vector对象的内容的拷贝就是浅拷贝,可能有人会问了,浅拷贝怎么了,int这种内置类型浅拷贝并没有问题的啊,话是如此说,但是不要忘了,我们现在使用的是vector,而我们模拟实现vector自然是兼容性越大越好,不只是针对int这一种类型,也就是说,我们的vector模板可能是vector<int>、vector<string>、vector<vector<int>>等等都有可能,那么memcpy浅拷贝内置类型确实没问题,但是自定义类型呢?比如vector<string>?这自然是不行的,我们到后面会进行演示
resize()
resize与reserve唯一不同的地方在于是否赋值和缩容,因此我们可以直接复用

cpp
void resize(size_t n,const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
构造函数
我们可以先看构造函数
我们可以看到有三种构造函数,其中的allocator是空间配置器方面的知识,我们以后会讲,我们下面会讲解实现后面三项
1.vector(size_t n, const T& val)
使用n个相同类型的值进行构造初始化
cpp
vector(size_t n, const T& val)
{
resize(n, val);
}
2.template vector(Inputiterator first, Inputiterator last)
使用迭代器来进行构造初始化
cpp
template<class Inputiterator>
vector(Inputiterator first, Inputiterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
3.默认无参构造函数 vector()
cpp
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
这里我们还要讲解一处易错点,当我们同时定义了第一种构造函数和第二种构造函数时,此时如果我们想初始化为n个相同类型的值,编译器会报错,如:

原因是什么呢?
可以看到,当我们想用10个1去进行初始化时,此时我们应该想调用第一种构造函数,因为我们的值1,类型为int,个数10也是int
而我们的两种构造函数参数里都有模板类型,第一种构造函数的第二个参数类型为模板类型,第二种构造函数的两个参数均为模板类型,也就是说,两个函数都可以识别int类型的参数
而在编译器中,识别参数类型后,会优先调用最适配的类型,
我们传参的类型为int int,
第一种构造函数的参数类型为sizet_t 模板识别的int类型,
第二种构造函数的参数类型为模板识别出的int类型 模板识别出的intl类型
那么适配度最高的的自然是第二种构造函数,此时我们就会调用第二种构造函数,inputiterator模板识别为int类型
那么为啥会出错呢,我们看进入到函数内部之后,我们要进行遍历了,因为第二种构造函数是给迭代器准备的,而*first,当first的类型被自动识别为int之后,自然是不被允许的,int类型不允许被解引用,此时就会报错
解决办法就是再添加一个参数类型均为int类型的函数
cpp
vector(int n, const T& val)
{
resize(n, val);
}
4.拷贝构造函数 vector(const vector& v)
cpp
vector(const vector<T>& v)
{
//reserve(v.capacity());
//memcpy(_start, v._start, sizeof(T)*v.size());
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
}
我们可以看到,此处我们依旧摒弃了memcpy的用法,这和之前的原因一样,memcpy是浅拷贝
如果我们依旧使用memcpy进行拷贝,那么当我们调用拷贝构造时,会使得原对象与当前对象指向同一块空间,那么就会遭遇二次析构的问题,会报错,譬如我们使用vector<string>时,

此时浅拷贝会两个vector的string对象都指向同一块空间,当释放空间时会重复释放
vector 当T为string这种自定义类型时,会依次调用数组中每个对象的析构函数,最后再将整个数组进行析构,释放整个空间,因为不允许使用memcpy,正确情况应如下:

插入函数与删除函数
cpp
void insert(iterator pos,const T& a)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
reserve(newcapacity);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = a;
_finish++;
}
此处我们需要补充一个变量len来保存pos位置和_start之间的距离,因为我们如果内存不够去进行扩容的话,扩容之后,_start和_finish的位置自然都会发生变化,一般都是异地扩容,那么此时pos的位置就会失效,我们需要根据原本的距离去更新扩容后pos的位置
并且此处使用迭代器代替下标,是有好处的,我们不需要考虑在挪动数据的情况下size_t类型始终不小于0的特殊情况
cpp
void erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
iterator begin = pos + 1;
while (begin != _finish)
{
*(begin - 1) = *begin;
++begin;
}
_finish--;
}
注:头插头删,尾插尾删都可以直接复用插入删除函数
赋值重载函数
cpp
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
此处补充一个reserve函数使用memcpy进行浅拷贝时会发生的情况

此时已经报错,分析程序:

当我们运行到delete[] _start但还未运行时,


可以看到我们的tmp和_start经过memcpy拷贝后,因为数据过长,并没有同时存于buffer数组中,而是存在了ptr空间中,且此时两者地址是相同的,当我们运行该语句后,会变成下图:



这已经说明,我们的memcpy是不可用的,因为memcpy是浅拷贝
同时,当我们的数据量过小比如只有几个字母的时候,我们不一定会容易观察到,因为vs配置情况下,为我们提供了一个buffer数组,当数据量比较小时一般都会存于buffer数组中,这会导致我们观察到的地址并不相同,但是报错原因还是相同的
同样,我们之前讲解过一个pos位置失效问题,当我们在外部进行调用时,依旧会产生这种情况

此时会非常危险,因为这不符合我们的逻辑预期,但是并没有报错,因此不要这么使用
此外,当我们如此调用时:

因为我们代码中默认4是一个节点,当超过4时就会触发一次扩容后,扩容后_start和_finish自然会改变位置,而函数传pos,我们是传值传参,也就是说函数中的pos确实改变了,但是外面的pos没有改变,此时pos位置可能就会失效,会引发错误,我们看到代码第137行报错,查找后发现是assert被触发,根据条件判断,也就是此时pos已经不在范围内了,也就是所谓的迭代器失效

针对这种外部迭代器失效的问题,我们的解决方法就是返回一个pos值,如:

stl库中也是如此解决的,但是当我们insert(p+3)的情况时,需要更加一步考虑,我们后续讲解