vector容器可以说是STL最基本的容器,其实现也可以说是最简单的,但是vector同时也是使用频率最高的STL容器。其实现的动态数组的效果可以广泛的应用在各个层面。
对于传统的数组(array),我们常常不得不因为无法知晓在运行期确定的元素个数而为其配置一个大块头,造成了严重的内存浪费。vector相比于传统数组的改进之处在于vector的空间可以自行扩展,且这种可以自行随着元素的加入,在内部自动实现。在必要时我们甚至可以手动为其重新分配空间,这其中涉及的元素移动、复制、构造操作也使用经过性能有优化的算法实现。在内存配置与元素操作上提供了极大的灵活性,下面我们就分析vector的内部原理。
让我们从三个方向将vector的代码切分,然后逐个击破:
1.构造操作
2.内存操作
3.元素操作
首先是构造操作,vector支持内含构造函数,我们拿出最普遍实现、最有代表性的三个来看下:
template<typename T,typename Alloc=alloc>//使用STL内置的第一层空间分配器
class stl_vector
{
public:
typedef T value_type;
typedef value_type* iterator;
typedef size_t size_type;
...
protected:
iterator start;
iterator finish;
iterator end_of_storage;
public:
stl_vector() :start(nullptr), finish(nullptr), end_of_storage(nullptr){}//default constructor
explicit stl_vector(size_type n){ fill_initialize(n, T()); }
stl_vector(size_type n, const T& value){ fill_initialize(n, value); }
...
};
Vector使用指向元素的指针类型为迭代器,这也是我们平时遍历数组的手段。(vector 因此免去了设计迭代器的任务)
我们再vector中维护三个表示界限指针:指向头部的指针(start),指向当前已使用空间尾部的指针(finish)(如果容器中没有元素,则finish==start),指向可用空间尾部(end_of_storage)。
这里我们要注意:关于STL迭代器的操作,全部都满足前开后闭的规则。因此,finish指向最后一个元素的下一个位置,所以我们将finish形容为指向已使用空间的尾部,而不是最后一个元素。
第一个构造函数为默认构造函数,将界限指针全部设为空指针(nullptr)。
第二个构造函数接受一个代表元素个数的形参,其分配n个元素的空间,并将元素初始化为默认值。(我们将其设置explicit,使之不受隐式类型转换的困扰)
第三个构造函数接受一个代表元素个数的形参,还接受一个代表元素初始值的形参,其分配容纳n个元素的空间,并将元素值初始化。
后两个构造函数的具体实现雷同,都是分配空间,然后再进行元素构造。为了避免代码重复,我们另设置一个fill_initialize函数来执行这些相同的操作:
template<typename T, class Alloc>
void stl_vector<T, Alloc>::fill_initialize(size_t n, const T& value)
{
start = allocate_and_fill(n, value);
finish = start + n;
end_of_storage = finish;
}
可以看到,fill_initialize内部使用了一个allocate_and_fill函数,这个函数的在堆区域配置一块n个T类型(元素类型)大小的区域,并使用值value填充。接着,将界限指针指向正确的位置。担当重任的allocate_and_fill如何做的呢?这就引领我们进去下一个区域:vector的内存操作
//配置空间并填满
template <typename T, class Alloc>
typename stl_vector<T, Alloc>::iterator stl_vector<T, Alloc>::allocate_and_fill(size_t n, const T& value)
{
iterator result = data_allocator::allocate(n);
uninitialized_fill_n(result, n, value);//全局函数 在未初始化空间中构造对象
return result;
}
allocate_and_fill也是个次级函数,其将配置空间与构造对象打包在一起。配置空间我们使用STL内置的第一级空间分配器,而构造操作由uninitialized_fill_n完成: //在未初始化空间中构造对象
template <typename ForwardIterator, typename T, typename Size>
ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n, const T& value)
{//中转函数,传递迭代器所指对象的类型信息
typedef typename stl_iterator_traits<ForwardIterator>::value_type value_type;
return _uninitialized_fill_n(first, n, value, value_type());
}
template < typename ForwardIterator, typename T, typename Size, typename T1 >
ForwardIterator _uninitialized_fill_n(ForwardIterator first, Size n, const T& value, T1)
{//中转函数,传递迭代器所指对象类型是否为Plain Old Data的信息
typedef typename stl_type_traits<T1>::is_POD_type is_POD;
return _uninitialized_fill_n_aux(first, n, value, is_POD());
}
template <typename ForwardIterator, typename T, typename Size>
ForwardIterator _uninitialized_fill_n_aux(ForwardIterator first, Size n,
const T& value, _true_type)
{//是Plain Old Data 使用全局函数fill_n为对象赋值
return fill_n(first, n, value);
}
template <typename ForwardIterator, typename T, typename Size>
ForwardIterator _uninitialized_fill_n_aux(ForwardIterator first, Size n,
const T& value, _false_type)
{//非Plain Old Data ,使用构造函数构造对象
for (; n > 0; --n, ++first)
construct(&*first, value);
return first;
}
template <typename OutputIterator, typename Size, typename T>
OutputIterator fill_n(OutputIterator first, Size n, const T& value)
{
for (; n > 0; --n, ++first)
*first = value;
return first;
}
在内存构造层面,vector还有一个非常重要的操作,其是vector作为动态数组的核心:溢出插入操作。
vector的溢出插入操作实现了当备用空间(end_of_storage-finish)不足以容纳待插入的元素时所进行的内存空间扩充。
因为vector一般情况下的内存扩充行为只有在向vector中插入元素从而导致现有空间不足时才触发(另外的情况是用户手动使用resize重新配置空间),所以将其于插入操作放置在一起:
//溢出插入操作
template <typename T, typename Alloc>
void stl_vector<T, Alloc>::insert(iterator position, size_type n, const T& value)
{
if (n > 0)
{//如果备用空间足够
if (end_of_storage - finish >= n)
{
const size_type elem_after = finish - position;
iterator old_finish = finish;
if (elem_after > n)
{//如果插入点之后元素个数大于插入元素个数
uninitialized_copy(finish, finish - n, finish);
finish += n;
copy_backward(position, old_finish - n, old_finish);
fill(position, position + n, value);
}
else
{//如果插入点之后元素个数小于插入元素个数
uninitialized_fill_n(finish, n - elem_after, value);
finish += n - elem_after;
uninitialized_copy(position, old_finish, finish);
finish += elem_after;
fill(position, old_finish, value);
}
}
else
{//备用空间不足,需要重新分配空间
const size_type old_size = size();
const size_type new_size = old_size + max(old_size, n);
iterator new_start = data_allocator::allocate(new_size);
iterator new_finish = new_start;
try
{
/*元素转移分为三步
1.复制插入点之前的元素到新空间
2.填充插入元素
3.复制插入点之后的元素到新空间
*/
new_finish = uninitialized_copy(start, position, new_start);
new_finish = uninitialized_fill_n(new_finish, n, value);
new_finish = uninitialized_copy(position, finish, new_finish);
}
catch (...)
{
/*实现"异常安全机制"如果以上三个操作抛出异常,将析构所有新构造的对象,再
释放新空间,即所谓"commit and rollback”
*/
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, new_size);
throw;
}
destroy(start, finish);
data_allocator::deallocate(start, end_of_storage-start);//释放旧空间
//调整界限,这是插入之前的迭代器全部失效的原因
start = new_start;
finish = new_finish;
end_of_storage = new_start + new_size;
}
}
为什么要根据插入点之后的元素个数与插入元素个数的差值划分两种情况呢?
这是因为当插入点之后的元素个数小于插入元素个数时,我们就不能使用原来的操作方法了,因为插入点之后的元素个数不够,拿不出插入元素个数的元素来调整finish的位置。所以需要分情况讨论。
刚才提到用户可以手动使用resize函数重新配置空间:
template <typename T, class Alloc>
void stl_vector<T, Alloc>::resize(size_type new_size, const T& value)
{
if (new_size < size())
erase(start + new_size, finish);
else
insert(finish, new_size - size(), value);
}
template <typename T, class Alloc>
void stl_vector<T, Alloc>::resize(size_type new_size){ resize(new_size, T()); }
reszie实际也使用了溢出插入操作来实现。
3.元素操作
vector支持的元素操作大部分都是常规操作。但要注意:虽然STL在定义了全局的头部插入(push_front)操作,但我们不应将其应用于vector中,因为对于一个数组来说(即使是动态数组),从头部插入元素将导致其余所有元素的移动操作,其复杂度是爆炸性的。如果我们需要使用大量头部插入操作,我们可以使用另一种STL容器:list。其在内部维护一个链表,对于任意位置上的插入,list可以在常数时间内完成。
下面是vector支持的部分元素操作实现;
template<typename T,typename Alloc=alloc>
class stl_vector
{
...
typedef T value_type;
typedef value_type* iterator;
typedef const value_type* const_iterator;
typedef value_type* pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef size_t size_type;
public:
iterator begin(){ return start; }
iterator end(){ return finish; }
size_type capacity()const{ return size_type(end_of_storage - start); }
reference operator[](size_type n){ return *(start + n); }
reference front(){ return *start; }
reference back(){ return *(finish - 1); }
void push_back(const T&);
void pop_back();
iterator erase(iterator);
iterator erase(iterator, iterator);
const_iterator begin()const{ return start; }
const_iterator end()const { return finish; }
const_reference operator[](size_type n)const{ return *(start + n); }
bool empty()const{ return start == finish; }
size_type size()const{ return size_type(finish - start); }
const_reference front()const{ return *start; }
const_reference back()const{ return *(finish - 1); }
...
};
template<typename T, class Alloc>
void stl_vector<T, Alloc>::push_back(const T& x)
{
if (finish != end_of_storage)
{
construct(finish, x);
++finish;
}
else
insert_aux(finish, x);
}
template<typename T, class Alloc>
void stl_vector<T, Alloc>::pop_back()
{
if (!empty())
{
--finish;
destroy(finish);
}
else
throw("vector empty before pop");
}
template<typename T, class Alloc>
typename stl_vector<T, Alloc>::iterator stl_vector<T, Alloc>::erase(iterator position)
{
if (position + 1 != finish)
copy(position + 1, position, finish);
--finish;
destroy(finish);
return position;
}
template <typename T, class Alloc>
typename stl_vector<T, Alloc>::iterator stl_vector<T, Alloc>::erase(iterator first, iterator last)
{
iterator i = copy(last, finish, first);
destroy(i, finish);
finish = finish - (last - first);
return first;
}
其中push_back使用的insert_aux为溢出插入操作的特化版本:
template <typename T, typename Alloc>
void stl_vector<T, Alloc>::insert_aux(iterator position, const T& value)
{
//还有备用空间
if (finish != end_of_storage)
{
construct(finish - 1, *finish);
++finish;
copy_backward(position + 1, finish - 2, finish - 1);
*position = value;
}
else
{
const size_type old_size = size();
const size_type len = old_size != 0 ? (old_size * 2) : 1;
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
try{
new_finish = uninitialized_copy(start, position, new_start);
construct(&*new_finish, value);
++new_finish;
new_finish = uninitialized_copy(position, finish, new_finish);
}
catch (...)
{
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
start = new_start;
finish = new_finish;
end_of_storage = start + len;
}
}