STL源码剖析

  1. 空间配置器 分为第一级空间配置器,和第二级空间配置器 配合使用 第一级空间配置器分配大内存大于128bytes, 第二级分配较小的小于128bytes的

第一级空间配置器 直接使用malloc分配内存,如果分配成功则返回地址;如果失败的话,首先在抛出内存不足异常前,进行类似c++的new_handle例程处理,该例程由程序员给出,查看是否还有可以释放整理,然后分配的内存,如果没有再抛出异常。

第二级空间配置器 处理小的内存分配,维护一个free_list 空闲待分配内存链表 ,链表连接的空闲内存区以8的倍数大小存储,还维护一个内存池;当有内存分配请求时,首先看是要调用第一级还是第二级空间配置器,当需要调用第二级空间配置器时,便在内存链表寻找最合适的空闲空间返回地址,如果内存表没有合适大小的区域,就向内存池请求分配最接近需求大小的空间若干个,返回给free_list, 并将第一个分配出去,如果内存池枯竭,就向系统空间索要新的空闲内存区间作为内存池使用,如果没有则使用第一级空间配置器,因为第一级配置器可以进行释放整理内存,继续在更大范围内寻找空闲内存,如果找不到,第一级内存配置器就抛出异常。

有三个重要的函数给容器初始化,复制,移动等操作,如下
uninitialized_copy(iterator first,iterator last,iterator first_new)
对内存操作函数copy(),进行封装,直接拷贝一块内存区域的数据到另一块内存区域,不同的是如果对象是一个类,需要用构造函数,则一个个copy。具备commit or rollback特点
uninitialized_fill(iterator first,iterator last,const &T x)
对内存操作函数fill(),进行封装,在fiest到last的一块区域内用x初始化赋值。具备commit or rollback特点,可以抛出异常
uninitialized_fill_n(iterator first,size n,const &T x)
对内存操作函数fill_n(),进行封装,在fiest为起始位置的一块区域内用x初始化赋值。具备commit or rollback特点,可以抛出异常

  1. 迭代器
    STL的中心思想在于:将数据和算法分开,彼此独立设计,最后再以一贴胶着剂将他们搓合在一起。
    迭代器是一种行为类似指针的对象,而指针的各种行为中最常见的也最重要的便是内容提领(dereference)和成员访问(member access),因此迭代器最重要的编程工作就是对operator*和operatoe->进行重载(overloading)工作。

神器 Traits编程技法
迭代器所指对象的型别,称为该迭代器的value type。
traits就是扮演“特性萃取剂”角色,可以让我们获取当前是什么类型从而去优化算法执行效率,在对这个型别进行构造、析构、拷贝、赋值等操作时,就可以采取最有效率的措施。比如copy函数,如果知道了类型是int 或者char*等基本型,那么可以直接进行memove对内存进行复制,执行效率非常高,而如果是一个复杂的类结构,那么就要调用相应的拷贝构造函数一个个进行复制,复制效率就会慢很多。
为了让这个traits(特性萃取剂)能够有效运作,每一个迭代器必须遵循约定,自行以内嵌型别定义的方式定义出相应型别。这是一个约定,谁不遵守这个约定就无法兼容于STL这个大家庭。

image.png

个人觉得trait中首先利用模板的自动推导机制,获取数据类型,然后当如果数据类型是被修饰符修饰的特殊类型,那么就使用偏特化的方法,分类定义五种数据类型,然后就可以解决。
在我理解偏特化,就是有些没办法抽象成统一类型,所以就在完全抽象和不抽象之间定义一个偏特化的概念,这种方法中一半是完全抽象出来的(比如 基本类型抽象成T),另一部分是没有抽象的(比如 指针 const)。
参考:Traits 详解

  1. 容器总结
    c++容器包含deque,list,queue,priority_queue,stack,vector,map,multimap,set,multiset,bitset,forward_list,unordered_map,unordered_multimap,
    unordered_set,unordered_multiset.

