STL之vector

Posted randyniu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STL之vector相关的知识,希望对你有一定的参考价值。

vector是我用的最多的容器之一,非常的好用。在做leetcode的时候,用的也很多,现在来剖析一下其源码的实现逻辑。

array和vector唯一的区别在于空间的灵活使用。

vector提供的迭代器就是原生指针类型。

vector其实就是3个指针变量,start, finish, end_of_storage指针变量,因此一个vector的大小应该为12byte。

vector是会生长的,当当前空间不足以满足将要放下的数据,将会重新找到一块连续的空间用来存放元素,这就涉及到了3个步骤,重新配置,元素移动,释放原来的空间,生长总是痛苦的,容器中可以生长也就是vector和dqueue了,但是和dqueue相比,vector的成长会更加痛苦。

vector的构造与内存管理

protected:
  allocator_type _M_data_allocator;//配置器类型
  _Tp* _M_start;//内存空间起始地址
  _Tp* _M_finish;//已使用空间结束地址
  _Tp* _M_end_of_storage;//可用空间结束地址

  _Tp* _M_allocate(size_t __n)//配置空间 是调用空间配置器来进行元素大小的分配。
    { return _M_data_allocator.allocate(__n); }
  void _M_deallocate(_Tp* __p, size_t __n)//释放空间
    { if (__p) _M_data_allocator.deallocate(__p, __n); }
};
explicit vector(const allocator_type& __a = allocator_type())
    : _Base(__a) {}//默认构造函数

  vector(size_type __n, const _Tp& __value,
         const allocator_type& __a = allocator_type()) 
    : _Base(__n, __a)//构造函数,里面包含n个初始值为value的元素
    //全局函数,填充值函数,即从地址M_start开始连续填充n个初始值为value的元素
    { _M_finish = uninitialized_fill_n(_M_start, __n, __value); }

  explicit vector(size_type __n)//该构造函数不接受初始值,只接受容易包含元素的个数n
    : _Base(__n, allocator_type())
    { _M_finish = uninitialized_fill_n(_M_start, __n, _Tp()); }

  vector(const vector<_Tp, _Alloc>& __x) 
    : _Base(__x.size(), __x.get_allocator())//拷贝构造函数
{ _M_finish = uninitialized_copy(__x.begin(), __x.end(), _M_start); }

将元素插入到尾部的时候,首先检查是否还有备用空间,如果有就直接在备用空间上面构造元素,并调整finish,是vector变大,如果没有备用空间,就需要扩充空间,重新配置,移动元素,释放原空间。

  void push_back(const _Tp& __x) {//在最尾端插入元素
    if (_M_finish != _M_end_of_storage) {//若有可用的内存空间
      construct(_M_finish, __x);//构造对象
      ++_M_finish;
    }
    else//若没有可用的内存空间,调用以下函数,把x插入到指定位置
      _M_insert_aux(end(), __x);
}

