STL源码剖析(一)

  • STL六大部件

容器(container) 、分配器(allocator) 、算法(algorithms) 、迭代器(iterator) 、适配器(adaptor) 、仿函数(functor)

  • 二   分配器 Allocator

VC6、BC++ 、以及SGI也有一个符合”部分“标准(一级分配器)、名为allocator的分配器, 它只是以 ::operator new和 ::operator delete 完成 allocate()和 deallocator(),而new、delete最后都会调用C底层的malloc()、free(),但malloc申请内存总是会有额外开销,总是带有cookie记录内存大小,占用8字节。所以SGI的allocator不建议使用,效率不佳。

SGI还有一个特殊的空间分配器std::alloc ,不接受任何参数。

不能采用标准写法:
vector<int,std::allocator<int>> iv;  //in VC or cB
必须这么写:
vector<int,std::alloc> iv; //in GCC 

通常,C++内存分配和释放的操作如下:

class Foo {...};
Foo *pf = new Foo;  //配置内存,然后构造对象
delete pf;         // 将对象析构,然后释放内存

new内含2阶段操作

  • 调用::operator new分配内存。调用构造函数构造对象

delete也含2阶段操作

  • 调用析构函数析构对象。调用::operator delete释放内存

STL allocator将两阶段操作区分开来。内存配置有alloc::allocate() 负责,内存释放由alloc::deallocate()负责;对象构造操作::construct(),对象析构::destroy()负责。皆定义在<memory>中,<stl_construct.h> <stl_alloc.h>

  • 内存分配与释放

