标准模板库


前言

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的容器种类

总的来说,容器可分为三大类

  1. 序列式容器(Sequence container),这是一种有序(ordered)集合,其内每个元素均有确凿的位置——取决于插入时机和地点,与元素值无关。如果你以追加方式对一个集合置入6个元素,它们的排列次序将和置入次序一致。STL提供了5个定义好的序列式容器:array、vector、deque、list和forward_list.
  2. 关联式容器(Associative container),这是一种已经排序(sorted)集合,元素位置取决于其value(或key——如果元素是个key/value pair)和给定的某个排序准则。如果将6个元素置入这样的集合中,它们的值将决定它们的次序,和插入次序无关。STL提供了4个关联式容器:set、multiset、map和multimap.
  3. 无序容器(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;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一份讲解全面的标准模板STL学习资料 标准模板STL主要由6大组件组成: (1)容器(Containers)。包括各种基本数据结构的类模板。 STL容器部分主要由头文件<vector>、<list>、<deque>、<set>、< map>、<stack>和<queue>组成。 (2)算法(Algorithms)。包括各种基本算法,如比较、交换、查找、排序、遍历操作、复制、修改、移除、反转、合并等等。 STL算法部分主要由头文件<algorithm>和<numeric>组成。 (3)迭代器(Iterators)。迭代器是面向对象版本的指针,如同指针可以指向内存中的一个地址,迭代器可以指向容器中的一个位置。 STL的每一个容器类模板中,都定义了一组对应的迭代器类,用以存取容器中的元素。这样,在STL中迭代器就将算法和容器联系起来了,通过迭代器,算法函数可以访问容器中指定位置的元素,而无需关心元素的具体类型。 STL迭代器部分主要由头文件<utility>和<iterator>组成。 (4)函数对象(Function Objects)。一种行为类似于函数的class,实现技术上是一个改写了“call operator()”的class。 STL 提供 15 个预定义的 Function objects。头文件<functional>中定义了一些类模板,用以声明函数对象。 (5)适配器(Adaptors)。简单地说就是一种接口类,专门用来修改现有类的接口,提供一种新的接口;或调用现有的函数来实现所需要的功能。 主要包括3种适配器Container Adaptors、Iterator Adaptors与Function Adaptors。其中迭代器适配器的定义在头文件<iterator>中,函数适配器的定义在头文件<functional>中。 (6)内存配置器(Allocators)。为STL提供空间配置的系统。 头文件<memory>中的主要部分是模板类allocator,它负责产生所有容器中的默认分配器。容器使用allocator完成对内存的操作,allocator提供内存原语以对内存进行统一的存取。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值