STL 6大组件
-
容器(Container)
-
迭代器(Iterator)
-
算法(Algorithm)
-
仿函数(Function object)
如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数) -
迭代适配器(Adaptor)
可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。 -
空间配置器(Allocator)
为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。
容器
序列容器
每个元素都有固定位置,该位置取决于插入时机和地点,和元素值无关
Vectors:将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取),数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时;
Deques:是“double-ended queue”的缩写,可以随机存取元素(用索引直接存取),数组头部和尾部添加或移除元素都非常快速。但是在中部或头部安插元素比较费时;
Lists:双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)),在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针;
关联式容器
元素位置取决于特定的排序准则,和插入顺序无关
Sets/Multisets:内部的元素依据其值自动排序,Set内的相同数值的元素只能出现一次,Multisets内可包含多个数值相同的元素,内部由二叉树实现,便于查找;
Maps/Multimaps:Map的元素是成对的键值/实值,内部的元素依据其值自动排序,Map内的相同数值的元素只能出现一次,Multimaps内可包含多个数值相同的元素,内部由二叉树实现,便于查找;
容器使用时机
vector | deque | list | set | multiset | map | multimap | |
---|---|---|---|---|---|---|---|
典型内存结构 | 单端数组 | 双端数组 | 双向链表 | 二叉树 | 二叉树 | 二叉树 | 二叉树 |
可随机存取 | 是 | 是 | 否 | 否 | 否 | 对key而言:不是 | 否 |
元素搜寻速度 | 慢 | 慢 | 非常慢 | 快 | 快 | 对key而言:快 | 对key而言:快 |
元素安插移除 | 尾端 | 头尾两端 | 任何位置 |
迭代器
Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。迭代器和 C++ 的指针非常类似
迭代器的作用:能够让迭代器与算法不干扰的相互发展,最后又能无间隙的粘合起来,重载了*,++,==,!=,=运算符。用以操作复杂的数据结构,容器提供迭代器,算法使用迭代器;
- 前向迭代器(forward iterator)
*假设 p 是一个前向迭代器,则 p 支持 ++p,p++,p 操作,还可以被复制或赋值,可以用 == 和 != 运算符进行比较。此外,两个正向迭代器可以互相赋值。
-
双向迭代器(bidirectional iterator)
双向迭代器具有正向迭代器的全部功能,除此之外,假设 p 是一个双向迭代器,则还可以进行 --p 或者 p-- 操作(即一次向后移动一个位置)。 -
随机访问迭代器(random access iterator)
随机访问迭代器具有双向迭代器的全部功能。除此之外,假设 p 是一个随机访问迭代器,i 是一个整型变量或常量,则 p 还支持以下操作: -
p+=i:使得 p 往后移动 i 个元素。
-
p-=i:使得 p 往前移动 i 个元素。
-
p+i:返回 p 后面第 i 个元素的迭代器。
-
p-i:返回 p 前面第 i 个元素的迭代器。
-
p[i]:返回 p 后面第 i 个元素的引用。
算法
常用遍历算法:
/*
for_each算法 遍历容器元素
@param beg 开始迭代器
@para_end 结束迭代器
@para_callback 函数回调或者函数对象
@return 函数对象
*/
/*
transform算法 将指定容器区间元素搬运到另一个容器中
注意: transform 不会给目标容器分配内存,所以需要我们提前分配好内存
@param beg 开始迭代器
@para_end 结束迭代器
@para_beg2 目标容器开始迭代器
@para_callback 函数回调或者函数对象
@return 目标容器迭代器
*/
常用查找算法:
/*
find算法 查找元素
@param beg 开始迭代器
@para_end 结束迭代器
@para_callback 函数回调或者函数对象
@return 函数对象
*/
/*
adjacent_find 查找相邻重复元素
@param beg 开始迭代器
@para_end 结束迭代器
@para_callback 函数回调或者函数对象
@return 返回相邻元素的第一个位置的迭代器
*/
/*
binary_search 算法 二分查找法
注意:在无序序列中不可用
@param beg 开始迭代器
@para_end 结束迭代器
@param_value 查找元素
@return bool 查找返回true,否则false
*/
/*
其他算法:
count
count_if
*/
常用排序算法:
/*
merge算法 容器元素合并,并存储到另一个容器中 这两个容许 必须也是有序的
目标容器需开辟空间
@param beg1 开始迭代器
@para_end1 结束迭代器
@param beg2 开始迭代器
@para_end2 结束迭代器
@para dest 目标容器开始迭代器
*/
/*
sort算法 容器元素排序
@param beg 开始迭代器
@para_end 结束迭代器
@para_callback 函数回调或者函数对象
*/
//random_shuffle(iterator beg,iterator end) 洗牌
//reverse(iterator beg,iterator end) 翻转
常用拷贝和替换算法:
/*
copy算法 将容器内指定范围的元素拷贝到另一容器
@param beg 开始迭代器
@para_end 结束迭代器
@para dest 目标容器起始迭代器
*/
/*
replace算法 将容器内指定范围的旧元素修改为新元素
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param oldvalue 旧元素
@param newvalue 新元素
*/
/*
replace_if算法 将容器内指定范围满足条件的元素替换为新元素
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param callback 函数回调或者谓词(返回bool类型的函数对象)
@param newvalue 新元素
*/
/*
swap算法 互换两个容器的元素
@param 源容器
@param 目标容器
*/
//copy(v.begin(),v.end(),ostream_iterator<int(cout," ")>)
常用算术生成算法:
/*
accumulate算法 计算容器元素累计总和
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param value 起始累加值
*/
/*
fill算法 向容器中添加元素
@param beg 容器开始迭代器
@param end 容器结束迭代器
@param value t填充元素
*/
常用集合算法:
/*
set_intersection算法 求两个集合的交集
注意:两个集合必须是有序序列
@param beg1 开始迭代器
@para_end1 结束迭代器
@param beg2 开始迭代器
@para_end2 结束迭代器
@para dest 目标容器开始迭代器
@return 目标容器的最后一个元素的迭代器地址
*/
/*
set_union算法 求两个集合的并集
注意:两个集合必须是有序序列
@param beg1 开始迭代器
@para_end1 结束迭代器
@param beg2 开始迭代器
@para_end2 结束迭代器
@para dest 目标容器开始迭代器
@return 目标容器的最后一个元素的迭代器地址
*/
/*
set_difference算法 求两个集合的差集
注意:两个集合必须是有序序列
@param beg1 开始迭代器
@para_end1 结束迭代器
@param beg2 开始迭代器
@para_end2 结束迭代器
@para dest 目标容器开始迭代器
@return 目标容器的最后一个元素的迭代器地址
*/
仿函数
仿函数的通俗定义:仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语法几乎和普通函数调用一样,不过作为仿函数的类,都必须重载 operator() 运算符。
仿函数的作用主要在哪里?
STL 所提供的各种算法,往往有两个版本,其中一个版本表现出最常用(或最直观)的某种运算,第二个版本则表现出最泛化的演算流程,允许用户“以template 参数来指定所要采取的策略”。
要将某种 “操作” 当作算法的参数,唯一办法就是先将该 “操作”(可能有数条以上的指令) 设计为一个函数,再将函数指针当作算法的一个参数;或是将该 “操作” 设计成一个所谓的仿函数(就语言层面上来说是个类),再以该仿函数产生一个对象,并以此对象作为算法的一个参数。
根据以上陈述,既然函数指针可以达到 “将数组操作当作算法的参数”,那又何须仿函数呢?原因在于函数指针毕竟不能满足 STL 对抽象性的要求,也不能满足软件积木的要求——函数指针无法和STL 其它组件(如适配器 adapter)搭配,产生更灵活的变化。同时,函数指针无法保存信息,而仿函数可以。
class MyPrint:public binary_function<int,int,void>
{
public:
void operator()(int v,int Start) const{
cout<<v+Start<<endl;
}
};
void test(){
vector<int>v;
for(int i =1;i<10;i++){
v.push_back(i*10);
}
int num;
cin>>num;
for_each(v.begin(),v.end(),std::bind2nd(MyPrint(),num));
}
STL 仿函数的分类,若以操作数的个数划分,可分为一元和二元仿函数;若以功能划分,可分为算术运算、关系运算、逻辑运算三大类。任何应用程序想使用 STL 内建的仿函数,都必须包含 #include 头文件,SGI 则将它们实际定义于 <stl_funcation.h> 中。
STL 仿函数应该能够被函数适配器配接,彼此像积木一样串接。为了拥有配接能力,每一个仿函数必须定义自己的相应类别,就像迭代器如果要融入整个 STL 大家庭,也必须依照规定定义自己的各个类别一样,这些相应类别定义是为了让适配器能够取出并获得仿函数的某些信息(仿函数能够保存信息,函数指针则不能)。相应类别都只是一些typedef,所有必要操作都在编译期就全部完成了,对程序的执行效率没有任何影响,不带来任何额外负担。
仿函数的类别主要是函数参数类型和传回值类型不同。为了方便起见,<stl_funcation.h> 定义了两个 classes,分别代表一元仿函数和二元仿函数(STL不支持三元仿函数),其中没有任何data members 或 member functions,唯有一些型别定义。任何仿函数,只要依个人需求选择继承其中一个 class,便自动拥有了那些相应类别,也就自动拥有了配接能力。
//unary_function 用来呈现一元函数的参数型别和返回值型别
template<class Arg, class Result>
struct unary_function
{
typedef Arg argument_type;
typedef Result result_type;
};
//binary_function用来呈现二元函数的第一参数型别、第二参数型别,以及返回值型别
template <class Arg1, class Arg2, class Result>
struct binary_function
{
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
};
适配器
配接器在STL组件中,扮演者轴承、转换器的角色。Adapter这个概念,事实上是一种设计模式。定义如下:将一个class 的接口转换为另一个 class 的接口,使原本因接口不兼容而不能合作的 class,可以一起运作。
STL所提供的配接器,可以分为三类:
1、容器适配器,改变容器的接口
2、迭代器适配器,改变迭代器的接口
3、仿函数适配器,改变仿函数的接口
容器适配器
容器适配器本质上还是容器,只不过此容器模板类的实现,利用了大量其它基础容器模板类中已经写好的成员函数。当然,如果必要的话,容器适配器中也可以自创新的成员函数。
STL 提供了 3 种容器适配器,分别为 stack 栈适配器、queue 队列适配器以及 priority_queue 优先权队列适配器。其中,各适配器所使用的默认基础容器以及可供用户选择的基础容器,如表 1所示。
迭代器适配器
C++ STL 标准库中迭代器大致分为 5 种类型,分别是输入迭代器、输出迭代器、前向迭代器、双向迭代器以及随机访问迭代器。值得一提的是,这 5 种迭代器是 STL 标准库提供的最基础的迭代器,很多场景中遍历容器的需求,它们并不适合。
所谓迭代器适配器,其本质也是一个模板类,比较特殊的是,该模板类是借助以上 5 种基础迭代器实现的。换句话说,迭代器适配器模板类的内部实现,是通过对以上 5 种基础迭代器拥有的成员方法进行整合、修改,甚至为了实现某些功能还会添加一些新的成员方法。由此,将基础迭代器“改头换面”,就变成了本节要讲的迭代器适配器。
本质上讲,迭代器适配器仍属于迭代器,可以理解为是基础迭代器的“翻新版”或者“升级版”。同时,“xxx 迭代器适配器”通常直接称为“xxx 迭代器”。
仿函数适配器
仿函数适配器,相比于其他适配器更加地灵活,可以自由地组合适配。目前提供的适配操作包括以下这些:
- 联结(bind)。通过bind,我们仿函数与参数进行绑定,可实现算法所需的条件判断功能,例如判断小于12的元素时,可使用bind2nd(less(),12),就可以达到目的。
- 否定(negate)。这里就是取反的操作,例如not1(bind2nd(less(),12)),就可判断不小于12的元素。
- 组合(compose)。当算法的判断条件需要进行一些复杂的数学运算时,即可采用这种适配操作。例如对每个元素v进行(v+2)*3操作,就可表示为compose1(bind2nd(multiplies(),3),bind2nd(plus(),2))。
- 一般函数适配器。一般函数可以当做仿函数供STL算法使用,但无配接能力,需要将其包装成仿函数,其原理就是在仿函数的运算符()内执行其所包装的函数即可。
- 成员函数适配器。这里将成员函数包装成仿函数,从而可使用成员函数搭配各种泛型算法。当容器内存储的是对象的实体时,需使用mem_fun_ref进行适配;当容器内存储的是对象的指针时,需使用mem_fun进行适配。