vector<_Tp, _Alloc>::_M_insert_aux(iterator __position, const _Tp& __x)
{
  if (_M_finish != _M_end_of_storage) {//若有可用的内存空间 处理的是在中间插入一个元素这一种情况
    construct(_M_finish, *(_M_finish - 1));//在备用空间起始处构造一个元素,并以vector最后一个元素值为其初始值
    ++_M_finish;//调整迭代器的位置
    _Tp __x_copy = __x;
    /*函数原型:
    template< class BidirIt1, class BidirIt2 >
    BidirIt2 copy_backward( BidirIt1 first, BidirIt1 last, BidirIt2 d_last );
    {
        while (first != last) 
        {
            *(--d_last) = *(--last);//反序复制数据
        }
        return d_last;
    }
    功能:把[first,last)范围的数据复制到d_last之前的空间
    */
    copy_backward(__position, _M_finish - 2, _M_finish - 1);//相当于把[position,finish-2]的数据向后以为覆盖数据
    *__position = __x_copy;//把x插入到指定的位置
  }
  else {//若不存在可用的内存空间,重新分配空间,使其满足要求
    const size_type __old_size = size();//保存原来的内存空间大小
    /*若原始空间不等于0,则新分配的空间为原来的两倍
    *若原始空间为0,则新分配的空间为1
    */
    const size_type __len = __old_size != 0 ? 2 * __old_size : 1;
    iterator __new_start = _M_allocate(__len);//分配大小为len的内存空间
    iterator __new_finish = __new_start;
    __STL_TRY {
        //把[start,position)的原始数据复制到新的内存空间
      __new_finish = uninitialized_copy(_M_start, __position, __new_start);
      construct(__new_finish, __x);//把x值插入到指定的位置
      ++__new_finish;
      //复制[position,finish)原始剩余数据到新的空间
      __new_finish = uninitialized_copy(__position, _M_finish, __new_finish);
    }
    __STL_UNWIND((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;
  }
}

在这里其实考虑过,为什么不尝试使用给一个realloc这个函数调用,如果后面的空间扩展足够两倍,这一次不久不需要进行大量的拷贝移动构造析构了么?也是不知道为什么,可能是工程经验吧。

  void pop_back() {//取出最尾端元素
    --_M_finish;
    destroy(_M_finish);//析构对象
  }
  iterator erase(iterator __position) {//擦除指定位置元素
    if (__position + 1 != end())
      copy(__position + 1, _M_finish, __position);//后续元素前移一位
    --_M_finish;
    destroy(_M_finish);//析构对象
    return __position;
  }
  iterator erase(iterator __first, iterator __last) {//擦除两个迭代器区间的元素
    iterator __i = copy(__last, _M_finish, __first);//把不擦除的元素前移
    destroy(__i, _M_finish);//析构对象
    _M_finish = _M_finish - (__last - __first);//调整finish的所指的位置
    return __first;
  }

  void resize(size_type __new_size, const _Tp& __x) {//改变容器中可存储的元素个数,并不会分配新的空间
    if (__new_size < size()) //若调整后的内存空间比原来的小
      erase(begin() + __new_size, end());//擦除多余的元素
    else
      insert(end(), __new_size - size(), __x);//比原来多余的空间都赋予初值x
  }
  void resize(size_type __new_size) { resize(__new_size, _Tp()); }
void clear() { erase(begin(), end()); }//清空容器

上面这些函数都很容易理解,就是找到位置,构造对象,移动指针,析构对象这些过程,是挺细腻的内容。

下面讲一下插入函数。

  iterator insert(iterator __position, const _Tp& __x) {//把x值插入到指定的位置
    size_type __n = __position - begin();//找到位置
    if (_M_finish != _M_end_of_storage && __position == end()) {//还有空间并且位置是在尾部
      construct(_M_finish, __x);//直接在尾部进行构造
      ++_M_finish;
    }
    else //调用之前的那个辅助函数
      _M_insert_aux(__position, __x);
    return begin() + __n;
  }
  iterator insert(iterator __position) {
    size_type __n = __position - begin();
    if (_M_finish != _M_end_of_storage && __position == end()) {
      construct(_M_finish);
      ++_M_finish;
    }
    else
      _M_insert_aux(__position);
    return begin() + __n;
  }

  void insert (iterator __pos, size_type __n, const _Tp& __x)
    { //在pos位置连续插入n个初始值为x的元素
        _M_fill_insert(__pos, __n, __x); }
 
template <class _Tp, class _Alloc>
void vector<_Tp, _Alloc>::_M_fill_insert(iterator __position, size_type __n, 
                                         const _Tp& __x)
{
  if (__n != 0) {//当n不为0,插入才有效 这种判断是很有必要的逻辑判断
    if (size_type(_M_end_of_storage - _M_finish) >= __n) {//若有足够的可用空间,即备用空间不小于新插入元素个数
      _Tp __x_copy = __x;
      const size_type __elems_after = _M_finish - __position;//计算插入点之后的现有元素个数
      iterator __old_finish = _M_finish;

      //case1-a:插入点之后的现有元素个数大于新插入元素个数
      if (__elems_after > __n) {
        uninitialized_copy(_M_finish - __n, _M_finish, _M_finish);//把[finish-n,finish)之间的数据复制[finish,finish+n)
        _M_finish += __n;//调整迭代器finish所指的位置
        copy_backward(__position, __old_finish - __n, __old_finish);//把[position,old_finish-n)之间的数据复制[old_finish-n,old_finish)
        fill(__position, __position + __n, __x_copy);//在指定位置(插入点)填充初始值
      }

      //case1-b:插入点之后的现有元素个数不大于新插入元素个数
      else {
        uninitialized_fill_n(_M_finish, __n - __elems_after, __x_copy);//先在可用空间填入n-elems_after个初始值x
        _M_finish += __n - __elems_after;//调整迭代器finish
        uninitialized_copy(__position, __old_finish, _M_finish);//把[position,old_finish)之间的数据复制到[old_finish,finish)
        _M_finish += __elems_after;
        fill(__position, __old_finish, __x_copy);
      }
    }

    //case2:若备用空间小于新插入元素个数
    else {//若备用空间小于新插入元素个数,则分配新的空间
        //并把原始数据复制到新的空间,调整迭代器
      const size_type __old_size = size(); //获取原始空间的大小  
      //新的空间为旧空间的两倍,或为旧空间+新增长元素个数
      const size_type __len = __old_size + max(__old_size, __n);
      //配置新的空间
      iterator __new_start = _M_allocate(__len);
      iterator __new_finish = __new_start;
      __STL_TRY {//把插入点之前的原始数据复制到新的空间
        __new_finish = uninitialized_copy(_M_start, __position, __new_start);
        //将新加入数据添加在[new_finish,new_finish+n)
        __new_finish = uninitialized_fill_n(__new_finish, __n, __x);
        //将插入点之后的原始数据复制到新空间
        __new_finish
          = uninitialized_copy(__position, _M_finish, __new_finish);
      }
      //释放原来空间的对象和内存
      __STL_UNWIND((destroy(__new_start,__new_finish), 
                    _M_deallocate(__new_start,__len)));
      destroy(_M_start, _M_finish);
      _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;
    }
  }
}

插入函数时进行转调用的,首先判断数量是不是等于0,如果是,就什么也不做,否在才进行插入。

判断剩余空间是否够用,如果够用则计算插入点之后现有元素的个数。

如果插入点之后现有元素个数大于新增元素的个数,则对后面的元素进行构造初始化,然后调用copy_backword函数将内容数据赋值到后边,在对插入点位置进行填充

如果插入点之后的元素个数小于新增元素的个数,则对后面新增的元素用x进行填充,然后讲前面的元素拷贝到后面,在对中间的元素进行填充赋值。这些完全都是从性能上看的考量。假设新增的元素数量是远远大于当前位置后面元素的数量,这样做可以直接构造成为你需要的具体元素,减少了调用构造函数的次数。

如果空间不满足, 则重新分配,元素移动,释放原空间。

先用x进行构造,然后在将前面的元素用之前的元素进行替换,这样就可以了,别忘了最后更新三个参数。

vector的操作大概就这样,还是很容易理解的,相比较而言,所有的这些考虑全部都是为了性能的考虑,真可谓是锱铢必较了。

忘了说一点,由于vector是会生长的,因此当你插入元素之后,你之前保留的迭代器就可能会失效了,因此这一点应该尤为注意。

 

以上是关于STL之vector的主要内容,如果未能解决你的问题,请参考以下文章

STL之vector4

STL之vector

STL之vector

超详细STL之基于源码剖析vector实现原理及注意事项

超详细STL之基于源码剖析vector实现原理及注意事项

STL之vector容器详解