内置数组array和valarray
array 数组模板 ,在C++11中才支持
通用格式:array<类型名, 元素个数> 数组名;
注意,因为长度固定,这里的元素个数不能是变量。
长度固定,提供了更好、更安全的接口,执行效率和内置数组相同,可以有效替代内置数组
valarray 面向数值计算的数组,在C++11中才支持
支持很多数值数组操作,如求数组总和、最大数、最小数等。
这两个数组是c++11 新加入的数组模板
有一个slice() 函数具有脚本语言的灵活性
该类主要配合valarray类使用,可以从valarray中提取子数组
slice( size_t _StartIndex,//截取数组的开始位置
const valarray _Len, //子数组的最大长度
const valarray _Stride//相隔多少个元素选中一个
);

1)vector 相关操作
vector可以动态扩充容量,相较array具有更大的灵活性,在进行插入操作时,如果只剩一个空闲位置,就默认扩充size为原数组大小的两倍,在进行数组拼接时如果两倍依然不能满足所需的数组空间,那就按照需要的大小进行扩充。
支持增、删、插、找、排序、拼接操作。

在老的stl中当需要扩充大小时,使用复制构造函数,在最新的stl中扩充大小可能会使用move(),移动复制函数,move()相当于static_cast(),类型转换为引用。

初始化:
vector v1; //vector元素为 int 型
vector v1(3, "3"); //vector元素为string 型 并初始化三个元素 都为"3"
vector v1(10); //vector为10个node元素
vector::iterator it; //定义一个迭代器
vec.size(); //向量大小:
vec.max_size();// 向量最大容量:
vec.resize();//更改向量大小:
vec.capacity();//向量真实大小:
vec.shrink_to_fit();//减少向量大小到满足元素所占存储空间的大小: //shrink_to_fit
元素操作:
v1.push_back() //在数组的最后添加一个数据
v1.pop_back() //去掉数组的最后一个数据
v1.front()     //返回第一个元素(栈顶元素)
v1.begin() //得到数组头的指针,用迭代器接受
v1.end() //得到数组的最后一个单元+1的指针,用迭代器接受
v1.clear() // 移除容器中所有数据
v1.empty() //判断容器是否为空
v1.erase(pos) //删除pos位置的数据
v1.erase(beg,end)// 删除[beg,end)区间的数据
v1.insert(pos,data) //在pos处插入数据
vector 没有查找元素存在性的成员函数,请使用顺序容器的通用方法。
vector v = { 1, 2, 3, 4 };
auto it_1 = find(v.begin(), v.end(), 1); // it_1 = v.begin();

2)list相关操作
c++ STL list模板是一个双向链表

初始化
lista{1,2,3}
lista(n) //声明一个n个元素的列表,每个元素都是0
lista(n, m) //声明一个n个元素的列表,每个元素都是m
lista(first, last) //声明一个列表,其元素的初始值来源于由区间所指定的序列中的元素,first和last是迭代器
resize() //调用resize(n)将list的长度改为只容纳n个元素,超出的元素将被删除。如果n比list原来的长度长,那么默认超出的部分元素置为0。也可以用resize(n, m)的方式将超出的部分赋值为m。
swap() //交换两个链表。a.swap(b)和swap(a, b),都可以完成a链表和b链表的交换。

assign() //有两种使用情况:
(1)a.assign(n, val):将a中的所有元素替换成n个val元素
b中的元素变为10, 10, 10, 10, 10
(2)a.assign(b.begin(), b.end())
a中的元素替换为b的
reverse() //可以实现list的逆置
merge() //a.merge(b) 调用结束后b变为空,a中元素包含原来a和b的元素。
sort() //给list排序
splice() 实现list拼接的功能。将源list的内容部分或全部元素删除,拼插入到目的list。
***********************函数有以下三种声明:**************************
一:void splice ( iterator position, list& x );
二:void splice ( iterator position, list& x, iterator it );
三:void splice ( iterator position, list& x, iterator first, iterator last );
****************************解释:*********************************
position 是要操作的list对象的迭代器
list&x 被剪的对象
对于一:会在position后把list&x所有的元素到剪接到要操作的list对象
对于二:只会把it的值剪接到要操作的list对象中
******************对于三:把first 到 last 剪接到要操作的list对象中*************************
transfer(iterator position, iterator first, iterator last), //迁移操作 [first, last)内的所有元素移动到position之前 类似splice() 函数
swap() //交换两个list
unique() //删除list中重复的元素
元素操作
push_front(x)://把元素x推入(插入)到链表头部
push_back(x)://把元素x推入(插入)到双向队列的尾部
pop_front()://弹出(删除)双向队列的第一个元素

