目录
vector简述以及各函数接口总览
简单介绍
vector成员变量
模拟实现总览
模拟实现过程
迭代器相关函数
容量和大小相关函数
size和capacity
reserve
resize
empty
内容修改函数
push_back
pop_back
insert
erase
swap
operator[]
默认成员函数
构造函数1
构造函数2
构造函数3
拷贝构造(传统实现方式)
拷贝构造(现代写法)
赋值运算符重载
析构函数
vector是表示大小可以更改的数组的序列式容器。
就像数组一样,vector为其元素使用连续的存储位置,这也就意味着可以使用指向其元素的常规指针的偏移量来访问它们的元素,使用这种访问方式的速度基本上接近数组。但与数组不同的是,vector的大小支持动态变化(它们的存储可由容器自动处理)。
在vector内部中,vector会使用动态分配的数组来存储其元素。为了在插入新元素时增加大小可能需要重新分配此数组,vector将会开辟一个新数组并将所有元素移动到其中。就处理时间而言,这是一项相对昂贵的任务,因此,每次将元素添加到容器时,vector不会每一个次都重新分配。简单来说,vector只有在扩容时会重新开辟空间拷贝数据,并且vector没有缩容操作。
vector可能会分配一些额外的存储空间以适应可能的增长,因此,容器的实际容量可能大于包含其元素所需的存储量。即vector中的capacity和size。vector与数组相比会消耗更多的内存,以换取管理存储和以高效方式动态增长的能力。
先看一看vector的源码,我们再进行模拟实现。
在vector当中有三个成员变量_start、_finish、_endofstorage。
与string不同的是,vector使用了三个迭代器。
//使用命名空间避免与库中的vector冲突
namespace my_vector
{
//模拟实现vector
template
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
//默认成员函数
vector(); //构造函数
vector(size_t n, const T& val); //构造函数
template
vector(InputIterator first, InputIterator last); //构造函数
vector(const vector& v); //拷贝构造函数
vector& operator=(const vector& v); //赋值运算符重载函数
~vector(); //析构函数
//迭代器相关函数
iterator begin();
iterator end();
const_iterator begin()const;
const_iterator end()const;
//容量和大小相关函数
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& v);
//访问容器相关函数
T& operator[](size_t i);
const T& operator[](size_t i)const;
private:
iterator _start; //指向容器的头
iterator _finish; //指向有效数据的尾
iterator _endofstorage; //指向容器的尾
};
}
为了方便实现下面的默认成员函数的现代写法,我们需要先实现迭代器相关函数,以及获取vector的容量和大小的函数,实现过程也较为简单。
//迭代器相关函数
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;
}
//容量和大小
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstorage - _start;
}
在上文中对于vector的简介中,我已经提到了vector只扩容不缩容的特性。因此在实现reserve函数时我们也遵守这个原则。
void reserve(size_t n)
{
//reserve只扩容不缩容
if (n > capacity())
{
size_t oldSize = size(); //记录扩容前vector中有效数据的个数
T* tmp = new T[n]; //开辟出可以容纳n个数据的空间
if (_start) //判断是否为空
{
//memcpy(tmp, _start, sizeof(T)*oldSize);memcpy无法处理自定义类型
for (size_t i = 0; i < oldSize; ++i) //将容器当中的数据一个个拷贝到tmp当中
{
tmp[i] = _start[i];
}
//释放旧空间
delete[] _start;
}
//_start指向新空间
_start = tmp;
_finish = tmp + oldSize;
_endofstorage = _start + n;
}
}
对于reserve函数有以下问题需要注意:
拷贝容器当中的数据时,不能使用memcpy函数进行拷贝
例如:当vector中存储的中string的时候,使用memcpy拷贝好之后释放原容器空间,原容器中存储的每个string在释放的时候会调用string的析构函数,将其指向的字符串也会进行释放,所以如果使用memcpy拷贝,那么vector中每个元素指向的空间就是一块已经被释放的空间,访问该容器的时候也就是对非法空间的访问。
有了上面的reserve我们实现resize就很简单了。实现resize可以复用reserve,resize也要遵守不缩容的原则,并且一般有下面三种情况。
void resize(size_t n,T val=T())
{
//判断是否需要扩容,遵守不缩容原则
if (n > capacity())
{
reserve(n);
}
//加数据
if (n > size())
{
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
else
{
//删除数据
_finish = _start + n;
}
}
缺省值不能直接写成T val = 0 ,因为T模板可能是任意类型,如果T是一个自定义的类就不能简单地用0去初始化。 应该写成T val = T() :生成一个匿名对象去初始化 (因此一个类最好要提供默认构造,否则这里就不方便使用了) 。
判断vector是否为空
bool empty()const
{
return _start == _finish;
}
要尾插数据首先得判断容器是否已满,若已满则需要先进行增容,然后将数据尾插到_finish指向的位置,再将_finish++即可 。(注意要使用传引用传参)
void push_back(const T& x) //传引用:对于自定义类型可以提高效率
{
//判断是否需要扩容
if (_finish == _endofstorage)
{
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
*_finish = x;
++_finish;
}
对于尾删,只需要注意空容器不要删除就可以了。
void pop_back()
{
//空容器不能尾删
assert(!empty());
--_finish;
}
C++标准库中vector的insert函数有三个重载函数,我在这里只实现第一种方式。在模拟实现insert的时候我们需要考虑到扩容之后会导致迭代器失效的问题。
iterator insert(iterator pos, const T& val)
{
//首先需要确保插入的位置正确
assert(pos >= _start);
assert(pos < _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
//扩容之后会导致pos迭代器失效,需要重新更新pos
pos = _start + len;
}
//挪动pos后面的数据
iterator end = _finish - 1;
while (end > pos)
{
*(end + 1) = *end;
--end;
}
//在pos位置插入val
*pos = val;
++_finish;
return pos; //返回val位置的迭代器
}
erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器释放为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。
//删除pos位置的数据
iterator erase(iterator pos)
{
assert(!empty()); //容器为空则断言
//将pos位置之后的数据统一向前挪动一位,以覆盖pos位置的数据
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
it++;
}
_finish--; //数据个数减少一个,_finish前移
return pos;
}
swap函数用于交换两个容器的数据,我们可以直接调用库当中的swap函数将两个容器当中的各个成员变量进行交换即可。
//交换两个容器的数据
void swap(vector& v)
{
//交换容器当中的各个成员变量
::swap(_start, v._start);
::swap(_finish, v._finish);
::swap(_endofstorage, v._endofstorage);
}
与string相同,vector也支持【】+下标的方式访问容器中的元素 。注意:当我们使用【】访问容器中的数据时,我们有时需要修改,有时候只需要读取就可以了。因此我们最好实现const和非const两个版本。
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
注意:对于构造函数2和3以及拷贝构造函数的现代实现方法,需要用到的push_back等函数,因此我将默认成员函数的模拟实现放到了最后。
vector首先支持一个无参的构造函数,对于这个无参的构造函数,我们直接将构造对象的三个成员变量都设置为空指针即可。
//构造函数1
vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{}
除此之外,vector还支持其他的构造函数,如下图:
vector还支持使用一段迭代器区间进行对象的构造。因为该迭代器区间可以是其他容器的迭代器区间,也就是说该函数接收到的迭代器的类型是不确定的,所以我们这里需要将该构造函数设计为一个函数模板,在函数体内将该迭代器区间的数据一个个尾插到容器当中即可。
//构造函数2
template //模板函数
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
//将迭代器区间在[first,last)的数据一个个尾插到容器当中
while (first != last)
{
push_back(*first);
first++;
}
}
此外,vector还支持构造这样一种容器,该容器当中含有n个值为val的数据。对于该构造函数,我们可以先使用reserve函数将容器容量先设置为n,然后使用push_back函数尾插n个值为val的数据到容器当中即可。
//构造函数3
vector(size_t n, const T& val)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(n); //调用reserve函数将容器容量设置为n
for (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中
{
push_back(val);
}
}
与string相同,vector的拷贝构造也会涉及到深拷贝的问题,由于未实现push_back函数,因此在这里先仅仅给出传统的实现方式。
//拷贝构造函数
vector(const vector& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
_start = new T[v]; //开辟一块和v大小相同的空间
//将v中的数据一个个拷贝过来
for (size_t i = 0; i < v.size(); i++)
{
//不可以使用memcpy,memcpy只能拷贝内置类型,无法处理自定义类型
_start[i] = v[i]; //如果v中的元素是自定义类型,那么会调用其拷贝构造函数
}
_finish = _start + v.size(); //容器有效数据的尾
_endofstorage = _start + v.capacity(); //整个容器的尾
}
注意: 将容器当中的数据一个个拷贝过来时不能使用memcpy函数,当vector存储的数据是内置类型或无需进行深拷贝的自定义类型时,使用memcpy函数是没什么问题的,但当vector存储的数据是需要进行深拷贝的自定义类型时,使用memcpy函数的弊端就体现出来了。与上面模拟实现reserve时提到的情况基本相同。这里不做过多解释。
有了上面写出的push_back,拷贝构造函数的现代写法也比较简单,使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可。但也要注意,现代写法的效率并不比传统写法高,只是这样写代码更加规范易懂。
//现代写法
vector(const vector& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(v.capacity()); //调用reserve函数将容器容量设置为与v相同
for (auto e : v) //将容器v当中的数据一个个尾插过来
{
push_back(e);
}
}
//现代写法
vector& operator=(vector v) //编译器接收右值的时候自动调用其拷贝构造函数
{
swap(v); //交换这两个对象
return *this; //支持连续赋值
}
~vector()
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}