由于vector和string的接口使用方法类似,这里便不再详细讲解vector各种接口的使用了,可以参考之前所发布的string的使用,或者从官方文档中获取详细的使用方法.
目录
vector介绍
构造函数(有参,无参,迭代器)
析构函数
获取容量函数capacity()
获取有效数据大小size()
reserve()
resize()
push_back()
[]运算符重载
迭代器相关
begin()、end()
pop_back()
insert()&&迭代器失效问题
erase()&&迭代器失效问题
vector介绍
这里直接开始讲解vector的模拟实现.
在此之前,先对vector进行一个简单的介绍.
1. vector是表示可变大小数组的序列容器。
2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小,为了增加存储空间。其做法是:分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
6. 与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起lists和forward_lists统一的迭代器和引用更好。
这些后面模拟接口实现的时候,就会对这些概念有一个大概的了解.
先来说一下vector模拟实现的整体框架.
vector内部是指针实现的,所以类成员有三个:指向数据开始的指针,指向数据结束的指针以及指向数据容量大小的指针.它的迭代器也是一个原生指针.
主要是实现其构造、拷贝构造、赋值运算符重载、析构函数,容量相关类的接口函数,[]重载(访问下标),对数据的修改操作(插入,删除等等).
下面是所有的接口声明.
这里我们把迭代器提前命名好了.
namespace hmylq
{
template<class T>
class vector
{
public:
// Vector的迭代器是一个原生指针
typedef T* iterator;
iterator begin();
iterator end();
/construct and destroy///
vector();
vector(int n, const T& value = T());
template<class InputIterator>
vector(InputIterator first, InputIterator last);
vector(const vector<T>& v);
vector<T>& operator= (vector<T> v);
~vector();
/// capacity
size_t size() const ;
size_t capacity() const;
void reserve(size_t n);
void resize(size_t n, const T& value = T());
///access///
T& operator[](size_t pos);
const T& operator[](size_t pos)const;
///modify/
void push_back(const T& x);
void pop_back();
void swap(vector<T>& v);
iterator insert(iterator pos, const T& x);
iterator erase(Iterator pos);
private:
iterator _start; // 指向数据块的开始
iterator _finish; // 指向有效数据的尾
iterator _endOfStorage; // 指向存储容量的尾
};
}
构造函数(有参,无参,迭代器)
先来看无参的构造函数:初始化列表将类成员3个指针全部初始化为空即可.
vector()
:_start(nullptr)
,_finsh(nullptr)
,_end_of_storage(nullptr)
{
;
}
有参构造函数:两个参数,分别是数据的总大小以及数据的内容.
比如传一个5,1.就相当于在vector里面存储1,1,1,1,1
这里给了两个缺省值.
vector(int n, const T& value = T())//T()是一个匿名对象,如string,会调用它的构造函数,内置类型如int,也有构造函数,默认为0.
:_start(nullptr)
, _finsh(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
这里的reserve()和push_back()函数我们后面会模拟实现.
还有最后一个是迭代器区间构造.
首先创建一个模板参数,我们认作它为迭代器类型,然后利用从起始位置不断++直到到了数据的结尾结束,每次从中插入每个数值.
//迭代器区间构造
vector(Inputlterator first, Inputlterator last)
:start(nullptr)
,_finsh(nullptr)
,_end_of_storage(nullptr)
{
for (first != last)
{
push_back(*first);
++first;
}
}
这里是保证到end时就一定结束,保证所有数据可以被访问到.
析构函数
析构函数比较简单,首先delete掉数据的内容,再将三个指针指向的内容全部置为空即可.
~vector()
{
delete[] _start;
_start = _finsh = _end_of_storage = nullptr;
}
获取容量函数capacity()
由于类成员是指针,我们没办法直接返回它,可以利用指针的相减来获得空间的大小.
用指向容量大小的指针 - 指向数据开始的指针既容量大小.
size_t capacity() const
{
return _end_of_storage - _start;
}
获取有效数据大小size()
这个和capacity()函数类似,只不过是用指向数据结束的指针 - 指向开始的指针.
size_t size() const
{
return _finsh - _start;
}
reserve()
这个函数的作用是扩容,当用户输入的n小于原本容量时则不需要任何操作,仅当n大于_end_of_storage时才进行扩容
首先新开辟一块空间,然后将原来的数据拷贝到新空间中,再分别更新_start,_finsh,_end_of_storage,但是更新的时候会有一些问题.
先来看一个常见的错误版本.下面是错误的代码!只是拿来示例一下问题.
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];//新开辟一块空间
if (_start)
{
memcpy(tmp, _start, sizeof(T) * size());//将原来的数据拷贝到新空间
delete[] _start;//释放掉旧数据,此时三个指针都指向空.
}
_start = tmp;//将开始的指针指向数据的开始
_finsh = tmp + size();//***wrong***
_end_of_storage = _start + n;
}
}
以上代码看起来没有任何问题,但是在wrong的那一行,会有一个bug.
我们前面已经把空间释放掉了,所以3个指针都指向空,但是在执行_finsh = tmp + size();这条语句时,size()是如何计算的呢,size()是_finsh - _start,而此时只有_start更新了,而_finsh依然是空,所以替换过来,相当于抵消了_start,结果还是_finsh,是空.
所以解决办法是一开始先保存上数据的大小.正确代码如下:
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;
}
_start = tmp;
_finsh = _start + sz ;
_end_of_storage = _start + n;
}
}
resize()
这个和string类型的很像,
1.如果n > 总容量,则直接扩容到n,并且初始化
2.如果n > size(),但是小于容量,直接初始化即可
3.如果n < size(),需要删除数据
void resize(size_t n, const T& value = T())
{
if (n > capacity())
{
reserve(n);
}
if (n > size() )
{
while (_finsh < _start + n)
{
*_finsh = value;
++_finsh;
}
}
else
{
_finsh = _start + n;
}
}
push_back()
这个函数的作用是尾部插入一个数据,这个比string的简单,因为vector只能插入一个数据.
首先判断是否需要扩容,再直接将*_finsh的值赋值为val,然后++_finsh,这里注意,_finsh指向的是最后一个数据的下一个位置,所以可以直接赋值.
void push_back(const T& x)
{
if (_finsh == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finsh = x;
++_finsh;
}
[]运算符重载
我们需要像数组一样下标访问或修改某一个元素,那么需要对[]进行重载.方法很简单,就是返回第pos个位置的值.
这里有两种,一种是普通的返回,还有一种是const修饰的,以便能适用const所修饰的调用.
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const修饰的
const T& operator[](size_t pos) const
{
return _start[pos];
}
迭代器相关
上面也说过,迭代器内部也是原生指针,所以
typedef T* iterator;
begin()、end()
迭代器中的begin和end就是返回指向数据开始的指针,和数据结尾的指针,如下
iterator begin()
{
return _start;
}
end()
iterator end()
{
return _finsh;
}
既然有了迭代器,那么也就对应的可以进行范围for遍历了.
for(auto e : v)
{
cout << e << endl;
}
范围for其实是一个傻瓜式的替换.
无论类里的迭代器的开始写的什么,最终都会被替换成begin(),所以如果自己实现的迭代器的开始函数名字不是begin(),则会报错,因为范围for被替换成了begin(),而没有begin()这个成员
pop_back()
思路很简单,直接--_finsh就相当于把最后一个元素删除掉了.
void pop_back()
{
assert(_finsh > _start);
--_finsh;
}
insert()&&迭代器失效问题
这里insert是插入一个元素,然后有两个参数,一个是迭代器类型的pos位置,另一个是要插入的值val.
我们的思路是:
1.如果插入后空间足够,我们从pos位置依次向后挪动一个空间,再把空出来的这一个空间插入val.
2.如果插入后空间不足,则需要扩容,扩容之后再进行第一步的操作,但这样会存在迭代器失效问题.
先来看这段存在迭代器失效问题的错误代码:
void insert(iterator pos, const T& x) // 注意迭代器失效
{
assert(pos >= _start);
assert(pos <= _finsh);
//扩容
if (_finsh == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);//如果需要扩容,pos位置则会改变,需要更新
}
//插入操作
iterator end = _finsh - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
_finsh++;
}
然后我们测试一段代码:
void test_vector5()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//v.push_back(5);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
//查找3
auto p = find(v.begin(), v.end(), 3);
//如果查找到了,则在3的位置插入一个30
if (p != v.end())
{
//在p位置插入数据以后就不要再去p了,因为可能已经失效了.
v.insert(p, 30);
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
运行起来,发现程序崩溃了.
这是为什么呢?
我们看insert()代码中,首先插入了4个数据,此时要将新数据val插入其中,此时需要扩容
扩完容之后,_start,_finsh,_end_of_storage都已重新更新了位置,并且将原空间中的数据拷贝到新空间中,释放掉旧空间,但注意此时pos依然还在指向旧空间的位置,这就使pos成为了野指针.
画图来理解一下
所以既然pos没有变化,那我们解决方案就是:在扩完容之后重新更新一下pos即可,具体做法为:先保存pos的长度,扩完容之后,再将_start+pos即可.
void insert(iterator pos, const T& x) // 注意迭代器失效
{
assert(pos >= _start);
assert(pos <= _finsh);
if (_finsh == _end_of_storage)
{
size_t len = pos - _ start;//先保存一下长度
reserve(capacity() == 0 ? 4 : capacity() * 2);//如果需要扩容,pos位置则会改变,需要更新
pos = _start + len;
}
iterator end = _finsh - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
_finsh++;
}
在那段测试代码中,我们插入了一个在p位置插入了一个30,但如果此时再插入一个40,便会再次出现问题,因为p的位置插入30时已经失效了,但有人会说我们不是更新了pos吗在扩容之后,我们仔细的看一下,pos只是一份形参!形参的改变不会影响实参,所以p并没有改变.
那可能就有人又说了,直接给pos加上引用不就可以了吗,这样pos更新,p也会跟着更新了。
这么做确实正确的解决了这个问题,但是如果下面这段代码该怎么处理呢?
insert(v.begin(),3);
这句代码是说想在begin()的位置插入一个3,但是由于begin()是传值返回,会形成一份临时拷贝数据,这个数据具有常性,而我们的就是普通的iterator&,权限不可以放大.这样又会造成了问题.
那我们在iterator前面加一个const不就可以了吗,就具有了常性,但加上之后扩容的时候我们该怎么修改这个p呢?这又是个问题.
所以正如以上所说:在p位置插入一个数据后,就不要再去使用这个p了,可能会失效.
erase()&&迭代器失效问题
erase()是删除某个位置的数据,思路也很简单,就是从pos+1位置开始,每次覆盖掉前一个位置.
//STL 规定erase返回删除位置的下一个位置的迭代器
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finsh);
iterator begin = pos + 1;
while (begin < _finsh)
{
*(begin - 1) = = *begin;
++begin;
}
--_finsh;
return pos;
}
虽然这个代码本身不涉及迭代器失效问题,但看下面一段代码:需要删除所有的偶数.
看代码
比如原来的数据是1,2,3,4.
我们可以看到问题是出现在每次erase之后,它的后一个数据已经移动过来了,但是it依然++,相当于it跳过了一个数据直接访问下一个,所以正确的方法是:
这样就成功解决了.
这里总结一句:insert/erase pos位置,不要直接访问pos,一定要更新!
直接访问,可能会有各种出乎意料的结果,这就是所谓的迭代器失效.
总代码如下:
#include<iostream>
using namespace std;
namespace hmylq
{
template<class T>
class vector
{
public:
//vector的迭代器是一个原生指针
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finsh;
}
/Construct and destory//
vector()
:_start(nullptr)
,_finsh(nullptr)
,_end_of_storage(nullptr)
{
;
}
vector(int n, const T& val = T())//T()是一个匿名对象,如string,会调用它的构造函数,内置类型如int,也有构造函数,默认为0.
:_start(nullptr)
, _finsh(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
template<class Inputlterator>
//迭代器区间构造
vector(Inputlterator first, Inputlterator last)
:_start(nullptr)
,_finsh(nullptr)
,_end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);
++first;
}
}
vector(const vector<T>& v)//拷贝构造
:_start(nullptr)
,_finsh(nullptr)
,_end_of_storage(nullptr)
{
//_start = new T[v.size()];//v.capacity也可以
//memcpy(_start, v._start, sizeof(T) * v.size());
//_finsh = _start + v.size();
//_end_of_storage = _start + v.size();
reserve(v.size());
for (const auto &e : v)
{
push_back(e);
}
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finsh, v._finsh);
std::swap(_end_of_storage, v._end_of_storage);
}
//vector(const vector<T>& v)//拷贝构造
// :_start(nullptr)
// , _finsh(nullptr)
// , _end_of_storage(nullptr)
//{
// vector<T> tmp(v.begin(), v.end());
// swap(tmp);
//}
vector<T>& operator= (vector<T> v)
{
swap(v);
return *this;
}
~vector()
{
delete[] _start;
_start = _finsh = _end_of_storage = nullptr;
}
///capacity///
size_t size() const
{
return _finsh - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;
}
_start = tmp;
_finsh = _start + sz ;
_end_of_storage = _start + n;
}
}
void resize(size_t n, const T& value = T())
{
if (n > capacity())
{
reserve(n);
}
if (n > size() )
{
while (_finsh < _start + n)
{
*_finsh = value;
++_finsh;
}
}
else
{
_finsh = _start + n;
}
}
/access///
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
return _start[pos];
}
/modify//
void push_back(const T& x)
{
if (_finsh == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finsh = x;
++_finsh;
}
void pop_back()
{
assert(_finsh > _start);
--_finsh;
}
void insert(iterator pos, const T& x) // 注意迭代器失效
{
assert(pos >= _start);
assert(pos <= _finsh);
if (_finsh == _end_of_storage)
{
size_t len = pos - _start;//先保存一下长度
reserve(capacity() == 0 ? 4 : capacity() * 2);//如果需要扩容,pos位置则会改变,需要更新
pos = _start + len;
}
iterator end = _finsh - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
_finsh++;
}
void erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finsh);
iterator begin = pos + 1;
while (begin < _finsh)
{
*(begin - 1) == *begin;
++begin;
}
--_finsh;
}
private:
iterator _start;//指向数据块的开始
iterator _finsh;//指向有效数据的尾
iterator _end_of_storage;//指向存储容量的尾
};
}