一.vector的定义与结构
vector的数据结构是一个简单的线性连续空间,其中有三个迭代器分别指向连续空间中已用空间的起始和结尾以及连续空间的结尾,它们是:_M_start,_M_finish,_M_end_of_storage。vector的定义摘要部分如下:
template<class _Tp, class _Alloc>
class _Vector_base{
public:
typedef _Alloc allocator_type;
// 返回所使用配置器类型的一个对象
allocator_type get_allocator() const { return allocator_type(); }
_Vector_base(const _Alloc&)
: _M_start(0), _M_finish(0), _M_end_of_storage(0) {}
_Vector_base(size_t __n, const _Alloc&)
: _M_start(0), _M_finish(0), _M_end_of_storage(0)
{
_M_start = _M_allocate(__n); // 调用配置器分配空间
_M_finish = _M_start;
_M_end_of_storage = _M_start + __n; // 指向容器尾部
}
protected:
_Tp* _M_start; // 已用空间首部迭代器
_Tp* _M_finish; // 已用空间尾部迭代器
_Tp* _M_end_of_storage; // 申请空间尾部迭代器
typedef simple_alloc<_Tp, _Alloc> _M_data_allocator;
_Tp* _M_allocate(size_t n) {
return _M_data_allocator::allocte(n); //simple_alloc接口会转而调用一级或二级配置器,并返回首地址
}
void _M_deallocate(_Tp* __p, size_t __n) {
_M_data_allocator::deallocate(__p, __n);
}
};
//【注】:由SGI STL 中的定义:
// - typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
// - define __STL_DEFAULT_ALLOCATOR(T) alloc
// 可知默认使用第二级配置器(当小于128字节时第二级会调用一级配置器)
template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc> {
private:
typedef typename _Base::allocator_type allocator_type;
...
public:
typedef value_type* iterator;
// 反向迭代器,定义于stl_iterator.h中
typedef reverse_iterator<iterator, value_type, reference, difference_type> reverse_iterator;
vector(const allocator_type& __a = allocator_type()) : _Vector_base<_Tp, _Alloc>(__a) {}
iterator begin() { return _M_start; }
// - 用_Tp*初始化反向迭代器
// - rend()返回的反向迭代器指向*_M_start
reverse_iterator rend() { return reverse_iterator(begin()); }
...
};
二.vector的内存管理
要说明vector的内存管理,就先从vector的内存构造开始说起吧。以下是vector中的一个构造函数,接受一个迭代器所指的区间作为参数:
vector(const _Tp* __first, const _Tp* __last, const allocator_type& __a = allocator_type())
: _Vector_base<_Tp, _Alloc>(__last - __first, __a)
{ _M_finish = uninitialized_copy(__first, __last, _M_start); }
在构造时vector首先在基类_Vector_base的构造函数中调用了空间配置器申请了一块大小为_last - _first的内存空间,注意这块空间是未初始化的,接下来将[_first, _last)区间内的对象调用uninitialized_copy函数(拷贝调用参:《STL 空间配置器(三)》)拷贝到vector的内存空间中。
接下来再思考当插入数据时vector是如何处理内存不够问题的,即如何进行内存扩充的,我们可以通过观察push_back函数的实现来窥得这一点。
void push_back(const _Tp& __x) {
if (_M_finish != _M_end_of_storage) { // 如果还有剩余内存空间,则在_M_finish位置上构造对象
construct(_M_finish, __x);
++_M_finish;
}
else
_M_insert_aux(end(), __x); // 否则调用_M_insert_aux函数
}
template <class _Tp, class _Alloc>
void vector<_Tp, _Alloc>::_M_insert_aux(iterator __position, const _Tp& __x)
{
if(_M_finish != _M_end_of_storage) { // 进行元素后移
// 想想为什么要进行这一步,二不是直接后移
construct(_M_finish, *(_M_finish - 1));
++_M_finish;
_Tp __x_copy = __x;
// 将区间[position, _M_finish - 2)的内容反向拷贝到以_M_finish - 1结尾的区间
copy_backword(position, _M_finish - 2, _M_finish - 1);
*__position = x;
}
else { // 已无备用空间
const size_type __old_size = size();
const size_type __len = __old_size != 0 ? 2 * __old_size : 1;
iterator __new_start = _M_allocate(__len);
iterator __new_finish = __new_start;
try{
// 分两步:从_M_start到__position, 从 ___position 到 _M_finish进行拷贝到新内存
__new_finish = uninitialized_copy(_M_start, __position, __new_start);
construct(__new_finish, __x);
++__new_finish;
__new_finish = uninitialized_copy(__position, _M_finish, __new_finish);
}
catch(...){
// 若拷贝过程出现错误,则析构掉新内存中已构造的对象,并回收新内存区。
destroy(__new_start,__new_finish),
_M_deallocate(__new_start,__len))
}
destroy(begin(), end()); //析构旧内存区中的对象
_M_deallocate(_M_start, _M_end_of_storage - _M_start);//回收旧内存区
_M_start = __new_start;
_M_finish = __new_finish;
_M_end_of_storage = __new_start + __len;
}
}
从以上源码可看出,当旧的内存区间不足时,会尝试申请一个是旧区间2被大小的新内存区(当然可能比这个小,原因请参照《STL 空间配置器(三)》。然后将旧内存区中的数据拷贝到新内存区(由于vector中迭代器使用的是原始指针,所以如果对象不是POD类型,那么此过程会调用拷贝构造函数,否则调用memmove),由此点,我们编程时对于那些不需要自己实现构造函数的class类型不应该画蛇添足的添加一个构造函数,因为这回影响STL容器以及一些算法的运行效率的运行效率。从上述代码看vector只可以保证_M_start至_M_finish这块内存是初始化过的。
还有一点是在代码中提出的:当前空间足够时为什么要先调用construct(_M_finish, __x),而不是直接使用copy_backward后移?这是因为_M_finish所指的那块区间是还未进行过初始化的,若直接使用copy_backward,那么对于POD类型而言没有太大问题,因为会直接调用memmove,而且POD类型只有默认构造函数,而对于有非默认赋值运算符的类型,会执行行*result = *first这样的语句,其中result指向的是还未经初始化的内存区间,而调用其赋值运算符就更加是一个未定义的事件了。
三.vector的元素操作
关于vector的元素操作没有太多好说的,但是insert函数的实现中有值得商讨的地方。
insert函数的处理方式如下:
1)若vector中的剩余大小足够存放插入的元素则分为以下两种情况进行处理
if (插入点之后的元素个数:after_element > 要插入的元素个数:n) {
总共要移动的个数是插入点之后的元素个数;
而对要移动的这部分元素分为两部分进行移动;
第一部分:靠后的n个元素要被移动到未被初始化过的内存区,因此调用uninitialized_copy()函数来移动这n个元素。
第二部分:插入点之后的 after_element - n个元素也需要向后移动n个位置,可是这n个位置是已被初始化过的内存区。因此STL采用
了copy_backward()来进行拷贝,这样做的原因是使用uninitialized_copy函数来操作以被初始化过的内存区间会造成构造函数
与析构函数的调用
次数不相等,这样可能会造成内存泄露等问题。
最后使用参数x来填充插入点后的n个元素。
}
else{//插入点之后的元素个数:after_element <= 要插入的元素个数:n
新元素若插入后在内存区间结尾应该是_M_finish + n - after_element;
因此先调用uninitialized_fill函数_M_finish处填充n - after_element个新元素。
再调用uninitialized_copy将插入节点后的after_element个旧数据移到_M_finish + n - after_element之后
【注意】由于上述操作的目的区间都是未初始化的,因此只能调用uninitialized_xxx函数
最后一一步是填充已被拷贝过的在插入节点后的after_element个内存区,由于这块区间是已被初始化过的,因此只能调用fill
}
2)若备用空间不足,则进行扩充并拷贝和填充,过程相似于前面提到的_M_insert_aux函数,此处不再赘述