pop_back()://弹出(删除)双向队列的最后一个元素
begin()://向量中第一个元素的迭代器
clear(): //清空list中的所有元素。
empty()://利用empty() 判断list是否为空。
front(): //获得list容器中的头部元素
back(): //获得list容器的最后一个元素。
insert() ://在指定位置插入一个或多个元素
erase() : //删除一个元素或一个区域的元素
rbegin() ://返回指向第一个元素的逆向迭代器
remove() ://从list删除元素 ,并不是真正的删除 只是移到链表最后,通过迭代器仍可访问,真正的移除需要使用erase()函数
remove_if() ://按指定条件删除元素
rend() : //指向list末尾的逆向迭代器

3)deque queue stack相关操作

  • STL中没有queue和stack容器,都是以deque为基础的container adapter(配接器)
  • c++ STL中deque是双向开口的连续线性空间,即可以再deque两端分别进行插入删除操作,其余操作与vector相似,可以动态扩充,但实现方式完全不一样
  • queue是一端只能进一端只能出的队列,实现先进先出的功能,没有遍历操作
  • stack是进出都在一端的栈,实现先进后出的功能,没有遍历操作
    这样基于deque就很容易实现queue和stack的功能。这里重点记一下deque的实现方式

dueue 是一段一段的定量连续空间。一旦有必要就可以在deque的前端或尾端增加新空间。deque通过控制分段的连续空间使整体连续,使用了复杂的迭代器结构。deque采用一块所谓的map(其实就是一个指针数组),每个元素都是指针,指向一段连续性的线性空间,称为缓冲区,缓冲区是deque的储存空间主体(默认是512 bytes)。
在增加元素时,判断头指针和尾指针所在的map节点指向的缓冲区是否还有备用空间,如果有则插入,调整缓冲区的状态,没有的话重换一个map节点,然后调整缓冲区的状态,如果没有多余的map节点,就重新整治map,重新换一个map,就像vector数组容量不足时的操作一样。先申请更大的map空间,将原map内容拷贝过来,释放原map,设定新的map的起始地址与大小。


deque的map图解.png

4)heap 和 priority_queue
大顶堆 任意一个根节点都比子节点的元素大
小顶堆 任意一个根节点都比子节点的元素小
完全二叉树 以数组的形式进行存储(下标从1开始),下标为i的节点的左叶子节点为(2 * i),右叶子节点为(2*i + 1),父节点为(i / 2)

有三个heap算法

  • push_heap()

新加入的元素放在数组最末端然后上溯直到寻找到合适的位置,满足堆得条件约束

  • pop_heap()

将第一个元素与最后一个元素互换位置,然后让下标为1的元素下溯直到寻找到合适的位置,满足堆得条件约束。

  • sort_heap()

进行多次pop_heap(),就可以将一个以数组形式存储的完全堆二叉树,排序,如24,21,16,19,13 sort_heap()之后变成13,16,19,21,24。

  • make_heap()

进行多次push_heap(),将一个无序数组变成heap形式存储在一维数组中,如13,16,19,21,24, sort_heap()之后变成24,21,16,19,13 。

priority_queue就是利用上面heap的方式,具有heap特性的队列,插入的数据以优先级的方式排序,每次插入和取出也都要是以优先级为依据。

以上是序列化容器,接下来是关联式容器 分为两大类
一类是以树,二叉树,平衡二叉树,红黑树作为底层数据结构的关联式容器有map, set, mutlimap, mutliset;
另一类是以hashtable 为底层数据结构的hash_map, hash_set, hash_mutlimap, hash_mutliset

