MyTinySTL的deque源码分析

mystl项目地址为:https://github.com/Alinshans/MyTinySTL

原STL库十分庞大,父子类关系十分复杂。故借这个项目来研究原STL库的代码逻辑,对代码的理解都以注释的形式写在了注释里

deque

底层是分段连续的 ,我的理解是开辟了数组,元素皆为指针。每个指针指向了一个连续的元素空间。
在内存中开辟一段区域map,也就是指针数组。其中的元素为node,也就是指针。每个指针指向的元素称为buffer。
需要强调,map的数据类型typedef T** map_pointer;

迭代器

deque很大的篇幅是写迭代器。对于迭代器,有四个元素。且deque在创建时维护first和last两个迭代器
理解:deque的迭代器是指向node的,但是node是指向一段空间的,所以管理一段空间需要四个指针。

  value_pointer cur;    // 指向所在缓冲区的当前元素
  value_pointer first;  // 指向所在缓冲区的头部
  value_pointer last;   // 指向所在缓冲区的尾部
  map_pointer   node;   // 缓冲区所在节点

deque的迭代器有其独特的构造、复制、移动函数。重点在理解其是指向node的,这样看其上函数就迎刃而解了。
迭代器的运算符重载,:
“=”重载是直接赋值的

cur = rhs.cur;
first = rhs.first;
last = rhs.last;
node = rhs.node;

减法的重载

difference_type operator-(const self& x) const //self就是迭代器,不过是typedef
{
    return static_cast<difference_type>(buffer_size) * (node - x.node)
    + (cur - first) - (x.cur - x.first);
}/*我感觉返回结果是两个迭代器之间的元素个数,但是底层的定义是__MINGW_EXTENSION typedef unsigned __int64 size_t;,而这个__int64是longlong类型的,也就是8个字节。
此段的逻辑是先计算buffer之间的距离,这个距离加上没有计算的距离cur - first。这时x.cur到x.first的距离已经重叠算进去了,所以要减去*/

重载的++和–都需要预判下一步的动作是否超出了缓冲区,然后修改cur的值。
疑问:竟然是迭代器的操作,为什么不是更改指向的node,而是更改buffer的元素.
想法:我觉得是有意为之,好让用户进行元素的处理,毕竟处理一段buffer是用户不想干的。

重载+=和-=,举例说明:

  self& operator+=(difference_type n)
  {
    const auto offset = n + (cur - first);//计算跳跃的距离,offset表示跳跃后应在的位置
    if (offset >= 0 && offset < static_cast<difference_type>(buffer_size))//位置在buffer之内
    { // 仍在当前缓冲区
      cur += n;
    }
    else
    { // 要跳到其他的缓冲区
      const auto node_offset = offset > 0 // node_offset表示需要跳过的缓冲区个数
        ? offset / static_cast<difference_type>(buffer_size)//每段buffer可以容纳的元素个数
        : -static_cast<difference_type>((-offset - 1) / buffer_size) - 1;
      set_node(node + node_offset);//此函数下面特殊说明
      cur = first + (offset - node_offset * static_cast<difference_type>(buffer_size));
    }
    return *this;
  }
  // 转到另一个缓冲区
  void set_node(map_pointer new_node)//参数为指向的新的node的地址
  {
    node = new_node;
    first = *new_node;//头部
    last = first + buffer_size;//尾部
  }//跳入到新的buffer后,头尾指针指向buffer的头尾

对于迭代器的运算符重载,我发现是有两种不同类型的。
一种是buffer中数据的移动,另一个是node的移动。

  self operator-(difference_type n) const
  {
    self tmp = *this;
    return tmp -= n;
  }//例如这个,就和上面的difference_type operator-(const self& x),编译器应该是根据参数的类型进行判断调用哪个重载函数

deque 构造析构实现

deque创建时维护一下变量

  // 用以下四个数据来表现一个 deque
  iterator       begin_;     // 指向第一个节点
  iterator       end_;       // 指向最后一个结点
  map_pointer    map_;       // 指向一块 map,map 中的每个元素都是一个指针,指向一个缓冲区
  size_type      map_size_;  // map 内指针的数目

构造函数:deque通过调用fill_init()和copy_init()进行构造,例如:

  deque()
  { fill_init(0, value_type()); }//value_type()会调用空间适配器allocator
  deque(std::initializer_list<value_type> ilist)
  {
    copy_init(ilist.begin(), ilist.end(), mystl::forward_iterator_tag());
  }
  //首先思考,deque的创建需要map和其中的buffer,事实也是如此
