STL 组件概述
容器、迭代器、算法。
序列式容器:vector、deque、list,元素排列顺序和置入顺序一致。
关联式容器:set、multiset、map、multimap,他们的插入位置与插入次序无关。关联式容器 自动排序。
序列式容器(Sequence Containers)
vector以一个dynamic array加以管理,在尾部添加或者删除元素均非常快速,但是在中部或者头部比较费时。push_back(),size()所有序列式容器都提供这个成员函数。可以通过下标操作符[]来存取vector内的元素。
deque 是double-ended deque的缩写。也是一个dynamic array,可以向两端发展。push_front()向前安插,vector没有这个函数。支持下标[]操作。
list 是由双向链表(doubly linked list)实作而成,这意味这list内的每一个元素都以一部分内存指示其前趋元素或者后继元素。不支持随机存储(即没有提供[]下标操作),优势是在任何位置上安插或者删除动作都很迅速。
关联式容器(Associative Containers)关联式容器依据特定准则,自动为其元素排序,默认以operator<进行比较。通常关联式容器由二叉树实作出来。在二叉树中,每个元素都有一个父节点和两个子节点,左子树的所有元素都比自己小,右子树的所有元素比自己大。关联式容器的差别在于元素的类别以及处理元素的方式。
set:内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复。
multiset:与set相同,不过他允许重复。
map:都是”实值/键值“所形成的一个对组(key/value pairs),每个元素有一个键,是排序准则的基础,每个键只能出现一次,不允许重复。
multimap: 和map一样,但是允许重复。也就是说multimap可以包括多个键值相同的元素。multimap可被当做字典。
set<int> coll;
coll.insert(3);
coll.insert(1);
coll.insert(5);
coll.insert(4);
coll.insert(1);
coll.insert(6);
coll.insert(2);
set<int>::iterator pos;
for(pos=coll.begin();pos!=coll.end();++pos)
cout << *pos << " ";
cout << endl;
multimap<int,string> intStringMMap;
intStringMMap.insert(make_pair(1,"I"));
intStringMMap.insert(make_pair(1,"have"));
intStringMMap.insert(make_pair(2,"a"));
intStringMMap.insert(make_pair(3,"dream"));
intStringMMap.insert(make_pair(3,"."));
multimap<int,string>::iterator itor;
for(itor = intStringMMap.begin(); itor != intStringMMap.end(); ++itor)
{
// cout << itor->first << " ";
// cout << itor->second << " ";
cout << (*itor).second<< " ";
}
cout <<endl;
multimap<string,int> stringIntMMap;
stringIntMMap.insert(make_pair("gfafasdf",2));
stringIntMMap.insert(make_pair("gdgfsdfgs",1));
stringIntMMap.insert(make_pair("hdhfsdfgs",3));
multimap<string,int>::iterator itor;
for(itor=stringIntMMap.begin(); itor!=stringIntMMap.end(); ++itor)
{
cout << itor->second << endl;
}
cout << endl;
容器配接器( Container Adapters ):除了容器类别,C++标准库还提供了容器适配器,他包括
stacks : stack容器对元素采用LIFO(后进先出)管理策略。
Queues :对元素采用FIFO(先进先出)的管理策略。也就是普通的缓冲区。
Priority Queues :容器中的元素可以拥有不通的优先权。所谓优先权,是基于程序员提供的排序准则(缺省使用operator<)定义。Priority Queue 的效果相当于一个buffer:"下一个元素永远是Queue中优先级最高的元素"。如果同时有多个元素具备最高优先权,则其次序无明确定义。
迭代器(Iterators): Operator *、Operator ++、Operator -- 与指针相同,也可以使用operator-> ,但是operator==和operator != 判断两个迭代器是否指向同一个位置。operator将其所指元素的位置赋值过去。
begin() 返回是第一个元素的位置。end() 返回一个迭代器,指向容器结束点。最后一个元素之后,"逾尾"迭代器。begin()和end()形成一个前闭后开区间。优势:为遍历元素提供循环结束时机,只要未达到end(),循环就可以继续进行。不必对空区间采取特殊处理,空区间就是begin()=end()。
任何一个容器都定义两种迭代器:
container::const_iterator 以”读写“模式遍历元素
container::iterator 以"只读"模式遍历元素。
list<char>::interator pos; ++pos, 前置式递增比pos++后置式递增效率高,后置式需要一个额外的临时对象,因为他必须存放迭代器原来位置并将他返回,所以最好使用++pos。
算法(algorithms)
STL 提供了一些标准算法,如搜寻、排序、拷贝等,算法并非容器类型的成员函数,而是一种搭配迭代器使用的全局函数,这么做有一个优势就是所有算法只需要做出一份,可以对所有容器进行操作,而不需要为每一个容器量身定做。注意:这里所阐述的并非面向对象思维模式(OOP paradigm),而是泛型函数式编程思维模式(generic functional programming paradigm)。在面向对象概念里,数据和操作结合为一体,而这里被明确划分开来,再透过特定的接口彼此互动。当然这样付出的代价:首先有失直观,其次某些数据和算法不兼容。因此要深入学习STL的概念并了解缺陷,才能取利而弊害。
迭代器 之 配接器(Iterator Adapters)
1. Insert iterators (安插型迭代器)
back_inserter 安插于容器最尾端,在容器尾部插入元素,内部调用push_back(),所以可以使用的只有vector、deque、list三个容器,
front_inserter 安插于容器最前端, 在内部调用push_front(),将元素安插于容器前端,内部调用push_front(),所以只有deque、list使用。
inserter(General inserters 一般性安插),内部调用的insert函数,所有容器提供insert()函数,因此所有函数都可以使用。
list<int> coll1;
for(int i=1; i<=9; ++i)
{
coll1.push_back(i);
}
coll1.push_back(0);
vector<int> coll2;
copy(coll1.begin(),coll1.end(),back_inserter(coll2));
vector<int>::iterator itor;
for(itor=coll2.begin(); itor!=coll2.end(); ++itor)
{
cout << *itor << " ";
}
cout << endl;
deque<int> coll3;
copy(coll1.begin(),coll1.end(),front_inserter(coll3));
deque<int>::iterator itor1;
for(itor1=coll3.begin(); itor1!=coll3.end(); ++itor1)
{
cout << *itor1 << " ";
}
cout << endl;
set<int> coll4;
copy(coll1.begin(),coll1.end(),inserter(coll4,coll4.begin()));
set<int>::iterator itor2;
for(itor2=coll4.begin(); itor2!=coll4.end(); ++itor2)
{
cout << *itor2 << " ";
}
cout << endl;
<img src="https://img-blog.csdn.net/20150815205954665?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
2. Stream Iterators(流迭代器)
读写stream的迭代器。
<span style="white-space:pre"> </span>vector<string> coll;
copy(istream_iterator<string>(cin),
istream_iterator<string>(),
back_inserter(coll)); //把输入的参数传递到coll
sort(coll.begin(),coll.end());//排序
unique_copy(coll.begin(),coll.end(),
ostream_iterator<string>(cout,"\n"));
程序输入不知道如何结束。
3. Reverse Iterator(逆向迭代器)
倒转筋脉似地以逆向方式进行所有操作。它将increment(递增)运算转换为decrement(递减)运算。反之亦然。所有容器都可以透过成员函数rbegin()和rend()产生reverse iterators。
vector<int> coll1;
for(int i=1; i<=9; ++i)
{
coll1.push_back(i);
}
copy(coll1.rbegin(),coll1.rend(),
ostream_iterator<int>(cout," "));
cout << endl;
//coll1.rbegin() 返回coll的逆向迭代器
//<span style="font-family: Arial, Helvetica, sans-serif;">coll1.rend() 逆向"逾尾"位置,指的是容器内第一个元素的前一个位置</span>
更易型算法( manipulating algorithms )
移除算法 ( remove ):只是把后面的元素向前移了,返回一个新的结尾。如果要真正修改,还需要配合distance()和erase函数
list<int> coll1;
for(inti=1; i<=6; ++i)
{
coll1.push_back(i);
coll1.push_front(i);
}
copy(coll1.begin(),coll1.end(),
ostream_iterator<int>(cout," "));
cout << endl;
list<int>::end = remove(coll1.begin(),coll1.end(),3);
copy(coll1.begin(),end,ostream_iterator(cout," "));
cout << endl;
cout << "number of remove elements:"
<< distance(end,coll1.end()) << endl;
coll1.erase(end,coll1.end());
copy(coll1.begin(),coll1.end(),
ostream_iterator<int>(cout," "));
cout << endl;
如果两个迭代器都是随机存取迭代器,可以采用operator-直接计算其距离。
如果要彻底删除元素,可以:
coll1.erase(remove(coll1.begin(),coll1.end(),3),coll1.end());
那么为何算法自己部调用erase(),这正是STL为了获取灵活性二付出的代价,透过"以迭代器"为接口,STL将数据结构和算法分离开来。而迭代器只是容器的一个位置的抽象概念而已,一般说来迭代器对自己所属的容器一无所知。任何“以迭代器访问容器元素”的算法,都不得(无法)透过迭代器调用容器类别所提供的任何成员函数。这样设计的结果是苏昂操作的对象不一定是“容器内的全部元素”所形成的区间,可以是一部分元素,甚至是一个“未提供成员函数erase()”的容器(如array),所以为了达成算法的最大弹性,不要求“迭代器必须了解容器细节”是有一定道理的。
注意,通常并不必要删除元素,通常以逻辑终点来取代容器的实际终点就足够了。
更易型算法和关联式容器
更易型算法(移除remove、重排resort、修改modify)用于关联式容器身上会出问题。关联式容器不能被当做操作目标,原因很简单,如果更易型算法用于关联式容器,会改变某位置上的值,进而破坏其已序特征,那就推翻了关联式容器的基本原则,容器内的元素总是根据某个排列准则自动排序,因此为了保证这个原则,关联式容器的所有迭代器均被声明为指向常量,如果你更易关联式容器中的元素,会导致编译错误。
那么如何从关联式容器中删除元素,原因很简单,调用成员函数。关联式容器提供了多种不同的erase()成员函数,返回值为被删除的元素个数。
如果高效是你的最高目标,你应该永远优先选用成员函数。但是一旦改变容器,你就得考量成员函数的合理性,而此时通用算法可能不需要修改代码。
仿函数(functors,Function Objects)
仿函数是泛型编程强大威力和纯粹抽象概念的又一例证。你可以说任何东西,只要行为像函数,它就是个函数,因此,如果你定义了一个对象,行为像函数,它就可以被当函数使用。所谓函数行为,是指可以“使用小括号传递参数,籍以调用某个东西”。没错,在C++中,只需要定义operator(),并给予合适的参数型别。
<span style="white-space:pre"> </span>class PrintInt
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>public:
<span style="white-space:pre"> </span>void operator()(int elem)const
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>cout << elem << " ";
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>};
<span style="white-space:pre"> </span>int main()
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>vector<int> coll;
<span style="white-space:pre"> </span>for(int i=1; i<=9; ++i)
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>coll.push_back(i);
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>for_each(coll.begin(),coll.end(),PrintInt());
<span style="white-space:pre"> </span>cout << endl;
<span style="white-space:pre"> </span>return 0;
<span style="white-space:pre"> </span>}
表达式PrintInt()产生此类别的一个临时对象,当做for_each()算法的一个参数。
仿函数优势:
1、仿函数是"smart functions"(智能型函数)
“行为类似指针”的对象,我们称为“smart pointers”。“行为类似函数”的对象我们称之为“smart functions”,因此他们的能力可以超越operator()。仿函数可拥有成员函数和成员变量,这意味着仿函数拥有状态。
2、每个仿函数都有自己的型别
仿函数定义的每一个函数行为都有其自己的型别。我们便可以将函数行为当做template模板来运用。
3、仿函数通常比一般函数速度快
就template概念而言,由于更多细节在编译期就已确定,所以通常可能进行更好的最佳化。所以传入一个仿函数(而非一般函数),可能获得更好的性能。
template <int theValue>
void add(int& elem)
{
elem += theValue;
}
int main()
{
vector<int> coll;
for(int i=1; i<=9; ++i)
{
coll.push_back(i);
}
for_each(coll.begin(),coll.end(),add<10>);
copy(coll.begin(),coll.end(),
ostream_iterator<int>(cout," "));
cout << endl;
return 0;
}
C++标准程序库包含一些预先定义的仿函数,涵盖许多基础运算。有了它们,很多时候你就不必费心自己去写仿函数。一个典型的例子是作为排序准则的仿函数,operator<之缺省排序准则乃是less<>,所以声明的
set<int> coll;会被扩展为set<int,less<int> >coll;
int main()
{
set<int,greater<int> > coll1;
deque<int> coll2;
for(int i=1; i<=9; ++i)
{
coll1.insert(i);
}
// transform all elements into coll2 by multiplying 10
transform(coll1.begin(),coll1.end(), //source
back_inserter(coll2), //destination
bind2nd(multiplies<int>(),10));//operation
// replace value equal to 70 with 42
replace_if(coll2.begin(),coll2.end(), //range
bind2nd(equal_to<int>(),70), //replace criterion
42); //new value
// remove all elements with values less than 50
coll2.erase(remove_if(coll2.begin(),coll2.end(), // range
bind2nd(less<int>,50)), // remove criterion
coll2.end());
return 0;
}
这里使用了配接器bind2nd,使得进行multiples<int>运算时,以源集群的元素作为第一参数,10作为第二参数。
配接器bind2nd的工作方式如下:transform()期待他自己的第四个参数是个能接纳单一参数的表达式,然而我们却希望先把该元素乘以10,再传给transfor()。所以我们必须构建一个表达式,接受两个参数,并以数值10作为第二个参数,以此产生一个“只需要单个参数”的表达式。bind2nd()正好胜任这项工作。他会把表达式保存起来,把第二个参数当做内部数值也保存起来。
此种方式的程序编写,导致函数的组合。有趣的是这些所有仿函数通常都声明为inline。还有一些仿函数。某些仿函数可用来调用群集内每一个元素的成员函数:
for_each(coll.begin(),coll.end(),
mem_fun_ref(&Person::save));
仿函数mem_fun_ref 用来调用它所作用的元素的某个成员函数。
STL的设计原则是效率优先,安全次之。错误检查相当花时间,所以C++几乎没有。C++标准库指出,对于STL的任何运用,如果违反规则,将导致未定义的行为。所以使用STL必须满足:1、迭代器务必合法有效。2、一个迭代器的“逾尾”位置,它不指向任何对象,因此operator*或者operator->不适用于任何end()和rend()。3、区间必须合法。4、如果涉及的区间不止一个,第二个区间及后继各区间必须拥有“至少和第一个区间一样多”的元素。5、覆盖(overwritten)动作中的目标区间必须拥有足够元素,负责就必须采用insert iterators(插入型迭代器)。
一个带有警戒能力的STL版本,名为“STLport”,几乎适用于任何平台。