标准模板库
前言
STL( standard template library,标准模板库)是C++标准库的核心,它深刻影响了标准库的整体结构。STL是一个泛型(generic)程序库,提供一系列软件方案,利用先进、高校的算法来管理数据。程序员无须了解STL的原理,便可享受数据结构和算法邻域中的这一革新成功。
从程序员的角度看来,STL是由一些可适应不同需求的集合类(collection class)和一些能够在这些数据集合上运作的算法构成。STL内的所有组件都由template(模板)构成,所以其元素可以是任意类型。更妙的是,STL建立了一个框架(framework),在此框架下可以提供其他集合类或算法,与现有的组件搭配共同 运作。总之,STL赋予了C++新的抽象层次。抛开dynamic array(动态数组)、linked list(链表)、binary tree(二叉树)、hash table(散列表)之类的东西,也不必再烦心各种search(查找)算法了,只需使用恰当的集合类,然后调用其成员函数和算法来处理就万事大吉了。
本文章探讨STL的总体概念,及其编程技术。
一、标准模板库
1.1 STL组件(Component)
若干精心勾画的组件共同合作,构筑起STL的基础。这些组件中最关键的是容器、迭代器和算法。
容器(Container),用来管理某类对象的集合。每一种容器都有其优点和缺点,所以,为了应付不同的需求,STL准备了不同的容器类型。容器可以是array 或linked list,或者每个元素都有一个特别的key。
迭代器(Iterator),用来在一个对象集合(collection of objects)内遍历元素。这个对象集合或许是个容器,或许是容器的一部分。迭代器的主要好处是,尾所有各式各样的容器提供了一组很小的共同接口。例如其中一个操作时进行至集合内的下一元素。至于如何做到当然取决于集合的内部结构。不论这个集合是array或tree或hash table,此一行进动作能成功,因为每一种容器都提供了自己的迭代器,而这些迭代器了解容器内部结构,知道该做些什么。
迭代器的接口和寻常的pointer差不多,调用operator++就累进,调用operator*就提令(访问)被指向的值。所以你可以把迭代器视为一种smart pointer,能够把 "前进至下一个元素"的意图转换成合适的操作。
算法(Algorithm),用来处理集合内的元素。它们可以处于不同的目的而查找、排序、修改、使用元素。通过迭代器的协助,我们只需撰写一次算法,就可以将它应用于任意容器,因为所有容器的迭代器都提供一致的接口。
还可以提供一些特殊的辅助函数供算法调用,从而获取更佳的灵活性。这样你就可以一方面用标准算法,一方面配合自己特殊或复杂的需求。例如,可以提供自己的查找准则(search criterion)或元素合并时的特殊操作。特别是由于C++11新引入了lambda,得以轻松指明在容器元素身上进行任何种类的动作。
STL的基本概念就是将数据和操作分离。数据由容器类加以管理,操作则由可定制(configurable)的算法定义之。迭代器在两者之间充当黏合剂,使任何算法都可以和任何容器交互运作
STL组件之间的合作
STL将数据和算法分开对待,而不是合并考虑。从某种意义上说,STL的概念和面向对象编程(OOP)的最初思想是矛盾的。然而这么做有着很重要的因素。首先,你可以将各种容器与各种算法结合起来,在很小的框架(framework)内达到非常大的弹性。
STL的一个根本特性是,所有组件都可以针对任意类型(type)运作。顾名思义,所谓standard template library意味着其内的所有组件都是“可以接受任意类型”的template,前提是这些类型必须能够执行必要的操作。因此STL成了泛型编程(generic programming)概念下的一个出色的范例。容器和算法被泛华为可适用于任意type和class.
STL甚至提供更泛化的组件。借由特定的适配器(adapter)和函数对象(function object,functor),你可以补充、约束或定制算法以满足特别需求。
1.2 容器(Container)
容器用来管理一大群元素。为了适应不同需要,STL提供了不同的容器,如图下图所示。
STL的容器种类
总的来说,容器可分为三大类
- 序列式容器(Sequence container),这是一种有序(ordered)集合,其内每个元素均有确凿的位置——取决于插入时机和地点,与元素值无关。如果你以追加方式对一个集合置入6个元素,它们的排列次序将和置入次序一致。STL提供了5个定义好的序列式容器:array、vector、deque、list和forward_list.
- 关联式容器(Associative container),这是一种已经排序(sorted)集合,元素位置取决于其value(或key——如果元素是个key/value pair)和给定的某个排序准则。如果将6个元素置入这样的集合中,它们的值将决定它们的次序,和插入次序无关。STL提供了4个关联式容器:set、multiset、map和multimap.
- 无序容器(Unordered (associative)container),这是一种无序集合(unordered collection),其内每个元素的位置无关紧要,唯一重要的是某特定元素是否位于此集合内。元素值或其安插顺序,都不影响元素的位置,而且元素的位置有可能在容器生命中被改变 。如果你放6个元素到这种集合内,它们的次序不明确,并且可能随时机而改变。STL内含4个预定义的无序容器: unorderde_set、unordered_multiset、unordered_map和unordered_multimap
Sequence容器通常被实现为array或linked list
Associative容器通常被实现为binary tree
Unordered容器通常被实现为hash table
严格地说,C++标准库并未规定任何容器必须使用任何特定的实现,然而由于C++ standard对于行为和复杂度的明确要求,使得这方面的变化余地不懂。所以实现中各个实现之间只可存在微小细节上的差异。
1.2.1 序列式容器(Sequence Container)
STL内部预先定义好以下序列式容器
array(其class 名为array)
vector
deque
list(singly/double linked)
以下讨论从vector开始,因为array是TR1新引入的,进入C++标准库的时间比较短,而且它有一些特殊属性,与其他STL容器不共通。
vector
vector将其元素置于一个dynamic array中管理。它允许随机访问,也就是说,你可以利用索引直接访问任何一个元素。在array尾部附加元素或移除元素都是很快的,但是在array的中间段或起始段安插元素就比较费时,因为安插点之后的所有元素都必须移动,以保持原本的相对次序。
以下例子针对整数类型定义一个vector,插入6个元素,然后打印所有元素:
#include<vector>
#include<iostream>
using namespace std;
static void printVector(const vector<int>&vec) {
for (auto it = vec.begin(); it != vec.end(); ++it){
cout << *it << " ";
}
cout << endl;
}
int main()
{
vector<int> coll;
for (int i = 1; i <= 6; ++i) {
coll.push_back(i);
}
printVector(coll);
system("pause");
return 0;
}
deque
所谓deque是“double-ended queue”的缩写。它是一个dynamic array,可以向两端发展,因此不论在尾部或头部安插元素都十分迅速。在中间部分安插元素则比较费时,因为必须移动其他元素。
以下的例子声明了一个元素为浮点数的deque,打印出所有元素。
#include<iostream>
#include<deque>
using namespace std;
static void printDeque(const deque<float>&deq) {
for (int i = 0; i < deq.size(); i++) {
cout << deq.at(i) << " ";
}
cout << endl;
}
int main()
{
deque<float> coll;
for (int i = 1; i <= 6; ++i) {
coll.push_front(i*1.1);
}
printDeque(coll);
system("pause");
return 0;
}
array
一个array对象乃是在某个固定大小的array(有时称为一个static array或C array)内管理元素。因此,不可以改变元素个数,只能改变元素值。必须在建立时就指明其大小。array也允许随机访问,意思是可以直接访问任何一个元素——只要只当相应的索引。
#include<iostream>
#include<array>
#include<string>
using namespace std;
int main()
{
array<string, 5> coll = { "hello","World!!!" };
for (int i = 0; i < coll.size(); ++i) {
cout << i << ":" << coll[i] << endl;
}
system("pause");
return 0;
}
list
从历史角度看,我们只有一个list class。然而自C++11开始,STL竟提供了两个不同的list容器:class list<> he class forward_list<>。因此,list可能表示其中某个class,或者是个总体术语,代表上述两个list class。然而就某种程度来说,forward list只不过是受到更多限制的list,现实中二者的差异并不怎么重要。因此当我们使用术语的list,通常指的是class list<>,它的能力往往超越class forward_list<>。如果特别需要指出class forward_list<>,我会使用术语forward list。
list<>由双向链表(doubly linked list)实现而成。这意味着list内的每个元素都以一部分内存指示其当前导元素和后继元素。
list不提供随机访问,因此如果你要访问第10个元素,你必须沿着链表依次走过前9个元素。不过,移动至下一个元素或前一个元素的行为,可以在常量时间内完成。因此一搬得元素访问动作会花费线性时间,因为平均距离和元素数量成正比例。这比vector和deque提供的摊提式常量时间,效率差很多。
list的优势是:在任何位置上执行安插或删除动作都非常迅速,因为只需要改变链接(link)就好。这表示list中段处移动元素比在vector和deque快的多。
#include<iostream>
#include<string>
#include<list>
using namespace std;
static void printList(const list<char>&lst) {
for (auto elem : lst) {
cout << elem << ' ';
}
cout << endl;
}
int main()
{
list<char> coll; //list container for character elements;
for (char c = 'a'; c <= 'z'; ++c) {
coll.push_back(c);
}
printList(coll);
system("pause");
return 0;
}
forward list
自C++11之后,C++标准库提供了另一个list容器:forward list。forward_list<>是一个由元素构成的单向(singly)linked list。就像寻常list那样,每个元素都有自己一段内存,为了节省内存,它只指向下一元素。
因此forward_list原则上就是一个受限的list,不支持任何“后退移动”或“效率低下”的操作。基于这个原因,它不提供成员函数如push_back()乃至size().
现实中,这个限制比乍听之下甚至更尴尬棘手。问题之一是,你无法查找 某个元素然后删除它,或是在它的前面安插另外一个元素。因为,为了删除某个元素,你必须位于其前一元素的位置上,因为正是那个元素才能决定一个新的后继元素。也因此,forward_list对比提供了一个特殊成员函数。
#include<iostream>
#include<forward_list>
using namespace std;
int main()
{
//create forward-list container for some prime numbers
forward_list<long> coll = { 2,3,5,7,11,13,17 };
//resize tow times
//-note:poor performance
coll.resize(9);
coll.resize(10, 99);
for (auto elem : coll) {
cout << elem << ' ';
}
cout << endl;
system("pause");
return 0;
}
1.2.2 关联式容器(Associative Container)
关联式容器依据特定的排序准则,自动为某元素排序。元素可以是任何类型的value,也可以是key/value pair,其中key可以是任何类型,映射至一个相关value,而value也可以是任意类型。排序准则以函数形式呈现,用来比较value,或比较key/value中的key。默认情况下所有容器都以操作符<进行比较,不过你也可以提供自己的比较函数,定义出不同的排序准则。
通常关联式容器由二叉树(binary tree)实现出来。在二叉树中,每个元素(节点)都有一个父节点和两个子节点;左子树的所有元素都比自己小,右子树的所有元素都比自己大。关联式容器的差别主要在于元素的种类以及处理重复元素时的方式(态度)。
关联式容器的主要优点是,它能很快找出一个具有特定 value的元素,因为它具有对数复杂度(logarithmic comlexity),平均而言你将有10次而不是500次比较动作。然而它的一个缺点是,你不能直接改动元素的value,因为那会破坏元素的自动排序。
下面是STL定义的关联式容器:
set 元素依据其value自动排序,每个元素只能出现一次,不允许重复
multiset 和set的唯一差别是:元素可以重复。也就是multiset可包括多个“value”相同的元素
map 每个元素都是key/value pair,其中key是排序准则的基准。每个key只能出现一次,不允许重复。map也可以被视为一种关联式数组(associative array),也就是“索引可以任意类型”的数组。
multimap 和 map的唯一差别是:元素可以重复,也就是multimap允许其元素拥有相同的key。multimap可被当作字典(dictionary)使用。
所有关联式容器都有一个可供选择的template实参,指明排序准则;默认采用操作符<。排序准则也被用来测试等同性(equivalence):如果两个元素的value/key互不小于对方,则两者被视为重复。
你可也将set视为一种特殊的map:元素的value等同于key。实际产品中所有这些关联式容器通常都由二叉树(binary tree)实现而成。
set/multiset
#include<iostream>
#include<set>
#include<string>
using namespace std;
static void printMultiset(const multiset<string>&mulst) {
for (auto elem : mulst) {
cout << elem << " ";
}
cout << endl;
}
int main()
{
multiset<string> cities{
"Branuschweig","Hanover","Frankfurt","New York",
"Chicago","Toronto","Paris","Frankfurt"
};
printMultiset(cities);
cities.insert({ "London","Munich","Hanover","Braunschweig" });
printMultiset(cities);
system("pause");
return 0;
}
map/multimap
#include<map>
#include<string>
#include<iostream>
using namespace std;
static void printMultimap(const multimap<int, string> &hash) {
for (auto elem : hash) {
cout << elem.second << " ";
}
cout << endl;
}
int main()
{
multimap<int, string> coll;
coll = {
{5,"tagged"},
{2,"a"},
{1,"this"},
{4,"of"},
{6,"strings"},
{1,"is"},
{3,"multimap"}
};
printMultimap(coll);
system("pause");
return 0;
}
1.2.3 无序容器(Unordered Container)
在无序(unordered)容器中,元素没有明确的排列次序。也就是如果安插3个元素,当你迭代器内的所有元素时会发现,它们的次序有各种可能。如果安插第4个元素,先前3个元素的相对次序可能会被改变。我们唯一关心的是,某个特定元素是否位于容器内。甚至如果你有2个这种容器,其内有着完全相同的元素,元素的排列次序也可能不同。
无序(unordered)容器常以hash table实现出来,内部结构是一个“由linked list 组成”的array。通过某个hash函数的运算,确定元素落于这个array的位置。Hash函数运算的目标是:让每个元素的落点有助于用户快速访问
无序()容器就是Hash Table
任何一个元素,前提则是hash函数本身也必须够快。由于这样一个快速而完美的hash函数不一定存在(或不一定被你找到),抑或由于它造成array耗费巨额内存而显得不切实际,因此,退而求次的hash函数有可能让多个元素落于同一位置上。所以设计上就让array的元素再被放进一个linked list中,如此一来array的每个位置(落点)就得以存放一个以上的元素。
无序(unordered)容器的主要优点是,当你打算查找一个带某特定值的元素,其速度甚至可能快过关联式容器。事实上无序容器提供的是摊提的常量复杂度(amortized constant complexity),前提是你有以恶良好的hash函数。然而提供一个良好的hash函数并非易事,可能需要提供许多内存作为bucket.
根据关联式容器的分类法,STL定义出下面这些无序容器:
unordered set是无序元素的集合,其中每个元素只可出现一次。也就是不允许元素重复。
unordered multiset 和 unordered set的唯一差别是它允许元素重复。也就是unordered multiset可能内含多个有着相同value的元素。
unordered map的元素都是key/value pair。每个key只可出现一次,不允许重复。它可以用作关联式数组(associative array),那是“索引可为任意类型”的array.
unordered multimap 和unordered map的唯一差别是允许重复。也就是unordered multimap可能内含多个“拥有相同key”的元素。它可以用作子字典(dictionary),所有这些无序容器的class都有若干可有可无的template实参,用来指明hash函数和等效准则(equivalence criterion),该准则被用来寻找某给定值,以便判断是否发生重复。默认的等效准则是操作符==
可以把unordered set视为一种特殊的unordered map,只不过其元素的value等同于key。现实中所有无序容器通常都使用hash table作为底层实现。
unordered set/multiset实例
#include<iostream>
#include<unordered_set>
#include<string>
using namespace std;
static void printUnorderedSet(const unordered_multiset<string>& unmulst) {
for (auto elem : unmulst) {
cout << elem << " ";
}
cout << endl;
}
int main()
{
unordered_multiset<string> cities{
"Braunschweig","Hanover","Frankfurt","New York",
"Chicago","Toroto","Paris","Frankfurt"
};
//print each element:
printUnorderedSet(cities);
cities.insert({ "London","Munich","Hanover","Braunschweig" });
printUnorderedSet(cities);
system("pause");
return 0;
}
unordered map/multimap
#include<unordered_map>
#include<string>
#include<iostream>
using namespace std;
int main()
{
unordered_multimap<string, double> coll{ {"tim",9.9},{"struppi",11.77} };
//square the value of each element:
for (pair<const string, double>&elem : coll) {
elem.second *= elem.second;
}
//print each element (key and value)
for (const auto & elem : coll) {
cout << elem.first << ":" << elem.second << endl;
}
system("pause");
return 0;
}