文章内容参考自侯捷C++STL和泛型系列教程以及《STL源码剖析》
作为标准库的重要组成部分,STL占据了标准库一半以上的内容,它由六个部分组成的:
分配器(Allocators)
容器(Containers)
泛型算法(Algorithms)
迭代器(Iterators)
仿函数(Functors)
适配器(Adapters)
在本文中,我将主要讲述六大部件的功能和他们之间的联系,以及在了解STL的实现代码之前必须知道的一些问题,而各个部件的详细描述我会在后边的专题文章中进行描述。
1、容器作为STL的主体,是许多不同的数据结构
2、分配器为容器的实现分配应有的空间
3、泛型算法用来处理容器中的数据
4、迭代器是泛型算法和容器之间的粘合剂
5、仿函数使得算法可以有更加灵活的自定义模式
6、适配器保证了自定义的功能可以和STL中现有的功能相融合
我们用一段简单的程序来体会一下六大部件的作用:
#include
#include //count_if
#include //less bind
#include
using namespace std;
int main(void)
{
//创建一个容器
int buf[] = { 27,210,12,47,109,83 };
//这里刻意写出第二模板参数,是为了解释分配器的作用
vector< int, allocator<int> > v(buf, buf + 6);
using namespace std::placeholders;//使用 占位符_1所在的 命名空间
//输出vector中 小于40的 元素的个数
cout << count_if(v.begin(), v.end(), bind( less<int>(), _1, 40 ) ) << endl;
//输出2
return 0;
}
这里的vector是一个容器,用来储存数据,它的第二模板参数allocator是为当前容器分配内存的分配器(一般情况下省略不写,默认就会使用allocator< T >)。
count_if( )是一个泛型算法,他的声明如下:
//返回范围 [first, last) 中,对于判定标准p返回true的元素的个数
//这里的p应该是一个一元谓词,对需要进行计数的元素返回为true
template< class InputIt, class UnaryPredicate >//模板参数
typename iterator_traits<InputIt>::difference_type//返回值
count_if( InputIt first, InputIt last, UnaryPredicate p );//函数名及参数列表
bind( )是一个适配器,声明如下:
//函数模板 bind 生成 f 的转发调用适配器。调用此适配器等价于以一些绑定到 args 的参数调用 f 。
//这里暂时可以理解为,将Args中的元素作为参数传递给仿函数f,不需要绑定的元素使用占位符_1等代替。
template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
less( )是一个仿函数,可能定义如下:
//进行比较的函数对象。调用类型 T 上的 operator< ,除非特化。
template< class T = void >
struct less
{
constexpr bool operator()(const T &lhs, const T &rhs) const
{//可能实现方式
return lhs < rhs;
}
}
这里我们分析一下最后一句调用的层次:
//输出区间 [ v.begin(),v.end() ) 中小于40的元素的个数
count_if(v.begin(), v.end(), bind( less<int>(), _1, 40 ) )
这里count_if( )需要两个迭代器和一个一元谓词,两个迭代器自然没问题,v.begin()和v.end()直接就满足调用的要求,但是对于最后一个参数p却不能直接满足需求。
我们想要统计小于40的元素,所以这个谓词需要对小于40的元素返回true。标准库中的仿函数less< int >是一个二元谓词,用来比较两个元素的大小,所以我们就需要提前绑定他的第二参数为40,让输入的数据和40去比较,适配器bind( )就是用来满足这个需求的。
如果看过我之前的文章,对于分配器的概念应该不会陌生:他是用来分配内存空间的。
但是和在内存分配中描述的分配器目标有些许不同:
1. 在内存分配中,分配器的主要目的是为了降低malloc产生的内存开销
2. 在STL中,分配器的主要目的是用来为容器存储的数据提供内存空间
关于内存分配和内存分配中的分配器,如果有兴趣可以参考我之前的文章:
C++内存分配详解三:内存分配模型
C++内存分配详解五:std::alloc源码剖析
C++内存分配详解六:malloc()详解
在后边的文章中,我会展示出一个简单的STL分配器,虽然它并不符合STL的标准,但是可以用来作为我们自己的STL的分配器去使用。
首先,容器分为三大类:顺序容器、关联式容器、无序容器。我个人比较倾向于分为两类,因为关联式容器和无需容器都是关联式容器,只是实现方法上有些许的不同,这在后边的文章中会详细讲到。
顺序容器:进行顺序存储的容器
array(数组)
vector(向量)
deque(双向队列)
list(双向链表)
解过的人可能会问static和heap呢?其实这两个严格意义上不能算是容器,而应该是适配器
关联式容器: 主要用来查询的容器,创建之后自带顺序
set/multiset(集合)
map/multiset(表)
关联式容器底层使用红黑树实现
无序容器: 主要用来查询的容器,创建之后自带顺序
unordered set/multiset(无序集合)
unordered map/multiset(无序表)
无序容器底层使用hashTable实现
至于各个容器之间的区别,我会在后边的文章中慢慢讲到,同时我也会简单的仿写几个容器大致的功能。
泛型算法之所以说是泛型算法,因为它并不是单独的为某一种容器去设计的,而是可以共多种不同的容器去使用的算法。为了满足这个需求,就需要对于算法来说,屏蔽掉容器的实现方法,但是却可以按照同一种顺序去访问大多数的容器,这就是迭代器的作用。
迭代器实际上是一个泛化的指针,或者说是一种智能指针,是一个类模板。它通过重载对于本身的++和- -等操作,使泛型算法可以很简单的使用迭代器完成对不同容器的访问。而这种方式就要求对于每一种不同的容器,我们都需要设计不同的迭代器。标准库也正是这么做的。
在STL中,对于每一次取得的容器区间总是前闭后开区间。 也就是说:
通过begin( )取得的迭代器是指向第一个有效的元素
通过end( )取得的迭代器是指向最后一个有效元素之后的元素
所以,对于end( )取得的迭代器进行赋值操作是非法的!
这种规定对于STL的泛型算法和仿函数也同样有效,也就意味着,传递给泛型算法或是仿函数的参数区间也应该是前闭后开区间。