参照SGI版本的vector实现,使用三个指针来维护这样一段内存空间
template<class T>
class vector
{
public:
//成员函数
private:
T* _start;
T* _finish;
T* _endOfStorage;
};
图示结构如下:
那么,对于vector的模拟实现和string的实现就有所不同了。
✅对于默认构造函数,我们不需要对其进行开辟空间等操作,所以直接在初始化列表给定三个成员变量空指针即可
vector()
:_start(nullptr)
,_finish(nullptr)
,_endOfStorage(nullptr)
{}
✅对于给定长度和值的构造函数,我们需要开辟空间,然后把val都填进去,但是这里对于开辟空间的操作,我们复用reserve即可,这样如果出现某些需要对开辟空间操作的修改,只需要修改reserve即可,提高了代码的可维护性
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
✅对于迭代器区间的初始化,按照迭代器的自增行为遍历整个迭代器区间,并将每个元素尾插到vector中。
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
✅对于拷贝构造,我们同样有经典写法和现代写法两种,由于现代写法的可读性更优,所以这里对于经典写法我们就不再赘述。现代写法,我们的想法是找到一个“工具人”tmp来帮我们构造,这里用到了迭代器区间初始化的方式来初始化tmp,然后再通过swap函数将tmp与this指针指向的对象进行交换从而实现了拷贝构造。
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{
vector<T> tmp(v.begin(), v._end());
swap(tmp);
}
✅对于析构函数,我们要做的事情就是释放new申请的资源,然后将成员变量置空即可。
~vector()
{
delete[] _start;
_start = _finish = _endOfStorage = nullptr;
}
✅对于赋值重载,我们同样找到了一个“工具人”tmp,在传参的过程中,使用传值的方式,构建了一个变量tmp,然后使用在vector类内部实现的swap交换this指针指向的类和tmp。
vector<T>& operator=(vector<T> tmp)
: _start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{
swap(tmp);
return *this;
}
vector和string的底层都是动态数组,所以对于vector类来说,原生指针也能够很好的支持迭代器行为。这里我们只实现正向迭代器的const和非const版本。
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
由于vector和string的成员变量不同,所以这里我们的size和capacity不能直接得到,需要进行运算
//按照图示的理解,由于指向数组的指针都是前闭后开,所以直接使用指针相减即可
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _endOfStorage - _start;
}
如果我们做了扩容操作,就一定是异地扩容,所以对于_finish和_endOfStorage的处理就要注意一下,提前将size保存一下,否则如果在_start重新赋值之后再调用size就会出现问题。
void reserve(size_t n)
{
if (capacity() < n)
{
size_t OldSize = size();
T* tmp = new T[n];
memcpy(tmp, _start, sizeof(T) * size());
delete[] _start;
_start = tmp;
_finish = _start + OldSize;
_endOfStorage = _start + n;
}
}
那么,这样做了修改之后是不是就没有问题了呢?
这里注意一下,我们写的vector是一个类模板,其中存放的元素类型可以是任意类型,如果这里vector中存放的元素类型是需要深拷贝的,例如vector中存放的是string对象的时候,结构如下图。
按照我们刚刚实现的逻辑,首先开辟一块新的空间,然后按照值拷贝的方式将vector中存放的元素拷贝到tmp中,然后调用vector的析构函数,析构的时候对于string对象,自动调用string的析构函数,将string指向的空间,即上述的绿色部分的空间释放,然后导致新开辟的空间中的string对象全部都无效。最终在该vector对象生命周期结束时,再次调用析构函数,导致其中的每个string对象都析构两次,程序崩溃(或者在结束之前对其中的stirng对象做操作,导致出现其他问题使程序崩溃)
那么应该怎么解决这个问题呢?
不能调用库函数memcpy执行值拷贝,而是自己重新写一个拷贝的操作
void reserve(size_t n)
{
if (capacity() < n)
{
size_t OldSize = size();
T* tmp = new T[n];
//memcpy(tmp, _start, sizeof(T) * size());
if (_start)
{
for (size_t i = 0; i < OldSize; ++i)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + OldSize;
_endOfStorage = _start + n;
}
}
这里resize的用法是和之前string中resize的用法相同,所以设计思路也是相同的,一共分成三种情况:
void resize(size_t n, const T& val = T())
{
if (n > capacity())//n>capacity
{
reserve(n);
}
if (n > size())//size < n < capacity
{
while (*_finish > _size + n)
{
*_finish = val;
++_finish;
}
}
else
_finish = _start + n;
}
这里补充一下,在上面的代码中,可以看到给val的缺省值是
T()
,而不是一个确定的值,这是因为存在vector中的元素可以是任意类型,有可能是自定义类型,如果使用某一个值可能无法构造出该类型的对象,所以使用T()构建一个T类型的匿名对象。❓对于内置类型,怎么会有默认构造函数呢?
✅在C++中,为了支持这种写法,对于内置类型,也支持了使用
类型()
的方式创建对象
针对数据访问,我们同样只实现一个[]的重载即可
T& operator[](size_t pos)//const版本
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const//非const版本
{
assert(pos < size());
return _start[pos];
}
老规矩,首先判断容量,如果已经没有容量插入就先扩容,然后在_finish指向的位置插入值,然后让_finish向后挪动一位
void push_back(const T& val)
{
if (_finish == _endOfStorage)
{
size_t newCapacity = capacity() == 0 ? 4 : 2 * capacity();
reserve(newCapacity);
}
*_finish = val;
++_finish;
}
显而易见,直接使_finish向前挪动一位即可,但是最好首先进行一下判空,这里顺便也实现以下empty
bool empty()
{
return _start == _finish;
}
void pop_back()
{
if (!empty())
{
--_finish;
}
}
对于insert,我们在上一篇博客中讲到了迭代器失效问题,给出的解决办法就是设定返回值类型为迭代器,所以这里对insert的设计就很清晰了。
iterator insert(iterator pos, const T& val)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _endOfStorage)
{
size_t len = pos - _start;//这里注意以下,要保存pos的相对位置,否则扩容之后会导致pos位置失效
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
pos = _start + len;//重新确定pos位置
}
//挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = val;
++_finish;
return pos;
}
同样的erase也会导致迭代器失效,所以最终我们也重新返回一个有效的pos值
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator begin = pos + 1;
while (begin < _finish)
{
*(begin - 1) = *begin;
++begin;
}
--_finish;
return pos;
}
由于vector和string一样,底层都是动态数组,所以swap的形式也非常像
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endOfStorage, v._endOfStorage);
}
clear的作用就是将数据清空,但是不销毁对象,所以直接改变_finish即可
void clear()
{
_finish = _start;
}