在上篇文章中,我们已经学习了vector的具体接口使用方法,在本篇文章中,我们将学习实现一个vector容器。
目录
一.vector各函数接口总览
由于标准库中也有vector类,因此我们模拟实现需要放在自己的命名空间内部。
namespace trousers
{
template<class T>
class vector
{
public:
//迭代器
typedef T* iterator;
typedef const T* const_iterator;
//默认成员函数
vector();
vector(size_t n, const T& val);
template <class InputIterator>//需要使用模板,传入的迭代器的类型不定义。(可以通过其他类型容器进行初始化)
vector(InputIterator first, InputIterator last);//迭代器区间构造
vector(const vector<T>& V);//拷贝构造
vector<T>& operator=(const vector<T>& v);//赋值运算符重载
~vector;
//迭代器相关函数
//正向迭代器
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
//反向迭代器
iterator rbegin();
iterator rend();
//容量与大小相关函数
size_t size() const;
size_t capacity() const;
void reserve(size_t n);
void resize(size_t n, const T& val = T());
bool empty()const;
//修改容器内容相关函数
void push_back(const T& x);
void pop_back();
void insert(iterator pos, const T& x);
iterator erase(iterator pos);
void swap(vector<T>& v);
//访问相关函数
T& operator[](size_t i);
T& operator[](size_t i) const;
private:
iterator _start;//指向容器头
iterator _finish;//指向有效数据尾
iterator _end_of_storage;//指向容器空间的尾
};
}
二.vector当中的私有成员
在之前的顺序表博客中提到过,我们的容器是先开空间,再往空间内填充数据的。因此我们的有效数据尾和容器空间尾可能并不相同。
假如现在有这么一个容器,开了9个空间,但是只有6个空间内填充了数据。
那么,我们的_start指向的就是容器的头部,_finish指向的是填充了数据的最后一个空间。而_end_of_storage指向的是空间的尾部。
三.默认成员函数
3.1构造函数
我们在这里实现3个vector的构造函数,分别是:
vector();
vector(size_t n, const T& val);
template <class InputIterator>//需要使用模板,传入的迭代器的类型不定义。(可以通过其他类型容器进行初始化)
vector(InputIterator first, InputIterator last);//迭代器区间构造
3.1.1构造函数1
首先,我们完成第一个无参的构造函数。
在此处,我们直接将构造的对象的三个私有成员全部置为空指针即可。
vector()
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{}
3.1.2构造函数2
该构造函数可以将构造的对象初始化为由n个val构成的容器。
这个函数要用到reserve和push_back,我们在本文的后续小节中介绍这两个函数的实现,在这里我们直接使用。
这个函数的实现思路是:先使用reserve设置空间,然后使用一个循环将数据设置进去。
具体实现代码如下:
vector(size_t n, const T& val)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
reserve(n);//reserve内有对私有成员的更新,因此初始化列表可以置为nullptr
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
该函数除了需要实现这个版本之外,还需要实现如下两个版本:
vector(long n, const T& val)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);//reserve内有对私有成员的更新,因此初始化列表可以置为nullptr
for (long i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T& val)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);//reserve内有对私有成员的更新,因此初始化列表可以置为nullptr
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
我们可以发现,这两个重载函数的不同就是其参数n的类型不同,那么为什么我们要实现这两个版本呢?这是因为编译器无法确认参数的类型。
vector<int> v1(5,7);
在运行上面的这行代码时,编译器会优先调用构造函数3,而不会调用构造函数2.
这是因为5和7是同一个类型的,和构造函数3的参数列表最相近,因此我们会在此处调用构造函数3。因此,我们需要实现上述两个重载函数。
3.1.3构造函数3
vector还支持迭代器区间初始化,我们可以使用一个前闭后开的迭代器进行初始化。
因为迭代器区区间可以是任意容器的迭代器区间,因此我们需要将这个函数设置成一个函数模板,以确保可以使用任意类型的迭代器初始化该容器。
而这个函数的实现逻辑和第二个构造函数的实现方式基本上是相同的。
template <class InputIterator>//需要使用模板,传入的迭代器的类型不定义。(可以通过其他类型容器进行初始化)
vector(InputIterator first, InputIterator last)//迭代器区间构造
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);//push_back中调用了reserve,可以用于设置私有成员
++first;
}
}
3.2拷贝构造函数
拷贝构造函数的实现是极其简单的,我们直接一个数据一个数据的复制过去即可。
vector(const vector<T>& V)//拷贝构造
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(V.capacity());
for (auto e : V)
{
push_back(e);
}
}
3.3赋值运算符重载函数
关于赋值运算符的重载可以有多种实现方法
实现方法1:
- 首先判断不能是自己给自己赋值
- 若是给自己赋值则不需要进行操作
- 若不是给自己赋值,则先清理掉原来的空间
- 之后开辟一块和容器v同样大的空间
- 然后将v中的数据一个一个拷贝过去
- 最后更新私有成员的值
vector<T>& operator=(const vector<T>& v);//赋值运算符重载
{
if (this != &v)//判断
{
//清理数据
delete[] _start;
//开空间
_start = new [v.capacity()];
//复制数据
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
//_start[i]=v[i]
}
//更新数据
_finish = _start + size();
_end_of_storage = _start + v.capacity();
}
return *this;//返回可以支持连续赋值。
}
这个方法实现起来过于繁琐,我很讨厌写这种要很长的代码。所以我选择下面的实现方法
实现方法2:
首先,我们需要修改该函数的参数列表
vector<T>& operator=(const vector<T> v);//赋值运算符重载
这里我们不选择引用传参,那么我们的编译器则会在此处形成一个会自动销毁的形参。
该函数具体的运行逻辑如下:
- 交换这两个对象
- 交换后,两个对象的地址互换了。
- 此时,形参的地址是this的原地址。
- 销毁形参,也就把this指向的空间销毁了
- 形参被销毁,this的赋值也完成了
因此,我们实现起来仅仅只需要交换一下即可。
vector<T>& operator=(const vector<T> v);//赋值运算符重载
{
swap(v);
return *this;
}
3.4析构函数
对于析构函数的实现很简单,我们直接将资源清理掉就可以了。
~vector()
{
if (_start)
{
delete[] _start;
//更新数据
_start = _end_of_storage = _finish = nullptr;
}
}
之所以判断_start是否为空,是因为如果为空的话可以少执行好多行代码。
四.迭代器相关函数
4.1begin和end
begin()返回start,end返回_finish即可。
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
4.2rbegin和rend
不必多言,直接看图然后看代码。
iterator rbegin()
{
return _finish - 1;
}
iterator rend()
{
return _start - 1;
}
五.容量与大小相关函数
5.1size和capacity
这两个函数的实现是相当简单的。
- _finish-_start就是size
- _end_of_storage就是capacity
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
5.2reserve
对于reserve的实现,我们要先了解reserve的规则:
- 当n大于当前对象的capacity时,实现扩容
- 当n小于当前对象的caoacity时,什么变化都不做
了解了reserve的规则了之后,我们就可以着手于实现reserve函数。
我们可以将实现分为如下几步:
- 判断当前n和capacity的大小
- 若大于,则开辟一块可以放置n个T数据的空间。
- 将原容器中的数据拷贝到该空间中
- 将原容器的空间释放
- 更新新空间的各个成员
具体的实现如下:
void reserve(size_t n)
{
size_t sz = size();
if (n > capacity())//判断
{
T* tmp = new T[n];//开空间
//拷贝
for (size_t i = 0; i <sz ; i++)
{
tmp[i] = _start[i];
}
//释放原空间
delete[] _start;
//更新数据
_start = tmp;
_finish = tmp + sz;
_end_of_storage = _tmp + n;
}
}
虽然,我们的reserve函数已经可以完成所有的功能了,但是,如果我们对0个数据的容器置为0的话,依旧需要走一次深拷贝。这就使性能下降,因此我们可以再加一个对该情况的处理。
if (_start)
{
//拷贝
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
//释放原空间
delete[] _start;//因为_start中没有数据,因此该情况下在外部释放它其实是无用功。而我们外部重置了_start,因此我们可以将释放_start的的逻辑放在if内部
}
5.3resize
首先,resize的规则是:
- 当n大于当前的resize时,将size扩大到n,并将扩大的空间的数据设置为val
- 当n小于当前的size时,截断掉即可。
根据resize的规则,我们可以得出实现该函数的步骤如下:
- 判断n的大小
- 如果n小于size,则更新finish
- 否则,则判断是否需要扩容
- 需要扩容则调用reserve,之后将扩大的空间设置为val即可。
具体的实现代码如下:
void resize(size_t n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
if (n > capacity())
{
reserve(n);
}
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
}
5.4empty
这个函数相当简单,我们只需要判断一下_start和_finish是否相同即可完成该函数
bool empty()const
{
return _start == _finish;
}
六.修改容器内容相关函数
6.1push_back
对于push_back的实现,我们的实现分为如下几个步骤:
- 判断容器的空间是否为满
- 若已满,则进行扩容
- 若未满,则将新数据插入到_finish处的位置
- 插入后,再将_finish的位置+1.
具体的实现代码如下:
void push_back(const T& x)
{
if (_finish = _end_of_storage)//判断
{
//扩容
size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
//使用reserve函数进行扩容
reserve(newcapacity);
}
*_finish = x;
_finish++;
}
6.2pop_back
对于数据的删除,我们使用的方法很简单,直接将_finish的位置往前移动1即可。
void pop_back()
{
assert(_finish!=nullptr)
--_finish;
}
6.3insert
insert函数可以用于在指定位置插入数据。
我们实现的步骤如下:
- 首先判断是否需要扩容
- 将pos位置以及其后的数据整体后移
- 向pos位置插入val
void insert(iterator pos, const T& x)
{
if (_finish == _end_of_storage)//判断
{ //扩容
size_t newcapacity = capacity() == 0:4 ? 2 * capacity();
reserve(newcapacity);
}
//后移
iterator end = _finish;
while (end>=pos+1)//移动本身则需要+1,不需要则不用+1.
{
*end = *(end - 1);
end--;
}
//更新
*pos = x;
_finish++;
}
在实践中,我们发现这段代码并无法完成insert的功能,这是因为我们扩容是可能是异地扩容的,而pos的位置却没有变化。因此这时便产生了问题。
我们可以通过记录pos和start的距离并在扩容后更新从而实现该功能。
void insert(iterator pos, const T& x)
{
if (_finish == _end_of_storage)//判断
{ //扩容
size_t len = pos - _start;
size_t newcapacity = capacity() == 0:4 ? 2 * capacity();
reserve(newcapacity);
pos = _start + len;
}
//后移
iterator end = _finish;
while (end>=pos+1)//移动本身则需要+1,不需要则不用+1.
{
*end = *(end - 1);
end--;
}
//更新
*pos = x;
_finish++;
}
6.4erase
删除一个位置的数据,那么我们直接让后面的数据往前覆盖即可。
iterator erase(iterator pos)
{
assert(!empty() && (pos > _start && pos << _finish));
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
it++;
}
//更新
_finish--;
return pos;
}
6.5swap
swap函数如果通过增加一个临时变量然后逐个复制过去的话,那么消耗就比较大了。因此我们这里不采取这种方式。
我们调用库里的swap函数,直接将两个容器的私有成员变量交换一下,即可完成这个函数逻辑。
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
七.访问容器相关函数
我们直接访问即可。
T& operator[](size_t i)
{
return _start[i];
}
const T& operator[](size_t i) const
{
return _start[i];
}