面向对象与泛型编程

  OOP是将data与method封装在一个class中,STL是将container容器与algorithms算法分开,容器和算法可以“闭门造车,互不干扰”,他们两个的交流通过迭代器完成。

分配器

  为容器提供内存。在vc系列的编译器中使用的分配器是allocator,这些编译器中分配器的关键部分源码如下

template <typename T>
class allocator{
.....    
    pointer  allocate(需要的空间);  //归根结底是调用malloc()
    void deallocate(指针,要归还的空间大小);//调用free();
...
}
//根据源码举个例子
int *p = allocator<int>().allocate(10,(int*)0);
allocator<int>.deallocate(p,10); //需要指明归还的大小

  主要就是对malloc()和free()进行包装再包装,不光浪费空间还影响效率。每malloc一次就会有额外的开销,其中就有cookie来记录每次malloc的大小,由free()来找到cookie,精准释放这块内存。

  而G2.9所使用的是分配器是alloc,由于容器中的元素大小都是一样的,所以没有必要记录每个元素的大小,也就不需要这么多的额外开销。G2.9的分配器有16条链表,每一个链表负责某一种特定大小的区块,比如第0号链表负责8字节大小的区块,第1号链表负责16字节大小的区块,第15号负责128字节大小的区块。容器中元素的大小都会被调整成8的倍数,由16条链表中的某一条负责,比如,容器中的元素大小为15字节,就会调整为16字节,如果这1号链表没有区块,就需要向操作系统一次mallco()要多个16字节的内存(16*20字节),把这么多内存切成20个区块,每个区块16字节,给出去一块,剩余19个区块连接起来。这些区块不需要每一个都有cookie,因此减少了浪费。

迭代器(iterators)的设计原则

  迭代器是算法和容器之间的桥梁,算法想要操作容器就需要迭代器提供的“指针”,通过这个“指针”来处理对应的元素。算法需要知道迭代器的特性(以下三种),由此来选择一个更高效的方式。

  1、iteratiors的5个属性

    iteratior_category()是指"指针"的移动性质,有的容器只允许"指针"做++操作,比如list,有的容器允许"指针"+n操作,跳跃访问比如vector;

    vaule_type:迭代器所指向的元素是什么类型的;

    difference_type:两个迭代器之间的距离用什么类型来表现;

    reference

    pointer

源码展示算法和容器之间的问与答,算法想要知道迭代器的几个性质。

//算法的提问,想要知道迭代器的五种特性,I表示iteratior
template<typename I>
inline void algorithm(I frist,I last)
{
    ...
    I::iterator_category;
    I::pointer;
    I::reference;
    I::value_type;
    I::deference_type;
    ...
}
//某种容器(list)的迭代器的回答
template<class T,class Ref,class Ptr>
struct __list_iterator
{
    ...
    typedef bidirectional_iterator_tag iterator_category;
     //告诉算法list容器的iterator_category是可以双向移动
    typedef T value_type;
    //告诉迭代器所指的元素是什么类型
    typedef Ptr pointer;
    typedef Ref reference;
    typedef ptrdiff_t difference_type;
    //告诉两个迭代器之间距离是什么类型
    ...

}

  list容器的迭代器是用class封装好的,但是如果是自然指针,算法怎么知道它的几个特性?指针怎么回答?这时候就需要Iterator Traits(萃取机)。当传递给traits是class封装好迭代器,算法问class来回答,就像上面一样。如果是自然指针就用另外一种方式来得到五种特性。

template<typename I,...> //I是某个迭代器,可能是封装好的,也可能是自然指针
void algorithm()
{
    typename iterayor_traits<I>::value_type;
    //算法问traits这iterator的value_type是什么
}
//如果class iterator 进入下面
template <class I>
struct iterator_traits{
    typedef typename I::value_type  value_type;
}
//如果是自然指针
template <class T>
struct iterator_traits<T*>
{
    typedef T value_type; //T就是value_type
}

  总的来说traits会分辨出class_iterator和 non class_iterator,让算法知道他想知道的特性。


list


链表是一个非连续空间,所以list的iterator不能是一个自然指针,但是我们希望list的iterator能够模拟指针的动作,比如++操作,iterator能够找到next指向的结点,所以iterator必须得是一个class,才能完成复杂的动作,里面必然有大量的操作符重载。