第一类 以树结构为底层数据结构的map, set, mutlimap, mutliset;

  • 二叉搜索树
    任何一个节点的键值一定大于其左子树中的每一个节点的键值,并小于其右子树中每一个节点的键值
  • 平衡二叉搜索树
    由于二叉搜索树可能出现所有节点都在一边的情况,导致查找效率再次变成O(n),所以引入平衡二叉树的概念,使得可以保持某种程度的平衡,一般而言其搜寻时间可节省25%左右。比如AVL tree就是一种额外加了平衡条件的二叉搜索树。
  • AVL tree
    其平衡条件的建立是为了保持整棵树的深度在为O(logN),要求任何节点的左右子树高度相差最多一。
    平衡被破坏的四种条件
    (1)插入点位于X的左子节点的左子树——左左——单旋一次 不平衡节点向右旋转一次
    (2)插入点位于X的左子节点的右子树——左右——双旋转 不平衡节点的下一级节点先向左旋转一次,不平衡节点再向右旋转一次
    (3)插入点位于X的右子节点的左子树——右左——双旋转 不平衡节点的下一级节点先向右旋转一次,不平衡节点再向左旋转一次
    (4)插入点位于X的右子节点的右子树——右右——单旋一次 不平衡节点向左旋转一次
  • 红黑树

STL唯一实现的一种搜寻树,作为关联式容器的底部机制之用。不仅在树形的平衡上表现的不错,在效率表现和实现复杂度上也保持相当的平衡。
满足以下四个规则
(1)每个节点不是红色就是黑色
(2)根节点为黑色
(3)节点为红,子节点必须为黑(不能有连续出现的红色相连接,黑色可以)
(4)任一节点至NULL(树尾端)的任何路径,所含之黑节点数必须相同。

插入节点步骤
假设我们插入的新节点为 X
1 .将新插入的节点标记为红色
2 .如果 X 是根结点(root),则标记为黑色
3 .如果 X 的 parent 不是黑色,同时 X 也不是 root:
----3.1 如果 X 的 uncle (叔叔) 是红色
--------3.1.1 将 parent 和 uncle 标记为黑色
--------3.1.2 将 grand parent (祖父) 标记为红色
--------3.1.3 让 X 节点的颜色与 X 祖父的颜色相同,然后重复步骤 2、3
!!!这里面可能会涉及到和AVL tree相同的四种旋转,见下
参考红黑树详解

插入节点后保持二叉树符合红黑树规则的伪代码

x->color = red
while(x!=root 并且 x->parent->color==red)
{
    //新节点为父节点之左子节点
    if(x->parent==x->parent->parent->left)
    {
        //y等于伯父节点
        y=x->parent->parent->right;
        //如果伯父节点存在且为红色
        if(y && y->color==red)
        {
            //更改祖父节点的子节点为黑色 祖父节点为红色; 
            x->parent->color=red;
            y->color=red;
            //x上溯 
            x=x->parents
        }
        else  //无伯父节点 或伯父节点为黑
        {
            if(x==x->parent->right)
            {
                x=x->parent;
                //左旋
                rotate_left(x);
                
            }
            x->parent->color=black;
            x->parent->parent->color=red;
            //右旋
            rotate_right(x->parent->parent);
        }
    }
    else  //父节点为祖父节点之右子节点
    {
        //y等于伯父节点
        y=x->parent->parent->right;
        //如果伯父节点存在且为红色
        if(y && y->color==red)
        {
            //更改祖父节点的子节点为黑色 祖父节点为红色; 
            x->parent->color=red;
            y->color=red;
            //x上溯 
            x=x->parents
        }
        else  //无伯父节点 或伯父节点为黑
        {
            if(x==x->parent->right)
            {
                x=x->parent;
                //左旋
                rotate_right(x);
                
            }
            x->parent->color=black;
            x->parent->parent->color=red;
            //右旋
            rotate_left(x->parent->parent);
        }
    }
} //while 结束
root->color=black;

set容器

