代码改变世界

C++ Standard Stl -- SGI STL源码学习笔记(07) stl_vector 与 一些问题的细化 3 resize函数剖析

2012-08-03 14:56  respawn  阅读(1810)  评论(0编辑  收藏  举报

  前面在介绍push_back函数的时候有说到placement new的用法.前面说的很简单.这几天处理一些其他的事情,直到昨天下午

才有时间看源码,顺便安静的看一下书. 其中我又看到了挂关于placement new的介绍,那么就在文章开始之前先说一下这个问题.

  placement new又称"定为new"(我想这纯粹是翻译过来的意思.),当在禁止使用Heap分配的时候,也就是声明对象的构造函数

为private的时候,我们不能调用构造函数去构造一个对象,这个时候就可以使用placement new. 前一段时间我在阅读sig stl源码的

时候也看到了stl容器对于placement new的使用.

  placement new 的作用是在一个特定的位置放置一个对象,所以不需要调用构造函数,但是却和构造函数的作用相同. 

需要注意的是placement new并不分配实际的存储区域, 而是返回一个指向已经分配好空间的指针.所以不要对其执行delete操作.

  但是确实创建了一个对象,如果想要销毁对象,那么需要调用嗯对象的析构函数.

       placement大多都是使用在容器技术中,而且是高级技术,也通常用来隐藏技术实现的细节,这里只做简单了解.

 

前面很多文章都是介绍stl_vector,这篇文章会介绍vector的resize函数,并作为结尾. 先看一下resize函数的源码:

 

  void resize(size_type __new_size, const _Tp& __x) {
    if (__new_size < size()) 
      erase(begin() + __new_size, end());    // 擦除begin()+__new_size -> end()之间的元素
    else
      insert(end(), __new_size - size(), __x);
  }
  void resize(size_type __new_size) { resize(__new_size, _Tp()); }  // 这和上面一样,只不过是提供默认的参数.

 

1. 首先第一点很容易看得出,erase函数执行的是擦除工作,并不能分配内存.

2. insert在__new_size >= size()的时候会执行内存的重新分配.

 

先看一下erase的源码:

  iterator erase(iterator __first, iterator __last) {
    iterator __i = copy(__last, _M_finish, __first);
    destroy(__i, _M_finish);
    _M_finish = _M_finish - (__last - __first);
    return __first;
  }

 跟着copy的源码走下去,最终会看到最后的实现是:(__copy_trivial)

template <class _Tp>
inline _Tp*
__copy_trivial(const _Tp* __first, const _Tp* __last, _Tp* __result) {
  memmove(__result, __first, sizeof(_Tp) * (__last - __first));
  return __result + (__last - __first);
}

 其实上面的erase在resize函数中使用的时候是不会执行copy函数的.因为end() == _M_finish.所以只需要将

   begin() + __new_size -> _M_finish之间的元素都销毁.destory源码如下:

inline void _Destroy(_Tp* __pointer) {
  __pointer->~_Tp();  // 这里不是使用delete,而是调用元素对象本身的析构函数.
}

  找了很多层,执行很多跳转最后才找到上面的最终源码,为什么stl要这么麻烦? 因为stl对于容器的内存是使用placement new

  前面说过,所以需要调用对象本身的析构函数来完成工作.

 

3. 接下来,我们看看,如果__new_size > size()执行insert函数的情况.insert函数源码如下:

  void insert (iterator __pos, size_type __n, const _Tp& __x)
    { _M_fill_insert(__pos, __n, __x); }

  下面调用到_M_fill_insert函数,这个函数在前面的文章中有讲解过.不过当时只讲解了该函数的一部分. 本文来看看上半部分. 

template <class _Tp, class _Alloc>
void vector<_Tp, _Alloc>::_M_fill_insert(iterator __position, size_type __n, 
                                         const _Tp& __x)
{
  if (__n != 0) {   // __new_size - size() > 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;
      if (__elems_after > __n) {
        uninitialized_copy(_M_finish - __n, _M_finish, _M_finish);
        _M_finish += __n;
        copy_backward(__position, __old_finish - __n, __old_finish);
        fill(__position, __position + __n, __x_copy);
      }
      else {
        uninitialized_fill_n(_M_finish, __n - __elems_after, __x_copy);
        _M_finish += __n - __elems_after;
        uninitialized_copy(__position, __old_finish, _M_finish);
        _M_finish += __elems_after;
        fill(__position, __old_finish, __x_copy);
      }
    }

  在上面的注释中,只会调用_M_fill_insert函数的前部分,而后半部分的源码在前面讲解过了.

    情况又作为两种子情况划分:

      1. size_type(_M_end_of_storage - _M_finish) >   __n(__n = __new_size - size() )

      2. size_type(_M_end_of_storage - _M_finish) <= __n(__n = __new_size - size())

  

对于这两种情况该如何解释呢?如何去理解. _M_end_of_storage代表着存储能力,_M_finish代表着当前的已存储位置, size()

返回的不是容器的存储能力,而是当前已经存储的元素个数. 理解了这些,上面的两种情况就比较好理解了.  

  

  大的前提条件是:需要resize的新长度已经大于已经存储的长度.

    对于1情况: __new_size在_M_end_of_storage内. 也就是在存储能力内.

    对于2情况: 则不在存储能力范围内了,需要扩大存储能力,也就是扩大存储内存单元.

好吧,我们去看看STL源码是如何处理的.

 

对于情况1:

  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;
	      if (__elems_after > __n) {  // 这下面的语句不会执行,因为_M_finish = _position 
	        uninitialized_copy(_M_finish - __n, _M_finish, _M_finish);
	        _M_finish += __n;
	        copy_backward(__position, __old_finish - __n, __old_finish);
	        fill(__position, __position + __n, __x_copy);
	      }
	      else {  // 从这里开始执行.
	        uninitialized_fill_n(_M_finish, __n - __elems_after, __x_copy);
	        _M_finish += __n - __elems_after;                    // _M_finish 向后移动__n-1
	        uninitialized_copy(__position, __old_finish, _M_finish);   
	        _M_finish += __elems_after;
	        fill(__position, __old_finish, __x_copy);
	      }
	    }

  我们假设容器中存储的元素类型不是POD,所以追溯源码就可以找到这里:

template <class _ForwardIter, class _Size, class _Tp>
_ForwardIter
__uninitialized_fill_n_aux(_ForwardIter __first, _Size __n,
                           const _Tp& __x, __false_type)
{
  _ForwardIter __cur = __first;
  __STL_TRY {
    for ( ; __n > 0; --__n, ++__cur)
      _Construct(&*__cur, __x);
    return __cur;
  }
  __STL_UNWIND(_Destroy(__first, __cur));
}

  很容易看得出,就是从_M_finish到后面的_n个位置都使用placement new初始化元素为__x.

  接着查看uninitialized_copy的源码可以发现并不会执行: 和前面一样,元素类型同样不是POD

template <class _InputIter, class _ForwardIter>
_ForwardIter 
__uninitialized_copy_aux(_InputIter __first, _InputIter __last,
                         _ForwardIter __result,
                         __false_type)
{
  _ForwardIter __cur = __result;
  __STL_TRY {
    for ( ; __first != __last; ++__first, ++__cur)   // _position == old_position , 所以不会执行.
      _Construct(&*__cur, *__first);
    return __cur;
  }
  __STL_UNWIND(_Destroy(__result, __cur));
}

  

   好吧,对于情况1的分析,已经很清楚了.下面看看情况2.

   const size_type __old_size = size();        
      const size_type __len = __old_size + max(__old_size, __n);  // 这里不是简单的扩张两倍.因为不知道new_size和old_size之间的关系.
      iterator __new_start = _M_allocate(__len);           // 重新申请内存
      iterator __new_finish = __new_start;              // 初始化
      __STL_TRY {
        __new_finish = uninitialized_copy(_M_start, __position, __new_start);  // 将原数组中_M_start -> _M_finish之间的元素复制到新数组中
        __new_finish = uninitialized_fill_n(__new_finish, __n, __x);       // 初始化_new_finish -> _new_finish + __n 之间的元素
        __new_finish
          = uninitialized_copy(__position, _M_finish, __new_finish);           

  情况2执行的代码相比较情况1要少的多,尽管要执行新的内存分配. 在上面的源码注释中,我也注明了,这里不是简单的扩张为原来的两倍.

  为什么要这么做呢? 原因其实很简单,因为resize时候,__new_size和size()之间的关系是不知道的,有可能是三倍四倍,也有可能是二倍,

  或者说是介于这些数字之间,所以不应该用一个确切的数字来决定.

  不知道会不会有人问一个问题:  关于内存扩张_len的确定为什么和_M_end_of_storage没有关系了,就是为什么和存储能力没有关系了。

  而是和size()有关系.  额,答案是这个样子的.在前面分析两种情况的时候就说明了,情况2是已经不在存储范围内了,所以需要结合这些基本情况

  联系在一起考虑.

 

最后对于情况1,情况2都执行相同的源码:  

    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;

  执行一些初始化和清理工作,收尾.

  

      到这里,关于resize函数的介绍就都结束了. 

 

 

  小结:

    STL中容器对于元素的存储在底层使用的都是数组,而实现数据结构都是使用_M_start,_M_finish,_M_end_of_storage.

    STL中的函数提供的通用性是很好的,而且源码的设计与数据结构的实现很精巧,同时也是很复杂的