reference operator*() const      //iterator的解引用
 {
    return (*node).data;
}
pointer operator->() const{ return &(operator*());} //返回结点data域的地址,通过->可以访问data域中的成员
self &operator++() //前置++
{
    node = (link_type)((*node).next);
    return *this;
} 
self operator++(int) //后置++
{
    self tmp = *this;
    ++*this;
    return temp;
}

总的来说list的iterator里面有两大部分,一部分是typedef,另外一部分是操作符重载。

 


 

vector容器源码分析

   

template <class T,class Alloc= alloc>
class vector{
public:
    typedef T value_type;
    typedef value_type * iterator;
    typedef value_type& reference;
    typedef size_t size_type;
protected:
    iterator start;
    iterator finish;
    iterator end_of_storage;
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();
     }
    reference operator[] (size_type n)
    {
        return *(begin()+n);
    }
    reference front()
    {
        return *begin();
    }
    reference back()
    {
        return *(end()-1);
    }
};

如上图所示,当我们要插入第九个元素时,vector内部是如何做到二倍增长,以下是源码实现

void push_back(const T &x)
{
    if(finish!=end_of_storage) //还有空闲空间
    {
        construct(finish,x);//全局函数,在尾部插入
        ++finish;
    }
    else
        insert_aux(end(),x);  //没有空闲空间,需要扩充
}
template <class T,class Alloc>
void vector<T,Alloc>::insert_aux(iterator position,const T&x)
{
    if(...)
    {
        ...
    }
    else
    {
        //扩充
        const size_type old_size = size(); //保存原有的大小
        const size_type len = old_size!=0?2*old_size:1;//初始size为0,分配一个元素大小
        //如果不为0,分配两倍
        iterator new_start = data_allocator::allocate(len); //开始分配
        iterator new_finish=new_start; 
        //重新分配了16个元素大小的空间,new_start和new_finish都指向了空间起始位置
        try{
            //将原来的内容拷贝到新的空间中,new_finish指向最后一个元素的下一个位置
            new_finish=uninitialized_copy(start,position,new_start);
            construct(new_finish,x);//将第九个元素插入
            ++new_finish;
            //拷贝安插点后的原内容,会被insert(pos,x)调用
            //比如在第三位插入新元素,把前两个元素移动到新地方,在两个元素后插入新元素,再把原来内存中第三个到最后一个元素拷贝到新内存中
            new_finish=uninitialized_copy(postion,finish,new_finish)
        
        
        }
        catch{
                         ...
        }
        //释放原来的空间
        destroy(begin(),end())
        deallocate();
        //调整
        start=new_start;
        finish=new_finish;
        end_of_storage=new_start+len;        
    }
}    

vector的每次扩充会大量调用拷贝和析构

vector iterator是一个指针,换句话说指针可以充当vector的迭代器,算法通过traits得到iterator的几个特性。

template<class T>
struct iterator_traits<T*>
{
    typedef random_access_iterator_tag iterator_category;  //特性1
    typedef T valuetype;//特性2
    typedef T* pointer;  //特性3
    typedef T& reference; //特性4
    typedef ptrdifft differencetype; //特性5
}

 


 

deque

  deque是一个分段连续的容器,连续是假象,分段是事实,迭代器在移动的时候要维持连续的假象,当迭代器走到边界无论是first还是last,他都要通过node回到控制中心,跳到下一个buffer中去。