set的特性是,所有元素都会根据元素的键值自动被排序,STL提供了一组set/mutliset相关算法,包括交集(set_intersection)、联集(set_union)、差集(set_difference)、对称差集(set_symmetric_difference)。
标准STL Set 即以RB-tree为底层机制。

常用操作
begin()    ,返回set容器的第一个元素
end()      ,返回set容器的最后一个元素
clear()    ,删除set容器中的所有的元素
empty()    ,判断set容器是否为空
max_size()   ,返回set容器可能包含的元素最大个数
size()      ,返回当前set容器中的元素个数
rbegin     ,返回的值和end()相同
rend()     ,返回的值和rbegin()相同
count() 用来查找set中某个某个键值出现的次数。这个函数在set并不是很实用,因为一个键值在set只可能出现0或1次,这样就变成了判断某一键值是否在set出现过了。
equal_range() ,返回一对定位器,分别表示第一个大于或等于给定关键值的元素和 第一个大于给定关键值的元素,这个返回值是一个pair类型,如果这一对定位器中哪个返回失败,就会等于end()的值。
erase(iterator) ,删除定位器iterator指向的值
erase(first,second),删除定位器first和second之间的值
erase(key_value),删除键值key_value的值
find() ,返回给定值值得定位器,如果没找到则返回end()。
insert(key_value); 将key_value插入到set中 ,返回值是pair::iterator,bool>,bool标志着插入是否成功,而iterator代表插入的位置,若key_value已经在set中,则iterator表示的key_value在set中的位置。
inset(first,second);将定位器first到second之间的元素插入到set中,返回值是void.
lower_bound(key_value) ,返回第一个大于等于key_value的定位器
upper_bound(key_value),返回最后一个大于key_value的定位器

map容器

map的特性是所有元素都会根据键值自动排序。map的所有元素都是pair,同时拥有实值和键值,map不允许两个元素拥有相同的键值
标准STL map即以RB-tree为底层机制

常用操作
begin() 返回指向map头部的迭代器
clear() 删除所有元素
count() 返回指定元素出现的次数
empty() 如果map为空则返回true
end() 返回指向map末尾的迭代器
rbegin() 返回一个指向map尾部的逆向迭代器
rend() 返回一个指向map头部的逆向迭代器
size() 返回map中元素的个数
equal_range() 返回特殊条目的迭代器对
erase() 删除一个元素
find() 查找一个元素
get_allocator() 返回map的配置器
insert() 插入元素
key_comp() 返回比较元素key的函数
lower_bound() 返回键值>=给定元素的第一个位置
upper_bound() 返回键值>给定元素的第一个位置
max_size() 返回可以容纳的最大元素个数
swap() 交换两个map
value_comp() 返回比较元素value的函数
map.count(key) 返回拥有某 key 的元素个数(0 or 1)
map.find(key) 返回键等于 key 的元素的迭代器,如果 map 中不存在该 key,返回
map.at(key) 返回键等于 key 的元素的键值 value,如果 map 中不存在该 key,运行时会抛出异常std::out_of_range
map[key] 访问键值为key的元素

mutliset和mutlimap

mutliset与set操作完全相同,唯一的差别在于他允许元素值重复,因此它的插入操作采用的是底层机制RB-tree的insert_equal()而非insert_unique();

mutlimap与map操作完全相同,唯一的差别在于他允许键值重复,因此它的插入操作采用的是底层机制RB-tree的insert_equal()而非insert_unique();

第二类 以hash_table为底层数据结构的hash_map, hash_set, hash_mutlimap, hash_mutliset;

  • hash_table

使用某种映射函数,负责将某一元素映射为一个"大小可接受之索引",这样的函数称为hash function(散列函数)。使用hash function会带来一个问题:可能有不同的元素被映射到相同的位置。这边是“碰撞”问题,解决碰撞问题的方法有许多种,包括线性探测、二次探测、开链等做法。

负载系数: 意指元素个数除以表格大小。元素系数永远在0~1之间——除非采用开链策略。

  • 线性探测

比如,取模计算元素下标,如果重复就顺延一个位置,到最后就从头开始,直到寻找到空位。
问题:容易产生主集团,及大面积位置因为顺延的问题而被占用。

  • 二次探测

