1.vector的基本介绍
vector内部是以动态数组的形式来存储数据的,表示可变大小的序列容器。vector的空间是连续的,可以采用下标对vector的元素进行访问。
vector通过随机访问元素的效率非常高,但是执行插入和删除时效率低。执行尾插或尾删效率高。
2.vector的成员
和string不同的是,vector具有三个迭代器对象,可以当成是原生指针。和模拟string一样,我们可以自己创建一个命名空间来保护自己模拟实现的vector。
#include <assert.h>
#include <iostream>
#include <algorithm>
using namespace std;
namespace L
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* iterator_const;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
三个成员表示的含义不同,_start指向第一个元素,_finish指向最后一个元素的下一个位置,而_end_of_storage指向vector的总容器大小。
3.模拟vector的构造函数
vector()
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{}
这里利用初始化列表对vector这个类的每个成员进行初始化。
除了最基本的这种构造函数,vector里还有提供初始化n个val的特殊构造函数。
vector(size_t n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
vector(int n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
为什么这里又要额外提供一个形参是int类型的函数呢?
因为在传int,int类型的时候如果没有提供第二个函数会报错。
假设我们没有提供第二个带int形参的函数。
void test2()
{
vector<int> v(10, 1);
}
这里是想构造一个10个空间,每个都初始化为1的vector。
结果报错了。
点击错误之后跳转到错误,发现错误出在完全不相关的迭代器区间函数(第四点有提到这个函数)上。
因为在传int,int类型的参数时,由于编译器会寻找参数最匹配的函数来调用,而迭代器区间函数恰好有两个相同的参数,并且我们的第一个初始化n个val的函数的第一个参数是size_t,因此编译器就会调用迭代器区间函数,最后的报错原因就是int不能进行解引用,所以出现了非法的间接寻址这个错误。
4.模拟vector的拷贝构造函数
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
while (first != last)
{
*_finish = *first;
++first;
}
}
void swap(vector<T>& v)
{
::swap(_start, v._start);
::swap(_finish, v._finish);
::swap(_end_of_storage, v._end_of_storage);
}
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
vector<T> tmp(v.begin(), v.end());
swap(tmp);
}
为了实现拷贝构造的功能,这里提供了两个特殊的函数接口,一个是迭代器区间函数,一个是交换函数,思路是构建一个临时变量tmp,无论是T是什么类型的数据,都先装进容器vector里,然后再与tmp进行交换,在构造函数结束时,临时变量tmp会去调用自己的析构函数来销毁自己。
5.模拟vector的插入与删除
vector和之前学的string一样,只要实现了insert和erase,就可以复用在头删尾删。
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len;
}
iterator end = _finish;
while (end > pos)
{
*end = *(end - 1);
--end;
}
*pos = x;
++_finish;
return pos;
}
思路是先断言pos的合理性,然后判断是否需要扩容,最后从后往前挪动数据。
iterator erase(iterator pos)
{
assert(pos < _finish);
assert(pos >= _start);
iterator del = pos + 1;
while (del != _finish)
{
*(del - 1) = *del;
++del;
}
--_finish;
return pos;
}
erase也和insert思路相同。不过erase是从前往后覆盖数据来实现删除数据。
(1)迭代器失效
为什么这里返回值是迭代器呢?写成无返回值的函数行吗?
不行。不然可能会导致迭代器失效的问题。insert这里提供的是返回插入之后的原pos迭代器,而erase是返回删除之后的原pos迭代器。
迭代器失效是指迭代器指向的元素或位置已经无效,无法进行正确的访问或修改。
不同编译器下,迭代器失效的情况可能不同,可能其他编译器不认为是失效。我用的是vs环境下的编译器。
1 | 插入元素/扩容引起的迭代器指向的元素或者空间发生变化,导致迭代器失效 |
2 | 删除元素使得某些元素次序发生变化使得原本指向某元素的迭代器不再指向希望指向的元素。 |
3 | 容器空间被释放导致存放原容器元素的空间不再有效,使得指向原空间的迭代器失效。 |
4 | 容器元素整体“迁移”导致存放原容器元素的空间不再有效,使得指向原空间的迭代器失效。 |
解决方法是提供原元素的迭代器即可。每次进行删除或插入等操作时更新迭代器。
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it = v.erase(it);
}
else
{
++it;
}
}
(2)尾插尾删等接口
T& front()
{
assert(_finish > _start);
return *_start;
}
T& back()
{
assert(_finish > _start);
return *(_finish - 1);
}
void push_back(const T& x)
{
/*if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = x;
++_finish;*/
insert(_finish, x);
}
void pop_back()
{
assert(_finish > _start);
--_finish;
}
获取头尾元素数据,直接返回头尾指针。尾插可以直接复用insert,尾删比较简单,直接_finish--,禁止访问数据即可。
6.模拟vector的相关扩容函数
vector和其他容器也一样,会有reserve和resize这两个扩容函数。本质区别是reserve只会单纯的扩容,如果容量够则不扩容。而resize则需要分容量够,容量不够的两种情况来看待,并且resize在扩容时会进行初始化。其中在容量够的情况下,还要细分开空间的大小有没有超过有效数据的数量,如果少于有效数据就是缩容,需要删除元素,如果大于有效数据就需要对_finish进行操作,并初始化后面的值。
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
iterator tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * sz);
for (size_t i = 0; i < n; ++i)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
void resize(size_t n, const T& val = T())
{
if (n > capacity())
{
reserve(n);
}
if (n > size())
{
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
else
{
_finish = _start + n;
}
}
同样reserve这里会出现野指针的问题,也就是迭代器失效,因为reserve多用于插入与删除函数里进行扩容操作,如果使用memcpy直接拷贝的话,假如进行异地扩容会导致迭代器失效,所以采用先创建临时变量tmp来开空间的方法,然后再将原数据采用for循环的方式一个一个拷贝到tmp,最后再拷贝回vector。
7.其他特殊接口函数
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
iterator_const begin() const
{
return _start;
}
iterator_const end() const
{
return _finish;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
size_t size() const
{
return _finish - _start;
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
8.模拟vector的析构函数
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
_start是一块连续的空间,所以直接delete就行了。