//fill_init()函数如下
fill_init(size_type n, const value_type& value)
{
  map_init(n);//首先创建map,此函数下面有
  ......
}
map_init(size_type nElem) 
{
  const size_type nNode = nElem / buffer_size + 1;  // 需要分配的缓冲区个数,要比原始大一个
  map_size_ = mystl::max(static_cast<size_type>(DEQUE_MAP_INIT_SIZE), nNode + 2);   //DEQUE_MAP_INIT_SIZE 的值为8 map的大小最小为8
  try{
    map_ = create_map(map_size_);//指向一块 map,map 中的每个元素都是一个指针,指向一个缓冲区。create_map()调用的是空间适配器allocate用来分配内存,而其又是通过operator new开辟空间的。据我所知,operator的底层是调用的malloc
  }                           
  ......
  // 让 nstart 和 nfinish 都指向 map_ 最中央的区域,方便向头尾扩充
  map_pointer nstart = map_ + (map_size_ - nNode) / 2;
  map_pointer nfinish = nstart + nNode - 1;
  try{
    create_buffer(nstart, nfinish);//创建缓冲区
  }
  ......
  //设置创建时维护的首尾迭代器
  begin_.set_node(nstart);
  end_.set_node(nfinish);
  begin_.cur = begin_.first;
  end_.cur = end_.first + (nElem % buffer_size);
}

至此总结:创建deque时,首先创建map。map的大小可以通过修改源代码(max函数)进行自定义。然后创建缓冲区,并且设置首尾迭代器的值。缓冲区的创建是有讲究的,在map的中间的指针指向创建的缓冲区,并不是所有所有指针都创建,只是一部分。这其实给了一定的扩展性,当然,如果全部创建的话也不是不可,但是一般而言是根据数据的增加动态创建。

复制函数:copy_init()调用了map_init()创建deque,然后调用emplace_back()调用构造器,在其尾部就地增加元素(其中会new对象)
析构函数:调用clear()和deallocate();主题思路是先释放buffer,然后释放map。clear 会保留头部的缓冲区。
感觉的意思是,第一个node指向的缓冲区可能不满。那么从第二个node开始往后面删除,第二个node的开始位置是第一个node位置的同位(如果第一个node的cur指针指向5,那么从第二个node的5位置开始删除)。删除完成之后,接着删除第二个node的全部(也就是0-4),最后删除第一个node指向的缓冲区。最后删除map。我不知道为什么需要这样缩小容量销毁的目的所在,既然重载了++,+,为什么不直接从第一个往后全部删除呢?

主要使用函数

push_front()

template <class T>
void deque<T>::push_front(const value_type& value)
{
  if (begin_.cur != begin_.first){//如果第一个位置没满,直接插入元素
    data_allocator::construct(begin_.cur - 1, value);
    --begin_.cur;
  }
  else{ //否则需要另外开辟node,开辟buffer
    require_capacity(1, true); //第二个参数为true,前面
    ......
  }
}
void deque<T>::require_capacity(size_type n, bool front)
{
  if (front && (static_cast<size_type>(begin_.cur - begin_.first) < n))
  { //需要的空间是前面 && 后面的条件应该是考虑插入多个元素时,元素个数大于buffer空间的情况
    const size_type need_buffer = (n - (begin_.cur - begin_.first)) / buffer_size + 1;//至少一个,考虑到插入一个或者多个元素
    if (need_buffer > static_cast<size_type>(begin_.node - map_)){
      reallocate_map_at_front(need_buffer);//重新分配空间
      return;
    }
    create_buffer(begin_.node - need_buffer, begin_.node - 1);
  }
  else if (!front && (static_cast<size_type>(end_.last - end_.cur - 1) < n))
  { //需要的空间是后面
    const size_type need_buffer = (n - (end_.last - end_.cur - 1)) / buffer_size + 1;
    if (need_buffer > static_cast<size_type>((map_ + map_size_) - end_.node - 1)){
      reallocate_map_at_back(need_buffer);
      return;
    }
    create_buffer(end_.node + 1, end_.node + need_buffer);
  }
}

我之前一直想不透怎么在map之前再加一个node,看了reallocate_map_at_front我大为震撼。说白了就是new了一块新空间,然后将原先空间赋值进去了,就是直接莽。这里需要细说的是,map有点类似vector,增长的情况一般是两倍。
我觉得旧map的数值会赋值到新map靠中间的位置,之所以这么想,是因为源码中出现了auto begin = new_map + (new_map_size - new_buffer) / 2;这么一句。源码变量很多很绕,具体就不看了。

const size_type new_map_size = mystl::max(map_size_ << 1,map_size_ + need_buffer + DEQUE_MAP_INIT_SIZE);  //仔细看map_size_ << 1

map_pointer new_map = create_map(new_map_size);//开辟新空间
for (auto begin1 = mid, begin2 = begin_.node; begin1 != end; ++begin1, ++begin2)
    *begin1 = *begin2; //直接赋值,因为是两倍增长,也就说明了为什么会出现一个mid

对于reallocate_map_at_back有相同的逻辑。

本文只是自己的理解,如有错误,请指出。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值