比如,取模计算元素下标,如果重复按照公式F(i)=i^2,顺延到下一个位置,到最后就从头开始,直到寻找到空位。这主要用于解决线性探测易产生主集团的问题。
问题:容易产生次集团,两个元素京hash function计算出来的位置若相同,则插入时所探测的位置也相同,形成某种浪费。消除次集团的方法也有,比如复式散列。

一般为了避免探测次数过多,使表格大小为质数

  • 开链

这种做法是在每一个表格元素中维护一个list,hash function为我们分配某一个list,然后我们在那个list身上执行元素的插入、搜寻、删除等操作。

STL中的hash_table采用的开链法解决碰撞问题的

hash_table中使用vector数组以利动态扩充,然后每个元素是一个链表,不是STL链表,就是自定义的单向链表。
其中使用的hash function 可以自定义,在中也提供了数个现成的hash function,但是这其中只能对int,long, short,char,const char格式的数据进行hash。double,float,string无法使用STL中提供的hash function进行hash,需要自己提供。*

hash_set容器

hash_set是以hashtable为底层机制,所供应的操作接口都是转调用hashtable的操作行为;以RB-tree为底层机制的set容器有自动排序功能而hash_set没有。hashtable无法处理的型别,hash_set也无法处理。

常用操作:
函数接口与set相似

hash_map容器

hash_map是以hashtable为底层机制,所供应的操作接口都是转调用hashtable的操作行为;以RB-tree为底层机制的map容器有自动排序功能而hash_map没有。hashtable无法处理的型别,hash_map也无法处理。

常用操作:
函数接口与map相似

hash_mutliset和hash_mutlimap

hash_mutliset与hash_set操作完全相同,唯一的差别在于他允许元素值重复,因此它的插入操作采用的是底层机制hash_table的insert_equal()而非insert_unique();

hash_mutlimap与hash_map操作完全相同,唯一的差别在于他允许键值重复,因此它的插入操作采用的是底层机制hash_table的insert_equal()而非insert_unique();

STL算法

C++ STL算法一般都包含多个版本,必如比较大小的sort算法,就可以自定义比较函数
作为仿函数(functor)传递进去。再比如质变算法(下面提及什么是质变)一般都会提供两个版本:一个是if-in place(就地进行版);一个是copy版(另地进行),先复制一份副本,然后在副本上进行修改并返回副本。copy版一般就是replace_copy后缀加上copy,但也有一些算法没有copy版,必如sort,需要自己复制一份副本。

总揽