//bufsize为每个buffer容纳的元素个数,如果bufsiz指定为5,说明每个buffer长度为5,如果是0,要知道一个元素是多大
,如果不超过512B,就用512/elemsize。例如:元素类型为int,一个buffer长度为512/4=103
template<class T,class Alloc=alloc,size_t BufSiz=0> class deque { public: typedef T value_type; typedef __deque_iterator<T,T&,T*,BufSize> iterator; protected: typedef pointer* map_pointer; //T** iterator start; iterator finish; map_pointer map; size_type map_size; public: ... }

 

//deque迭代器源代码
template<class T,class Ref,class Ptf,size_t BufSiz>
Struct __deque_iterator{
    typedef random_access_iterator_tag iterator_category;  //特性1,制造出连续的假象,可以随机访问
    typedef T valuetype;//特性2
    typedef Ptr pointer;  //特性3
    typedef Ref reference; //特性4
    typedef size_t sizetype
    typedef ptrdifft differencetype; //特性5
    typedef T** map_pointer;
    typedef __deque_iterator self;
    T* cur;
    T* first;
    T* last;
    map_pointer node;  //二级指针
...
}

  我们在可以使用insert(pos,elem);在某个位置插入一个元素。如果pos指定插入为最前面的位置,insert函数内部会调用push_front(elem)把元素插到最前方;如果pos为最后位置会调用push_back(elem)把元素插入末端。以上两种情况是刚好在两端插入元素,如果在中间,deque会判断出要插入的位置是离头部近还是离尾部近,假设有一百个元素,我们要在第5个位置插入元素,deque就会把前面4个元素元素向前推,而不是把后面的96个元素向后推,这就是deque的高明之处。

//insert函数实现
iterator insert(iterator position,const value_type &x)
if(position.cur==start.cur) //判断插入的元素是否在最前端
{
    push_front(x);  //调用push_front()
    return start;
}
else if(position.cur==finish.cur) //是否在最末端
{
    push_back(x); //调用push_back()
    iterator tmp = finish;
    --tmp;
    return tmp;
}
else  //在中间插入
{
    return insert_aux(positon,x); //判断插入元素离哪一端近
}

deque怎么模拟连续空间,迭代器++和--是怎么跳到下一个缓冲区?

  

  那么--操作也和++操作类似,指针从右到左移动,先要判断是否到了边界(cur==frist),如果到了边界,就会回到控制中心,找到前一个缓冲区的位置,把迭代器中的cur修改为新buffer的last,frist和last指向新buffer的两端,node指向控制中心原来元素的上一个位置。

  deque的如此设计让用户感觉不到他的分段。如果用户需要一次跳跃n个位置,deque的设计思想:不管是往前跳跃还是往后跳跃,deque内部都会判断是否会跨越缓冲区,跨越几个缓冲区,然后退回控制中心,找到目的缓冲区,再决定还剩几个元素要走。

self& operator+=(difference_type n)
{
    //假设是在连续空间,+n操作目的元素编号
    difference_type offset=n+(cur-frist);
    //目标位置在同一个buffer中
    if(offset>=0&&offset<difference_type(buffer_size()))
        cur+=n;
    else  //目标位置不在同一个buffer中
    {
        //计算需要跨越几个缓冲区
        difference_type node_offset=offset>0?offset/difference_type(buffer_size())
                    :-difference_type((-offset-1)/buffer_size())-1;
        set_node(node+node_offset) //回到控制中心切换到正确的buffer
        //在本buffer中还需要跳跃几个元素
        cur = first+(offset-node_offset*difference_type(buffer_size()));
    }
    return *this;
}

 


 

stack与queue

  

从图中可以看出,queue和stack是包含于deque,只需要关闭deque个几个功能就可以创建出queue和stack,所以不需要重写queue和stact内部功能,只需让他们内含一个deque,然后封掉某些功能。

//queue源码,内部有一个deque,调用deque的功能
template<class T,class Sequence=deque<T>>
class queue
{
public:
    ...
protected:
    Sequence c;  //底层容器
public:
    bool empty() const{return c.empty();}
    size_type size() const {return c.size();}
    reference front(){return c.front();}
    const_reference front() const {return c.front();}
    reference back(){return c.back();}
    const_reference back(){return c.back();}
    void push(const value_type&x){c.push_back(x);}
    void pop(){c.pop_front();}
}

可以看出queue和stack并不是严格意义上的容器,更像是一个适配器,他把别的容器改装一下成为自己。

template<class T,class Sequence=deque<T>>
class stack
{
public:
    ...
protected:
    Sequence c;  //底层容器
public:
    bool empty() const{return c.empty();}
    size_type size() const {return c.size();}
    reference top(){return c.back();}
    const_reference top() const{return c.back();}
    void push(const value_type&x){c.push_back(x);}
    void pop(){c.pop_front();}
}

除了可以用deque作为底层结构,list容器也可以

void test01()
{
    stack<int, list<int>> s; //list为底层结构
    for (size_t i = 0; i < 10; i++)
    {
        s.push(i);
    }
    cout << "stack的size=" << s.size() << endl;
    cout << "top=" << s.top() << endl;
    s.pop();
    cout << "stack的size=" << s.size() << endl;
    cout << "top=" << s.top() << endl;
}
void test02()
{
    queue<int, list<int>>q;      //使用list作为queue的底层结构
    for (size_t i = 0; i < 10; i++)
    {
        q.push(i);
    }
    cout << "size=" << q.size() << endl;
    cout << "front=" << q.front() << endl;
    q.pop();
    cout << "size=" << q.size() << endl;
    cout << "front=" << q.front() << endl;
    cout << "back=" << q.back() << endl;  
}

另外stack可以用vector作为底层结构,queue不能。因为在queue想要出队pop(),会调用pop_front()函数,而vector没有这个功能。

stack和queue不能遍历,不提给迭代器,如果可以访问任意一个元素,就破坏了先进后出和先进先出的行为模式。

 


 

set/mutiset

set/multiset和map/multimap底层都是红黑树结构。

 set/multiset是rb_tree为底层结构,因此有自动排序的特性,排序的依据是key,而set/multiset的元素的value和key合一:value就是key。无法使用set/multiset的iterator来改变元素值,iterator是其底部的RB tree的const iterator,就是为了禁止用户对元素赋值

set容器的key必须是独一无二,他所调用的insert()方法用的是rb_tree的insert_unique()。

multiset容器的key可以重复,是因为他调用的insert()是rb_tree的insert_equal()。

set源代码


 

map/multimap

map/mutimap是以rb_tree为底层结构,因此有自动排序的特性,排序的依据是key。

我们无法通过它的迭代器来改变key,rb_tree把key_type设定为const,但是可以通过key来修改后面的data(key和data组成了value),这一点与set/mutiset不同。set是通过把iterator设为const iterator来禁止修改key,而map是在打包过程中把key设置为const,而包里的data可以修改。

map容器的key必须是独一无二,他所调用的insert()方法用的是rb_tree的insert_unique()。

multimap容器的key可以重复,是因为他调用的insert()是rb_tree的insert_equal()。

map源码

map与multimap关于[]的使用差别

    map<int, string>m1;
    m1.insert(make_pair(1,"liming"));
    m1[2] = string("jenny");//map容器在insert时并没有报错
    multimap<int, string>m2;
    m2.insert(make_pair(2, "jenny"));
    m2[1] = string("zhang");  //error:没有与这些操作数匹配的[]运算符    

由此可见operator[]是map容器所特有的。他要完成:返回与key相对应的data,如果指定的key不存在的话(在上面例子中,m1没有2这个key),会创建出这个key,放在容器中。

 


 


unordered set/multiset与unordered map/multimap

  这个两个容器与上面两个的最大差别是底层的数据结构不同,unordered系列容器使用的是hash table(散列表),散列表理想状态下查找的时间复杂度能达到O(1),并使用拉链法来处理哈希碰撞,当容器的填装因子过大,也就是说元素的个数大于篮子的个数时,容器会把原来散列表打散再扩大重新散列,防止篮子后面的链表结点过长,影响查找效率。

    unordered_set<int> set;
    for (int i = 0; i < 10; i++)
    {
        set.insert(rand() % 30);
    }
    cout << "set.size: "<<set.size() << endl;//容器的大小
    cout << "set.bucket_count:"<<set.bucket_count()<< endl;//篮子的个数
    for (size_t i = 0; i < set.bucket_count(); i++)
    {
        cout << "bucket: 第" << i << "个篮子有" 
            << set.bucket_size(i)<<"个元素" << endl;
    }    
set.size: 8
set.bucket_count:8
bucket: 第0个篮子有1个元素
bucket: 第1个篮子有1个元素
bucket: 第2个篮子有0个元素
bucket: 第3个篮子有2个元素
bucket: 第4个篮子有1个元素
bucket: 第5个篮子有0个元素
bucket: 第6个篮子有1个元素
bucket: 第7个篮子有2个元素
请按任意键继续. . .

这时候把元素曾多

set.size: 14 //容器里的元素
set.bucket_count:64   //篮子个数

hash_table容器分析