stl源1码剖析(4)顺序容器

总览

 

还有3个适配器 stack queue priority_queue

vector

维护的是一个连续线性空间,对于gnu2.9来说,有3个数据成员 start,finish,end_of_storage,都是普通指针,分别指向开头、尾后、和实际可容纳最后一个元素+1 的位置, push_back函数就是 首先检查是否还有备用空间,如果有就直接在备用空间上构造函数,并调整finish,没有备用空间,就新申请一个double size的空间,把原来的数据 拷贝过去,(可以用 traits进行优化,bitwise copy还是逐个调用拷贝构造函数这样),然后释放原来的空间。 (唯一可能需要注意的点就是,对vector的任何操作,引起空间重新配置,那么指向原vector的迭代器就会失效),感觉没什么东西。

哦,还有vector的最大大小:

vector的下标是size_t, (这里有个坑!!!unsinged int 无符号数 看关于size_t 这篇文章)它的类型跟具体平台有关系,32位应该是 unsigned int  32位比特 也就是 2的32次方, 64位应该是unsigned long, 64位比特,2的64次方。 这个也是限制vector最多能放多大元素的其中一个因素,另一个因素是机器的内存和外存也是有限的,对于容器是保存在栈上还是堆上又有点区别。

加点别人的总结,随便看看吧。

在STL中,最常用的就是容器,最常用的容器就是vector了。vector类似内置数组。但是数组是静态的,一旦配置就不能再变大小,而容器的大小事容器本身自己调整的。在实现容器的代码中可以看到,容器可以动态增大,但是不能动态减小。
容器有已用空间和可用空间,已用空间就是容器已经使用了的空间,可用空间就是指vector的大小capacity。
容器是占用一段连续线性空间,所以容器的迭代器就等价于原生态的指针(这是造成我一直以为迭代器就是指针的原因),vector迭代器类型是RandomAccessIterator类型。vector的实现依赖于前面内存的配置和内存的初始化,以及迭代器,看学习vector代码可以帮助我们更加深入理解stl_alloc.h、stl_uninitialized.h、stl_iterator.h。
 

gnu 2.9 vector中的iterator 就是一个内置指针

gnu 4.9 vector中的iterator 变成了一个类,这个类里面有一个内置指针型的数据成员 T * 

关于vector迭代器失效:

对于vector而言,它的迭代器其实就是一个原生指针或者一个封装了原生指针的类,所以假如因为insert or push_back

这些操作引起了扩容,所有元素都搬到一个新的地址空间去,那全部迭代器都会失效,而平常的插入or删除操作,没有引起扩容,那只会让被操作的那个元素之后的迭代器失效(因为vector是放在一段连续的地址空间,你删除了一个元素,后面的所有元素都会往前面挪一下),而之前的迭代器没什么影响。

更详细的:
看下reserve resize 和 swap吧

这里是一系列比较常用的工具函数,包括begin、end、size、capacity等,都是很简单的函数。

从size和capacity函数,就可以理解finish和end_of_storage指针的区别。然后,构造函数,都是通过uninitialized_fill_n来实现的,也是说,如果是拷贝PODtype的vector,实际上和memset没有区别,因为不会逐个调用构造函数。而且,析构函数也没有释放内存,只是destory所有元素。如果该类型有nontrivial的destructor,才会依次调用析构函数。

vector<_Tp, _Alloc>& operator=(const vector<_Tp, _Alloc>& __x);
  void reserve(size_type __n) {
    if (capacity() < __n) {
      const size_type __old_size = size();
      iterator __tmp = _M_allocate_and_copy(__n, _M_start, _M_finish);
      destroy(_M_start, _M_finish);
      _M_deallocate(_M_start, _M_end_of_storage - _M_start);
      _M_start = __tmp;
      _M_finish = __tmp + __old_size;
      _M_end_of_storage = _M_start + __n;
    }
  }

  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);
  }

reserve(n)的逻辑: 当 n小于等于现有的 capacity 无事发生, 大于的话, 新申请一个n的空间,把原来的拷贝过来,原来的析构、释放。

这个reserve函数用来保证vector有可以存放n个value元素的空间。如果空间不够了,就会执行allocate new area -> copy -> destroy and deallocate。而resize函数与之类似,只不过reserve是对capacity而言,而resize是对size操作。(并且假如新size比老size小,那把之前老size多余部分给erase掉了,而如果新size比老size大,会把在后面插入那多出来的那部分,并且执行默认构造函数),所以说reserve比resize效率高?

void swap(vector<_Tp, _Alloc>& __x) {
    __STD::swap(_M_start, __x._M_start);
    __STD::swap(_M_finish, __x._M_finish);
    __STD::swap(_M_end_of_storage, __x._M_end_of_storage);
  }

另外一个函数swap也是用的比较多,这个函数是用来交换两个vector,但是实际上只是交换了那三个重要指针,也就是交换了两个vector的底层内存。

所以,这样就可以理解为什么会有利用swap来释放某个vector的空间。一般是先创建一个栈上的vector,并且是空的,然后swap另一个vector,这样另一个vector的capacity就是0了,然后本来是空的vector拥有了capacity,会在生命周期结束时调用析构函数,释放空间了。

void clear() { erase(begin(), end()); }

还有这个clear函数,并不会清空内存,只是改变了size,而不会改变capacity,毕竟内存都是在vector base的析构函数中释放的。

insert

vector中最重要的,大概就是push_back、insert和erase函数了,而这些函数都实际上会调用insert_aux来做底层实现。