算法名称 算法用途 质变 所在文件
accumulate 元素累计
adjacent_difference 相邻元素的差额 是if in-place
adjacent_find 查找相邻而重复(或符合某条件)的元素
binary_search 二分查找
copy 复制 是if in-place
copy_backward 逆向复制 是if in-place
copy_n 复制n个元素 是if in-place
count 计数
count_if 在特定条件下计数
equal 判断两个区间相等与否
equal_range 试图在有序区间中寻找某值(返回一个上下限区间)
fill 改填元素值
fill_n 改填元素值,n次
find 循环查找
find_if 循环查找符合特定条件
find_end 查找某个子序列的最后一个出现点
find_first_of 查找某些元素的首次出现点
for_each 对区间内的每一个元素实行某操作
generate 以特定操作之运算结果填充特定区间内的元素
generate_n 以特定操作之运算结果填充n个元素内容
includes 是否涵盖于某序列之中
inner_product 内积
inplace_merge 合并并就地替换(覆盖上去)
iota* 在某区间填入某指定值的递增序列
is_heap* 判断某区间是否为一个heap
is_sorted* 判断某区间是否已排序
iter_swap 元素互换
lexicographical_compare 以字典顺序进行比较
lower_bound "将指定元素插入区间之内而不影响区间之原本排序"的最低位置
max 最大值
max_element 最大值所在位置
merge 合并两个序列 是if in-place
min 最小值
min_element 最小值所在位置
mismatch 找到不匹配点
next_permutation 获得下一个排列组合
nth_element 重新安排序列中的第n个元素的左右两端
partial_sort 局部排序
partial_sort_copy 局部排序并复制到他处 是if in-place
partial_sum 局部求和
partition 分割
prev_permutation 获得前一个排列组合
power* 幂次方。表达式可指定
random_shuffle 随机重排元素
random_sample* 随机取样 是if in-place
random_sample_n* 随机取样
remove 删除某类元素(但不删除)
remove_copy 删除某类元素并将结果复制到另一个容器
remove_if 有条件的删除某类元素
remove_copy_if 有条件的删除某类元素并将结果复制到另一个容器
reverse 反转元素次序
reverse 反转元素次序并将结果复制到另一个容器
rotate 旋转
rotate_copy 旋转,并将结果复制到另一个容器
search 查找某个子序列
search_n 查找"连续发生n次"的子序列
set_difference 差集 是if in-place
set_intersection 交集 是if in-place
set_symmetric_difference 对称差集 是if in-place
set_union 并集 是if in-place
sort 排序
stable_partition 分割并保持元素的相对次序
stable_sort 排序并保持等值元素的相对次序
swap 交换(对调)
swap_ranges 交换(指定区间)
transform 以两个序列为基础,交互作用产生第三个序列
unique 将重复的元素折叠缩编,使成唯一
unique_copy 将重复的元素折叠缩编,使成唯一,并复制到他处 是if in-place
upper_bound "将指定元素插入区间之内而不影响区间之原本排序"的最高位置
make_heap 制造一个heap
pop_heap 从heap中取出一个元素
push_heap 将一个元素推进heap内
sort_heap 制造一个heap

非质变算法(nonmutating algorithms)意思是不改变操作对象之值
所有泛型算法的前两个参数都是一对迭代器。简仓称为first和last,用以表示算法的操作区间。STL习惯采用前闭后开表示法,写成 [first,last), 表示区间涵盖first至last(不含last)之间的所有元素。当first==last时,上述所表现的便是一个空区间

数值算法 实现于

accumulate 累加运算

image.png

版本二多一个二元操作运算,传入一个运算符(BinaryOperation binary_op)就可以实现多种累计运算,如传入乘法,就可以计算累乘。

基本算法

fill_n(), 将[first, last)内的前n个元素该填新值,返回的迭代器指向被填入的最后一个元素的下一个位置

image.png

那么就会有一个问题?如果n超越了容器的现有大小,会造成什么结果?
从源码容易知道,由于每次迭代进行的都是assignment操作,是一种覆写(overwrite)操作,所以一旦操作区间超越容器大小,就会造成不可预期的结果。解决办法之一是,利用insert()产生一个具有插入(insert)而非覆写(overwrite)能力的迭代器。inserter()可产生一个用来修饰迭代器的配接器(iterator adapter)。用法如下:

int ia[3]={0, 1, 2};
vector iv(ia, ia+3);
fill_n(inserter(iv.begin()), 5, 7);

copy 复制一段区间的内容到另一端


image.png

通过上面一层层的调用,最后达到匹配使用最优的函数完成复制的功能,以达到最高的效率。复制时有可能出现目标区域与源区域重叠的部分,那就看调用哪个函数了,如果调用的是memove就不会对结果产生什么影响,因为memove是先保存一个副本再进行复制的,所以不会弄脏原来的内容。
在参数的层层传递中,通过利用traits技巧判断元素型别,而最终决定该调用哪一个copy函数。


image.png

image.png

image.png

image.png

image.png

image.png

image.png

仿函数 (函数对象)

仿函数是将“function call 操作符”重载,的一种class,任何算法接受仿函数作为参数时,总是在演算过程中调用仿函数的operator()

仿函数的作用是将某种“操作”当做算法的参数,一般的做法是将该“操作”设计为一个函数,再将函数指针当做算法的一个参数。但由于函数指针不能满足STL对抽像性的要求,也不能让满足软件积木的要求——函数指针无法和其他组件(如配接器)搭配,产生更灵活的变化。
在STL中使用仿函数的做法,叫做函数对象更加贴切一点,仿函数就语言层面而言是个class,再以该仿函数产生一个对象,并以此对象作为算法的一个参数。