SGI对内存分配与释放的设计哲学如下:

  • 向system heap申请空间
  • 考虑多线程状态
  • 考虑内存不足时的应变措施
  • 考虑过多“小型区块”可能造成的内存碎片问题(SGI设计了双层级分配器

C++的内存分配基本操作是::operator new(),内存释放基本操作是::operator delete()。这两个全局函数相当于C的malloc()和free()函数。

SGI正是以malloc和free()完成内存的分配与释放。

SGI 第一级分配器_malloc_alloc_template 以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重配置操作,并实现类似C++ new handler机制(一旦malloc 无法完成任务,在丢出 std::bad_alloc 异常状态之前,会先调用有客户端指定的处理例程。)

第二级分配器 _default_alloc_temolate。由于每次用malloc申请内存时,都要有额外开销。

SGI第二级配置器的做法是,如果区块够大,>128 bytes,就用第一级配置器处理。当区块<128 bytes,则以内存池(memory pool)管理,此方法又称为次层配置。每次配置一大块内存,并维护对应的自由链表(free-list)。为了方便管理,二级配置器会主动将任何小额区块内存需求量上调至8的倍数(例如客户端要求30 bytes,就自动调整为32 bytes)并维护16个free-list,各自管理大小分别为8,16,24.。。。128bytes 的小额区块。

重新填充free-list的函数refill()

  • 若free-list中没有可用区块时,会调用chunk_alloc从内存池中申请空间重新填充free-list。缺省申请20个新节点(新区块),如果内存池空间不足,获得的节点数可能小于20。

chunk_alloc()函数从内存池申请空间,根据end_free-start_free判断内存池中剩余的空间

  • 如果剩余空间充足
    • 直接调出20个区块返回给free-list
  • 如果剩余空间不足以提供20个区块,但足够供应至少1个区块
    • 拨出这不足20个区块的空间
  • 如果剩余空间连一个区块都无法供应
    • 利用malloc()从heap中分配内存(大小为需求量的2倍加上一个随着分配次数增加而越来越大的附加量),为内存池注入新的可用空间(详细例子见下图
    • 如果malloc()获取失败,chunk_alloc()就四处寻找有无”尚有未用且区块足够大“的free-list。找到了就挖出一块交出
    • 如果上一步仍未成功,那么就调用第一级分配器,第一级分配器有out-of-memory处理机制,或许有机会释放其它的内存拿来此处使用。如果可以,就成功,否则抛出bad_alloc异常

上图中,一开始就调用chunk_alloc(32,20),于是malloc()分配40个32bytes区块,其中第1个交出,另19个交给free-list[3]维护,余20个留给内存池;接下来客户调用chunk_alloc(64,20),此时free_list[7]空空如也,必须向内存池申请。内存池只能供应(32*20)/64=10个64bytes区块,就把这10个区块返回,第1个交给客户,余9个由free_list[7]维护此时内存池全空。接下来再调用chunk_alloc(96,20),此时free-list[11]空空如也,必须向内存池申请。而内存池此时也为空,于是以malloc()分配40+n(附加量)个96bytes区块,其中第1个交出,另19个交给free-list[11]维护,余20+n(附加量)个区块留给内存池...


  • 三 迭代器概念与traits编程技巧

迭代器是一种泛化指针,最重要的编程工作是对operator* 和 operator-> 进行重载。每一种STL容器都提供专属的迭代器。

template <class Iterator>
struct iterator_traits{
    typedef typename Iterator::iterator_category   iterator_category;
    typedef typename Iterator::value_type          value_type;
    typedef typename Iterator::difference_type     difference_type;
    typedef typename Iterator::pointer             pointer;
    typedef typename Iterator::reference           reference;
};

这个traits的意义是,若 I 有定义自己的value type ,那么通过traits 萃取出的value_type 就是I::value_tye。

 多一层间接层(itetator_traits)的好处是能拥有特化版本。当iterator是个原生指针时:

//以C++内建的ptrdiff_t(定义于<cstddef>头文件)作为原生指针的difference type

//针对原生指针的偏特化版本
template <class T>
struct iterator_traits<T*>{
    //原生指针是一种Random Access Iterator
    typedef random_access_iterator_tag   iterator_category;
    typedef T                            value_type;
    typedef ptrdiff_t                    difference_type;
    typedef T*                           pointer;
    typedef T&                           reference;
};

//针对原生pointer-to-const的偏特化版本
template <class T>
struct iterator_traits<const T*>{
    //原生指针是一种Random Access Iterator
    typedef random_access_iterator_tag   iterator_category;
    typedef T                            value_type;
    typedef ptrdiff_t                    difference_type;
    typedef const T*                     pointer;
    typedef const T&                     reference;
};

 

       最常用到的迭代器相应型别有五种: value_type 迭代器所指对象类型;difference_type 两个迭代器之间的距离,因此也用来表示一个容器的最大容量。对于连续空间的容器而言,头尾之间的距离就是最大容量,如果一个泛型算法提供计数功能,例如STL的count(),返回值就必须使用迭代器的difference_type。reference_type pointer_typeiterator_category 迭代器种类。

(直线与箭头代表的并非继承关系,而是concept 与 reference 关系)

      任何迭代器都应该提供五个内嵌相应型别,以利于traits萃取。STL提供了一个iterator class如下,每个新设计的迭代器都可以继承它,保证符合STL的规范。

  • _ _type_traits

iterator_traits 负责萃取迭代器的特性

_ _type_traits:负责萃取类型的特性,包括:

  • 该类型是否具备non-trivial default ctor
  • 该类型是否具备non-trivial copy ctor
  • 该类型是否具备non-trivial assignment operator
  • 该类型是否具备non-trivial dtor

通过使用__type_traits,在对某个类型进行构造、析构、拷贝、赋值等操作时,就可以采用最有效率的措施。这对于大规模而操作频繁的容器,有着显著的效率提升。<type_traits.h>对所有C++标量类型都定义了_ _type_traits特化版本。


  • 四 序列式容器 Sequence containers

             4.1 容器的概观与分类

         

        上图中的“衍生”并非“派生(inherit)”,而是内含(contain)关系。例如heap内含一个vector,priority-queue内含一个heap,stack和queue都含一个deque,set/map/multiset/multimap都内含一个RB-tree,has_x都内含一个hashtable。

       4.1.1 序列式容器

C++语言本身提供了一个序列式容器array,STL另外再提供vector、list、deque、stack、queue、priority-queue等,其中stack和queue只是deque改头换面而已,技术上被归类为一种配接器。

 4.2 vector

array是静态空间,一旦配置了就不能改变;vector与array非常相似,但是vector是动态空间,随着元素的加入,内部机制会自动扩充以容纳新元素 。

SGI STL中vector的定义。使用线性连续空间,以两个迭代器startfinish 分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器end_of_storage 指向整块连续空间(含备用空间)的尾端。

template<class T,class Alloc=alloc>
class vector{
   .....
   public:
   iterator begin() {return start;}
   iterator end() {return finish;}
   size_type size()const {return size_type(end()-begin()) ;}
   size_type capacity()const {
        return size_type(end_of_storage-begin()) ;}
   bool empty()const { return begin()==end(); }
}

      当我们以push_back()将新元素插入vector尾端时,该函数首先检测是否还有备用空间,如果有,就直接在备用空间构造元素,并调整迭代器finish,使vector变大,若没有备用空间,扩充空间(重新配置、移动数据、释放原空间)。

void push_back(const T& X){
    if(finish!=end_of_storage){  //还有备用空间
        construct(finish,x);
         ++finish;
     } 
    else
       insert_aux(end(),x) ;  //分配内存
}

vector操作的实现

常见的vector操作包括:

插入操作可能造成vector的3个指针重新配置,导致原有的迭代器全部失效

4.3 list

    STL list的节点结构 

template<class T>
struct _list_node{
    typedef void* void_pointer;
    void_pointer prev;  //类别为void*,其实可设为 _list_node<T>*
    void_pointer next;
    T data; 
}

   

 SGI list不仅是一个双向链表,还是一个环状双向链表。所以它只需要一个指针,便可完整表现整个链表:

template <class T, class Alloc = alloc>
class list {
protected:
    typedef __list_node<T> list_node;
public:
    typedef list_node* link_type;

protected:
    link_type node; //只要一个指针,便可表示整个环状双向链表
};

iterator begin() { return (link_type)((*node).next); }
iterator end() { return node; }
size_type size() const {
    size_type result = 0;
    distance(begin(), end(), result);
    return result;
} 

 list 是一个双向链表,迭代器必须具备前移、后移的操作,所以list提供的是Bidirectional Iterators

list操作的实现

//删除某个节点的例子
ite=find(ilist.begin(),ilist.end(),1)
if(ite!=0)
    cout<< *(ilist.erase(ite)) <<endl;

       list 内部提供 transfer 迁移操作,将连续范围的元素迁移到某个特定位置之前。这个操作作为其它复杂操作如splice,sort,merge等奠定良好的基础。list 不使用STL算法的sort(),必须使用自己的sort() member function(是一个快排),因为STL算法sort()只接受RamdonAccessIterator。

4.4 deque

deque是一种双向开口的连续线性空间。deque的大小是40byte

deque和vector最大的差异:

  1. deque允许于常数时间内对起头端进行元素的插入或移除操作
  2. deque没有所谓容量观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来(deque没有必要提供所谓的空间保留功能)
  3. 除非有必要,应尽可能选择使用 vector 而非 deque。对 deque进行排序操作,为了提高效率,可将 deque先完整复制到一个vector上,将vector排序后,再复制回 deque。

deque 由一段一段的定量连续空间构成,一旦有必要在deque 的 前端或者尾端 增加新空间,便配置一段定量连续空间,串接在整个 deque的头端或尾部。避开了 “重新配置,复制,释放”,代价是复杂的迭代器架构。

dequeue 数据结构

deque采用一块所谓的 map(不是STL 的map 容器)作为主控(中控器)。这里所谓的map是指一小块连续空间,其中每个元素都是一个指针指向另一段、(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。SGI STL允许我们指定缓冲区大小,默认值0表示使用512bytes缓冲区

temlate <class T,class Alloc = alloc,size_t BufSiz = 0>
class deque{
public:     //Basic types
    typedef T value_type;
    typedef value_type* pointer;
    typedef size_t size_type;
    ...

public:
    typedef __deque_iterator<T,T&.T*,BufSiz> iterator;  //迭代器类型

protected:  //Internal typedefs
    //元素的指针的指针
    typedef pointer* map_pointer;

protected:  //Data members
    iterator start;         //第一个节点的迭代器
    iterator finish;        //最后一个节点的迭代器

    map_pointer map;        //指向map,map是块连续空间
                            //其每个元素都是个指针,指向一个节点(缓冲区)
    size_type map_size;     //map的大小,即内有多少个指针
...
};

deque 除了维护一个指向 map 的指针外,还需要维护 start, finish 两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一个位置)。还需要记住 map的大小。

deque的中控器、缓冲区、迭代器的关系如下图:

deque 迭代器

deque是分段连续空间,维持其“整体连续”假象的任务,落在了迭代器的operator++和operator-- 两个子运算上。

deque 迭代器 必须能够指出分段连续空间(即缓冲区)在哪里,其次能够判断是否处于所在缓冲区的边缘,如果是,一旦前进或者后退时就必须跳跃至下一个或上一个缓冲区,所以deque 必须随时掌握 map。关系图如上所示。

operator ++ 实现

self& operator++(){
   ++cur;  //切换至下一个元素
   if(cur==last){        //如果已达所在缓冲区的尾端
       set_node(node+1);  //就切换至下一节点(下一个缓冲区)
       cur=first;          //的第一个元素
   } 
   return *this;
}

void set_node(map_pointer new_node){
    node=new_node;
    first=*new_node;
    last=first+difference_type(buffer_size());
}

push_back()操作

void push_back(const value_type& t){
     if(finish.cur != finish.last-1){
         //最后缓冲区尚有一个以上的备用空间
         construct(finish.cur,t);  //直接在备用空间上构造元素
         ++ finish,cur;
      }
      else //最后缓冲区已无(或只剩一个)元素备用空间
         push_back_aux(t);
}

template<class T,class Alloc,size_t BufSize>
void deque<T,Alloc,BufSize>::push_back_aux(const value_type& t){
       value_type t_copy=t;
       reserve_map_at_back();  //如符合条件则必须重换一个map
       *(finish.node + 1)= allocate_node();  //配置一个新节点(缓冲区)
       _STL_TRY{
           construct(finish.cur,t_copy);  
           finish.set_node(finish.node+1);
           finish.cur=finish.first;
        }
}

deque操作的实现

4.5 stack(适配器)

一种先进后出的数据结构。没有迭代器

以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”的特性,形成一个stack,是很容易做到的。deque是双向开口的数据结构,若以deque为底部结构并封闭其头端开口,便形成一个stack

由于stack以底部容器完成其所有工作,而具有”修改某物接口,形成另一种风貌“的性质者,称为适配器。因此,STL stack往往不被归类为容器,而被归类为容器适配器。

除了 deque 之外,list  也是双向开口的数据结构。所以 stack 也可以以 list 作为 底层容器。

4.6 queue

先进先出的数据结构。若以deque为底部结构并封闭其底端的出口和前端的入口,便形成一个queue。

4.7 heap

heap 并不属于STL容器组件,扮演priority queue 的助手。priority queue 允许用户以任何次序将任何元素放入容器内,但取出时一定是从优先级最后的元素开始取。binanry max heap 正具有这样的特性,适合作为priority queue 的底层容器

heap是一颗完全二叉树,完全二叉树使用数组实现,因此使用一个vector作为heap的结构,然后通过一组xxx_heap算法,使其符合heap的性质。

pop_heap 算法,取走根节点(即底部容器 vector 的尾端节点)。

sort_heap 算法,持续对整个 heap 做 pop_heap操作,每次将操作范围从后向前缩减一个元素(pop_heap 会把 键值最大的元素 放在底部容器尾端)。当整个程序执行完毕时,便有了递增序列。

4.8 priority_queue

优先队列缺省情况下priority_queue 利用一个max_heap完成。没有迭代器,不提供遍历功能。 

以下为SGI STL中priority_queue的定义:

template <class T, class Sequence = vector<T>, 
          class Compare = less<typename Sequence::value_type> >
class  priority_queue {
public:
  typedef typename Sequence::value_type value_type;
  typedef typename Sequence::size_type size_type;
  typedef typename Sequence::reference reference;
  typedef typename Sequence::const_reference const_reference;
protected:
  Sequence c;       //底层容器
  Compare comp;     //元素大小比较标准
public:
  priority_queue() : c() {}
  explicit priority_queue(const Compare& x) :  c(), comp(x) {}

  //以下用到的make_heap()、push_heap()、pop_heap()都是泛型算法
  //构造一个priority queue,首先根据传入的迭代器区间初始化底层容器c,然后调用
  //make_heap()使用底层容器建堆
  template <class InputIterator>
  priority_queue(InputIterator first, InputIterator last, const Compare& x)
    : c(first, last), comp(x) { make_heap(c.begin(), c.end(), comp); }
  template <class InputIterator>
  priority_queue(InputIterator first, InputIterator last) 
    : c(first, last) { make_heap(c.begin(), c.end(), comp); }

  bool empty() const { return c.empty(); }
  size_type size() const { return c.size(); }
  const_reference top() const { return c.front(); }
  void push(const value_type& x) {
    //先利用底层容器的push_back()将新元素推入末端,再重排heap
    __STL_TRY {
      c.push_back(x); 
      push_heap(c.begin(), c.end(), comp);
    }
    __STL_UNWIND(c.clear());
  }
  void pop() {
    //从heap内取出一个元素。但不是真正弹出,而是重排heap,然后以底层容器的pop_back()
    //取得被弹出的元素
    __STL_TRY {
      pop_heap(c.begin(), c.end(), comp);
      c.pop_back();
    }
    __STL_UNWIND(c.clear());
  }
};

 

    •  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值