void push_back(const _Tp& __x) {
    if (_M_finish != _M_end_of_storage) {
      construct(_M_finish, __x);
      ++_M_finish;
    }
    else
      _M_insert_aux(end(), __x);
  }

  iterator insert(iterator __position, const _Tp& __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;
  }

对于单个元素的插入,都是先判断容量是否充足,如果不是,就会调用insert_aux这个函数,去具体实现。然后,对于范围insert就会有点其他操作,范围insert分为通过iterator指定的范围数据和插入n个相同的数据,后者实现相对简单,会让fill_insert函数来执行具体操作。

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_backward(__position, _M_finish - 2, _M_finish - 1);
    *__position = __x_copy;
  }
  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;
    __STL_TRY {
      __new_finish = uninitialized_copy(_M_start, __position, __new_start);
      construct(__new_finish, __x);
      ++__new_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;
  }
}

这里的insert_aux函数看起来很复杂,但是大部分都是调整那三个指针,以及搬运数据。先是判断capacity是否足够,如果充足,那么就将position后面的数据整体后移,然后插入新数据。如果capacity不够了,那就按照两倍增长的速度来申请新的空间,然后把东西搬过去,再释放旧空间,这就是vector比较核心的动态增长策略的实现了。

还有一个函数fill_insert,与insert_aux的区别只是插入的是n个元素,所以具体实现上很类似,就是capacity增长有点不同:

const size_type __len = __old_size + max(__old_size, __n);
      iterator __new_start = _M_allocate(__len);

这里不是两倍增长,而是判断插入的元素有多少个,如果两倍增长都不够,那就增长n个。比如说,有一个capacity为8的vector,如果插入100个元素,那么增长到16是肯定不行的,所以需要增长到108。

erase

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);
    return __first;
  }

删除很简单,毕竟没有涉及到capacity的问题,只是简单的destroy和调整那三个指针。

list

双向链表,不向vector一样是连续线性空间,相较于vector,空间额外开销比较大,因为一个node 存了3个元素,有两个额外的指针,所以对于小的data元素来说,尽量别选list 

只支持双向顺序访问 对于任何位置插入删除,都是常数时间

list的迭代器是 一个 has a 指向list节点的普通指针 的类,所以不是random iterator 而是双向迭代器,另外有一个重要的性质,插入,删除啊,这些操作基本都不会让原有迭代器失效

SGI 的list 是一个环状双向链表,也就是循环双向链表,在实现上在环状链表的尾部加上一个空白节点,而list类它有一个data 成员   就是指向这个节点的指针

stl 有几个点需要注意,对于迭代器 永远是前闭后开 [ ) 的形式  (所以对于  迭代器范围构造函数啊这些,不要迷惑)

还有一个是insert,永远的 插入节点之前,这是stl的策略

还有就是注意几个成员函数:

splice 将1个list的一段连续数据接在另一个list的某个位置之前

merge、sort(使用的是归并)

deque

1 . deque是由一段一段的定量连续空间构成的,它自身维护一个指针数组(map),数组里面的元素分别指向一段相同长度的连续空间,除了头和尾元素所在的那缓冲区(就是某段连续空间)之外,中间的缓冲区都是已经被占满了的,并且假如要在 deque 的前端开一个新的缓冲区,那么新元素是先占据那个缓冲区的尾部,而要在尾端增加新空间,新元素是先占据新开的缓冲区头部。

2. 并且deque初始化的时候,是先占据那个指针数组(map)中间的区段,目的是为了头尾两端的扩充一样多(p154)

3. deque的 insert和erase 具体实现是看移动前半部分的元素比较方便(也就是元素数目少),还是移动后半部分的元素少,来决定移动那个半边的(肯定要移动,为了中间的缓冲区一定要被占满,这样迭代器才好移动)

4. 假如map数组满了的话,deque的实现跟vector是一样的,新申请一个新的map数组(新的size至少是原来的两倍),把原来的那个map数组里面的值给它拷贝过来(同样先占据中间的位置),注意,原来map数组的元素的值是没变的,也就是已经开辟了的缓冲区是不释放的,新的map数组里面的元素还是指向那些缓冲区

5. deque的迭代器类型是 random access,所以可以+n -n

  • deque 是一种双向开口的连续线性空间。可以在头尾两端分别做元素的插入和删除操作。
  • deque 和 vector 的差异:第一,deque 允许于常数时间内对起头端进行元素的插入或移除操作;第二,deque 没有容量,它是动态地以分段连续空间组合而成。
  • deque 由一段段的定量连续空间构成。一旦有必要在 deque 的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个 deque 的头端或尾端。
  • deque 实现复杂,其复杂的迭代器架构。
  • 在 deque 的以前版本中,还有一个额外的模板参数,以便用户可以控制节点的大小。 这种扩展证明是违反 C++ 标准(可以使用模板模板参数检测到),并且已被删除。

deque 的中控器 map

deque 采用一块所谓的 map 作为主控。这个 map 是一小块连续空间,其中每个节点都是指针,指向另一段连续线性空间,称为缓冲区。

deque 的迭代器 _Deque_iterator

deque 是分段连续空间,维持其“整体连续”假象的任务。

迭代器包含 cur 指向缓冲区的当前元素,first 指向缓冲区的头,last 指向缓冲区的尾,node 指向中控器某一个 node 节点。

push_back():尾端插入

push_front():前端插入

pop_back():尾端取出

pop_front():前端取出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值