image.png

image.png

可配接(Adaptable)的关键
STL仿函数也有能力被函数配接器修饰,为了拥有配接能力,每一个仿函数必须定义自己的相应型别,就像迭代器如果要融入整个STL大家庭,也必须依照规定定义自己的5个相应型别。这些型别是为了让配接器能够取出,获得仿函数的某些信息。相应型别都是一些typedef,所以必要操作在编译期就全部完成了,对程序的执行效率没有任何影响,不带来任何额外负担。
为方便起见,定义了两个classes,分别代表一元仿函数和二元仿函数(STL不支持三元仿函数),其中没有成员变量和成员函数,只有一些型别定义。任何仿函数,只要一个人需求选择继承其中一个class,便自动拥有了那些相应型别,也就自动拥有了配接能力。

  • unary_function
    用来呈现一元函数的参数型别和回返值型别,定义如下
//STL规定,每一个Adaptable Unary Function都应该继承此类别
template 
struct unary_function
{
typedef Arg argument_type;
typedef Result result_type;
}

实现一个仿函数如下,示例

//以下仿函数继承unary_function
template 
struct negate:public unary_function
{
T operator()(const T& x) const {return -x;}
}
//以下配接器用来表示某个仿函数的逻辑负值
template 
class unary_negate
...
public:
   bool operator() (const typename Predicate::argunment_type& x) const 
{
...
};
  • binary_function
    用来呈现二元函数的参数型别和回返值型别,定义如下:
//STL规定,每一个Adaptable Binary Function都应该继承此类别
template 
struct binary_function
{
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
}

示例与unary_function相似,见上述用法。

用法示例

image.png

配接器

配接器(adapters)在STL组件的灵活组合运用功能上,扮演着轴承、转换器的角色。Adapter这个概念,事实上是一种设计模式。

配接器在STL中有以下三种应用

  • 应用于容器 container adapters

STL提供的两个容器queue和stack,其实都是配接器。他们修饰deque的接口而成就出另一种容器风貌。

  • 应用于迭代器 iterator adapters

STL提供许多应用于迭代器身上的配接器,包括insert iterator,reverse iterators,iostream iterator。
insert iterator,可以将一般迭代器的赋值操作转变为插入操作。比如,专司尾端插入操作的back_insert_iterator;专司头端插入操作的front_insert_iterator;以及可从任意位置执行插入操作的insert_iterator

image.png

reverse iterator,可以将一般迭代器的行进方向逆转,使原本应该前进的operator++操作变成后退操作。这种倒转筋脉的性质运用在“从尾端开始进行”的算法上,有很大的方便性。
iostream iterator,可以将迭代器绑定到某个iostream对象身上。绑定到istream对象(例如std::cin)身上的,称为ostream_iterator,拥有输出功能。
image.png

示例

image.png

image.png

  • 应用于仿函数 functor adapters

functor adapters(亦称为function adapters)是所有配接器中数量最庞大的一个族群,其配接灵活度也是前二者所不能及。可以多次叠加配接。这些配接操作包括系结(bind)、否定(negate)、组合(compose)、以及对一般函数或者成员函数的修饰(使其成为一个仿函数)。
functor adapters的价值在于,通过它们之间的绑定、组合、修饰能力。几乎可以无限制地创造出各种可能的表达式。例如:


image.png

由于仿函数就是将function call操作符重载的一种class,而任何孙发就受一个仿函数时,总是在其演算过程中调用该仿函数的operator(),这使得不具备仿函数之形、却有真函数之实的“一般函数”和“成员函数”得以无缝隙地与其它配接器或算法结合起来。
请注意,所有期望获得配接能力的组件,本身都必须是可配接的,换句话说,一元函数必须继承自unary_function,二元函数必须继承自binary_function,成员函数必须以mem_fun处理过。一个未经ptr_fun处理过的一般函数,虽然也可以函数指针的形式传递给STL算法使用,却无法拥有任何配接能力。

image.png

你可能感兴趣的:(STL源码剖析)