-
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_type、iterator_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的定义。使用线性连续空间,以两个迭代器start 和finish 分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器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(size_type n,const T &value)
- push_back(const T &x)
- pop_back()
- erase(iterator first, iterator last)
- erase(iterator position)
- insert(iterator position, size_type n, const T& x)
插入操作可能造成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操作的实现
-
节点操作
- 分配一个节点:get_node
- 释放一个节点:put_node
- 生成(分配并构造)一个节点:create_node(调用get_node、construct函数)
- 销毁(析构并释放)一个节点:destroy_node
- 节点插入:push_back、push_front、 insert
- 节点移除:erase、pop_front、pop_back
- 移除某一数值的所有节点:remove
- 移除数值相同的连续节点:unique
//删除某个节点的例子
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最大的差异:
- deque允许于常数时间内对起头端进行元素的插入或移除操作
- deque没有所谓容量观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来(deque没有必要提供所谓的空间保留功能)
- 除非有必要,应尽可能选择使用 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操作的实现
- deque构造与初始化:deque
- 元素初始化fill_initialize
- 空间分配与成员设定create_map_and_nodes
- 元素初始化fill_initialize
- 插入操作:
- 在队列末尾插入:push_back
- 最后缓冲区只有1个可用空间时:push_back_aux
- 在队列首部插入:push_front
- 第一个缓冲区没有可用空间时:push_front_aux
- 指定位置插入一个元素:insert
- 在首部插入:push_front
- 在尾部插入:push_back
- 在中间插入:insert_aux
- 在队列末尾插入:push_back
- 弹出操作:
- 弹出队列末尾元素:pop_back
- 最后缓冲区没有元素时:pop_back_aux
- 弹出队列首部元素:pop_front
- 第一个缓冲区仅有一个元素时:pop_front_aux
- 弹出队列末尾元素:pop_back
- 清除所有元素:clear
- 清除某个区间的元素:erase
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());
}
};