您的位置:首页 > 编程语言 > C语言/C++

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

2012-08-03 14:56 1001 查看
  前面在介绍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. [b]size_type(_M_end_of_storage - _M_finish) <= __n(__n = __new_size - size())[/b]

  

对于这两种情况该如何解释呢?如何去理解. _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中的函数提供的通用性是很好的,而且源码的设计与数据结构的实现很精巧,同时也是很复杂的.

  

 

  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: