slacr_

Just to record my life and thoughts.
笔记/编程/杂乱/极简

[C++笔记]泛型程序设计与STL

May 2, 2023C_C++10447 words in 70 min

泛型程序设计

所谓泛型程序设计,就是编写不依赖于具体数据类型的程序。c++ 中,模板是泛型程序设计的主要工具

泛型程序设计的主要思想是将算法从特定的数据结构中抽象出来,使算法成为通用的、可以作用于各种不同的数据结构。这样就不必为每种容器都编写一套同样的算法,当容器类模板修改、扩充时也不必重写大量算法函数。这种以函数模板形式实现的通用算法与各种通用容器结合,提高了软件的复用性。

我们可以用概念 (concept)来描述泛型程序设计中作为参数的数据类型所需具备的功能。这里的"概念"是泛型程序设计中的一个术语,它的内涵是这些功能,它的外延是具备这些功能的所有数据类型。

例如,"可以用比大小、具有公有的复制构造函数并可以用’='赋值的所有数据类型"就是一个概念,可以把这个概念记作 Sortable 。具备一个概念所需要功能的数据类型称为这一概念的一个模型 (model) 。例如 .int 数据类型就是 Sortable 概念的一个模型。

对于两个不同的概念A和B. 如果概念A所需求的所有功能也是感念B 所需求的功能(即概念B的模型一定是概念A的模型) .那么就说概念B 是概念A的子概念(有些书上又把它称为精炼(refinement))。我们把"可以比大小的所有数据类型"这一橄念记为 Comparable ,把"具有公有的复制构造函数并可以用’='赋值的数据类型"这一概念记为 Assignable ,那么Sortable 既是 Comparable 的子概念,也是 Assignable 的子概念。
很多 STL 的实现代码就是使用概念来命名模板参数的。

STL简介

标准模板库 (Standard Template Library , STL) 最初是由 HP 公司的 Alexander Stepanov 和 Meng Lee 开发的一个用于支持 C++ 泛型编程的模板库, 1994 年被纳入了C++ 标准, 成为 C++ 标准库的一部分。由于 C++ 标准库有多种不同的实现,因此 STL 有不同的版本,但它们为用户提供的接口都遵守共同的标准。

STL定义了一套概念体系,为泛型程序设计提供了逻辑基础。 STL 中的各个类模板、函数模板的参数都是用这个体系中的概念来规定的。使 STL 的一个模板时所提供的类型参数既可以是 C++ 标准库中已有的类型,也可以是自定义的类型, 只要这些类型是所要求概念的模型,因此, STL 是一个开放的体系。

容器(container)

容器是容纳、包含一组元素的对象。容器类库中包括7种基本容器: 向量 (vector) 双端队列 (deque) 、列表 (list) 、集合 (set) 、多重集合( multiset) 、映射 (map) 和多重映射(multimap)

在 STL 中,容器是封装起来的类模板,其内部结构无从知晓,而只能通过容器接口来使用容器。

可以分为两种基本类型 顺序容器 (sequence container) 和关联容器 (associative container) 。顺序容器将一组具有相同类型的元素以严格的线性形式组织起来,向量、双端队列和列表容器就属于这一种。关联容器具有根据一组索引来快速提取元素的能力,集合和映射容器就属于这一种。

迭代器(iterater)

迭代器提供了顺序访问容器中每个元素的方法。因此指针本身就是一种迭代器,迭代器是泛化的指针

函数对象(function object)

函数对象是一个行为类似函数的对象,对它可以像调用函数一样调用。任何普通的函数和任何重载了"()"运算符的类的对象都可以作为函数对象使用,函数对象是泛化的函数
使用 STL 的函数对象,需要包含头文件<functional>

算法 (algorithm)

STL 包括 70 多个算法,这些算法覆盖了相当大的应用领域,其中包括查找算法、排序算法、消除算法、计数算法、比较算法、变换算法、置换算法和容器管理等。这些算法的一个最重要的特性就是它们的统一性,并且可以广泛用于不同的对象和内置的数据类型。
使用 STL 的算法,需要包含头文件<algorithm>

STL 把迭代器作为算法的参数,通过迭代器来访问容器而不是把容器直接作为算法的参数 ;STL 把函数对象作为算法的参数而不是把函数所执行的运算作为算法的一部分。这些都是非常成功的设计,它为 STL 提供了极大的灵活性。使用 STL中提供的或自定义的迭代器和函数对象,配合 STL 的算法,可以组合出各种各样的功能。

迭代器

虽然指针也是一种迭代器,但迭代器却不仅仅是指针。指针可以指向内存中的一个地址,通过这个地址就可以访问相应的内存单元;而迭代器更为抽象,它可以指向容器中的一个位置,我们不必关心这个位置对应的真正物理地址,只需要通过迭代器访问这个位置的元素。

输入输出流迭代器

输入/输出流迭代器用来从一个输入流/输出流中连续地输入/输出某种类型的数据.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

int main() {
istream_iterator<int> is_i = istream_iterator<int>(cin);
ostream_iterator<int> os_i = ostream_iterator<int>(cout, "\n"); //第二个参数 delimiter

int a = *is_i;
is_i++;
int b = *is_i;
*os_i = a;
os_i++;
*os_i = a + b;


cout << "----------------" << endl;
for (int i = 0; i < 10; i++) {
*os_i = i;
os_i++;
}

return 0;
}

输出流迭代器的特点是它在赋值时会将值写入到它所指向的输出流中。当我们使用输出流迭代器时,每次对它进行解引用操作 * 时,它会将该值写入到它所指向的输出流中.

虽然输入流迭代器和输出流迭代器本身并不能比输入流和输出流提供更强大的功能,但由于它们采用迭代器的接口,在这两种迭代器的帮助下,输入流和输出流可以直接参与 STL 的算法,这就是引入这两种迭代器的意义。输入流迭代器和输出流迭代器可以被看作一种适配器。适配器 (adapter) 是指用于为已有对象提供新的接口的对象,适配器本身一般并不提供新的功能,只为了改变对象的接口而存在。输入流迭代器和输出流迭代器将输入流和输出流的接口变更为迭代器的接口,因此它们属于适配器。

迭代器的分类

用输入选代器读入的序列不能保证是可重复的。,如果 pl == p2 ,并不能保证 ++p1 == ++p2, 输入流迭代器只适用于作为那些只需遍历序列一次的算法的输入。p++返回结果未知, 最好用 ++p.

使用输出迭代器,写入元素的操作和使用"++“自增的操作必须交替进行。如果连续两次自增之间没有写入元素,或连续两次使用”* pl =t"这样的语法写入元素之间没有自增,其行为都是不确定的。

前向迭代器这一概念是输入迭代器和输出迭代器这两个概念的子概念,它既支持数据读取,也支持数据写入。前向迭代器支持对序列进行可重复的单向遍历。它去掉了输入迭代器和输出迭代器这两个概念中的一些不确定性.
前向迭代器对序列的遍历是可重复的。,前向迭代器不再有输出迭代器关于"++"自增操作和对元素的写入操作必须交替进行"的限制。

双向迭代器这一概念是单向迭代器的子概念。在单向迭代器所支持的功能基础上,它又支持迭代器向反向移动。

随机访问迭代器这一概念是双向迭代器的子概念。在双向迭代器的基础上,它又支持直接将选代器向前或向后移动n个元素,因此随机访问迭代器的功能几乎和指针一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
#include <vector>
#include <algorithm>


// 实现泛型排序
template<class T, class input_iterator, class output_iterator>
void mySort(input_iterator first, input_iterator last, output_iterator result) {
vector<T> v;
for ( ; first != last; ++first) v.push_back(*first);
sort(v.begin(), v.end());
copy(v.begin(), v.end(), result);
}


int main() {
double arr[] = { 1.1, 2.7, 9.0, 1.25, 0.3 };
mySort<double>(arr, arr + 5, ostream_iterator<double>(cout, " "));

//mySort<int>(istream_iterator<int>(cin), istream_iterator<int>(), ostream_iterator<int>(cout, " "));
return 0;
}

迭代器的辅助函数

STL 为迭代器提供了两个辅助函数模板 advance 和 distance 。它们为所有迭代器提供了一些原本只有随机访问迭代器才具有的访问能力:前进或后退多个元素,以及计算两个迭代器之间的距离。

template<class Inputlterator, class Disstance>
void advance (Inputlterator& iter, Distance n);

template<class Inputlterator>
unsigned distance (Inputlterator first , Inputlterator last);

容器

容器的基本功能

S s1 容器都有→个默认构造函数,用于构造一个没有任何元素的空容器。
s1 op s2 这里 op 可以是= =,! = ,<,<= ,>,>=之一,它会对两个容器之间的元素按字典顺序进行比较。
s1.begin() 返回指向 s1 第一个元素的迭代器。
s1.end() 返回指向 s1 最后一个元素的下一个位置的迭代器。
s1.clear() 将容器 s1 的内容清空。
s1.empty() 返回一个布尔值,表示 s1 容器是否为空。
s1.size() 返回 s1 的元素个数。
s1.swap(s2) s1 容器和 s2 容器的内容交换。
S::iterator 表示与 相关的普通迭代器类型,迭代器指向元素的类型为T
S::const_iterator 表示与 相关的常迭代器类型,迭代器指向元素的类型为 const T; s1 是常量时,使用 s1.begin()返回的迭代器的类型就是 S::const_iterator否则是S::iterator
s1.rbegin() 得到指向容器的最后一个元素的逆向迭代器。
可逆容器
s1.rend() 得到指向容器的第一个元素的前一个位置的逆向迭代器。
S::reverse_iterator 表示与S相关的普通迭代器类型,选代器指向元素的类型为T
S::const_reverse_iterator 表示与S相关的常迭代器类型,迭代器指向元素的类型const ,因此只能通过迭代器读取元素,不能通过迭代器改写元素。

容器作为一种 STL 的概念,有许多子概念。容器分为顺序容器和关联容器,这就是容器的两个子概念,这种划分是基于容器中元素的组织方式的。另一方面,按照与容器所关联的迭代器类型划分,容器又具有"可逆容器"这一子概念,可逆容器又具有"随机访问容器"这一子概念

使用一般容器的 begin()或 end() 成员函数所得到的迭代器都是前向迭代器,也就是说可以对容器的元素进行单向的遍历。而可逆容器所提供的迭代器是双向迭代器,可以对容器的元素进行双向的遍历。

顺序容器

STL 中的顺序容器包括向量 (vector) 、双端队列 (deque) 和列表(list) ,它们在逻辑上可看作是一个长度可扩展的数组,容器中的元素都线性排列。程序员可以随意决定每个元素在容器中的位置,可以随时向指定位置插入新的元素和删除已有的元素。每种类型的容器都是一个类模板,都具有一个模板参数,表示容器的元素类型,该类型必须符合Assignable这一概念(即具有公有的复制构造函数并可以用"="赋值)。

构造函数
S s(n, t); 构造一个由n元素t构成的容器实例 s
S s(n); 构造一个有n个元素的容器实例s, 每个元素都是 T()
S s(q1,q2); 使用将 [q1 q2) 区间内的数据作为s的元素构造s
赋值函数
s.assign(n, t) 赋值后的容器由n元素构成。
s.assign(n) 赋值后的容器有n个元素的容器实例s,每个元素都是 T()
s.assign(q1, q2) 赋值后的容器的元素为 [q1, q2) 区间内的数据。
元素的插入
s.insert(p1, t) 在s容器中 p1 所指向的位置插入一个新的元素t,插入后的元素夹在原p1 和 p1-1 所指向的元素之间,该函数会返回一个迭代器指向新插入的元素。
s.insert(p1, n, t) 在s容器中 p1 所指向的位置插入 n个新的元素,插入后的元素夹在原 p1 和 p1-1 所指向的元素之间,没有返回值。
s.insert(p1,q1, q2) 将[q1, q2) 区间内的元素顺序插入到s容器中 p1 位置处,新元素夹在原 p1和p1-1 所指向的元素之间。
元素的删除
s1.erase(p1) 删除 s1 容器中 p1 所指向的元素,返回被删除的下一个元素的迭代器。
s1.erase(p1, p2) 删除 s1 容器中 [p1, p2) 区间内的元素,返回最后一个被删除元素的下一个元素的迭代器(即在删除前 p2 所指向元素的迭代器).
改变容器的大小
s1.resize(n) 将容器的大小变为n, 如果原有的元素个数大于 n 则容器末尾多余的元素会被删除; 如果原有的元素个数小于 n, 则在容器末尾会用T()填充。
首尾元素的直接访问
s.front() 获得容器首元素的引用。
s.back() 获得容器尾元素的引用。
在容器尾部插入、删除元素
s.push_back(t) 向容器尾部插入元素t
s.pop_back() 将容器尾部的元素删除
在容器头部插入、删除元素 列表 list 和双端队列 deque 两个容器支持高效地在容器头部插入新的元素或删除容器头部的元素,但是向量容器 vector 不支持。支持这一操作的概念构成了(FrontlnsertionSequence) “前插顺序容器” 这一概念,它是"顺序容器"的子概念。
s.push_front(t) 向容器头部插入元素t
s.pop_front() 删除容器头部的元素t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
using namespace std;
#include <vector>
#include <deque>
#include <list>
template<class T>
void printContainer(T iterator_begin, T iterator_end) {
while (iterator_begin != iterator_end) {
cout << *iterator_begin << " ";
iterator_begin++;
}
cout << endl;
}

int main() {
vector<int> v;
v.resize(10);
for (int i = 0; i < v.size(); i++) {
v[i] = i;
}
for (vector<int>::iterator v_it = v.begin(); v_it != v.end(); v_it++) {
cout << *v_it << " ";
}
cout << endl;

deque<int> d(v.rbegin(), v.rend());
while (!d.empty()) {
deque<int>::const_iterator d_c_it = d.end()-1 ;
cout << *d_c_it << " ";
d.pop_back();
}
cout << endl;

list<int> l;
l.assign(v.begin(), v.end());
l.push_back(100);
l.pop_front();
l.erase(l.begin());
printContainer<list<int>::iterator>(l.begin(), l.end());
return 0;
}

三种容器的比较

vector

向量容器是一种支持高效的随机访问和高效向尾部加入新元素的容器。向量容器­般实现为一个动态分配的数组,向量中的元素连续地存放在这个数组中,因此对向量容器进行随机访问,具有和动态访问动态数组几乎一样的效率。

将已有元素从向量容器中删除时,多出的闲置空间并不会被释放,因为再插入新的元素时可能会重新占用这些空间。因此,向量容器对象已分配的空间所能容纳的元素个数,常常会大于容器中实际有效的元素个数,前者叫做向量容器的容量(capacity) ,后者叫做向量容器的大小(size).
  |  
s.capacity() | 返回s的容量
s.reserve(n) | 若当前的容量大于或等于n,什么也不做,否则扩大s的容量,使得的容量不小于n. 如果在准备向向量容器中插入大量数据之前,能够粗略估计出插入元素之后向量元素的大小,就可以在插入前使用 reserve 函数来确保这部分空间被分配,避免在插入过程中多次重新分配空间,提高效率。

deque

双端队列是一种支持向两端高效地插入数据、支持随机访问的容器。双端队列的内部实现不如向量容器那样直观,在很多的 STL 实现中,双端队列的数据被表示为一个分段数组, "容器中的元素分段存放在一个个大小固定的数组中,此外容器还需要维护一个存放这些数组首地址的索引数.

执行向两端加入元素的操作时,会使所有的迭代器失效,但是不会使任何指向已有元素的指针、引用失效。指针和引用不会失效是因为向两端加入新元素不会改变已有元素在分段数组中的位置,而选代器之所以会失效,是因为向两端插入新元素可能会引起索引数组中已有元素位置的改变(例如索引数组被重新分配) ,而迭代器需要依赖索引数组。

当删除双端队列中间的元素时,由于被删除元素到某一端之间的所有元素都要向中间移动,删除的位置越靠近中间,效率越低,删除操作也会使所有迭代器和指针、引用失效。

list

列表是一种不能随机访问但可以高效地在任意位置插入和删除元素的容器。类似双向链表.
执行删除操作时只会使指向被删除元素的迭代器和指针、引用失效,而不会影响其他迭代器或指针、引用。
列表容器还支持一种特殊的操作 接合 (splice).

s1.splice(p. s2) 将 s2 列表的所有元素插入到s1列表中 p-1 和 p 之间, 将 s2 列表清空.
s1.splice(p, s2, q1) 将 s2 列表中 q1 所指向的元素插入到 s1列表中 p-1 和 p 之间, 将 q1 所指向的元素从 s2 列表中删除.
sl.splice(p, s2, ql, q2) 将 s2 列表中[q1, q2) 区间内的所有元素插入到s1列表中 p-1 和 p 之间, 将 [q1, q2) 区间内的元素从 s2 列表中删除.

执行接合操作时,原先指向被接入 s1 列表中的那些元素的迭代器和指针、引用都会失效,其他迭代器或指针、引用不会受到影响。

列表容器还支持其他特殊的操作,其中有很多是与一定的 STL 算法相对应的,例如删除(remove) 、条件删除 (remove_if)、排序 (sort) 、去重(unique) 、归并(merge) 、倒序(reverse) 等。但由于列表容器存储结构的特殊性,直接使用这些 STL 算法并不能达到最高的效率,因此这些算法提供了专门针对列表容器的实现,作为列表容器的成员函数。

顺序容器的插入迭代器

插入迭代器是一种适配器,使用它可以通过输出迭代器的接口来向指定元素的指定位置插入元素。因而如果在调用 STL 算法时使用输出迭代器,可以将结果顺序插入到容器的指定位置,而无须覆盖已有的元素。

1
2
3
template<class FrontlnsertionSequence>class front_insert_iterator;
template<class Sequence>class back_insert_iterator;
template<class Sequence>class insert_iterator;

把一个容器类型作为模板参数,分别用于向指定容器头部、尾部和中间某个指定位置插入元素.front_insert_iterator 只适用于前插顺序容器(双端队列和列表), back_insert_iterator insert_iterator 适用于所有顺序容器。

插入迭代器的实例可以通过构造函数来创建,但一般无须直接调用构造函数,而是可以通过下面的三个辅助函数。

1
2
3
4
5
6
template<class FrontlnsertionSequence> 
front_insert_iterator<FrontlnsertionSequence> front_inserter(FrontlnsertionSequence& s);
template<class Sequence>
back_insert_iterator<Sequence> back_inserter(Sequence& s);
template<class Sequence , class Iterator>
insert_iterator<Sequence> inserter(Sequence& s , Iterator i);

使用辅助函数与直接使用构造函数的优点在于,辅助函数是一般的函数模板,调用时可以自动推导类型参数而无须显式将其给出,从而使代码变得更简短,因此人们一般用辅助函数代替构造函数。

顺序容器的适配器

STL 提供的容器适配器找(stack) 和队列 (queue) ,就是对顺序容器的封装。如果使用时不指定第二个参数,则默认使用双端队列容器。

template<class T, class Sequence=deque<T>>class stack;
template<class T , class FrontInsertonSequence = deque<T> > class queue;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;
#include<stack>
#include<vector>

int main() {
stack<int, vector<int>> s; // 使用容器适配器将vector的接口转换成stack的接口
for (int i = 0; i < 5; i++) {
s.push(i);
}
while (!s.empty()) {
cout << s.top() << endl;
s.pop();
}
return 0;
}

优先队列适配器
template< class T, class Sequence= lector<T>>class priority_queue;

优先级队列的基础容器必须是支持随机访问的顺序容器,因此它必须是向量容器或双端队列容器,默认为向量容器。优先级队列也像栈和队列一样支持元素的压人和弹出,但元素弹出的顺序与压入的顺序无关,而是与元素的大小有关,每次弹出的总是容器中最"大"的一个元素。

关联容器

对于关联容器而言,它的每个元素都有一个键 (key) ,容器中元素的顺序按照键的取值升序排列
而关联容器会将元素根据键的大小组织在一棵"平衡二又树"中,最坏情况下只需要大约 log_2{n} 次比较就可根据键来查找一个元素。

简单关联容器只有一个类型参数,该类型既是键类型,又是容器类型; 二元关联容器则有两个类型参数,前一个是键类型,后一个是附加数据的类型,二元关联容器的元素类型是键类型和附加数据类型的组合,这种组合类型可以用一个二元组 (pair)来表示, pair 是 <utility> 头文件中定义的结构体模板.

S s(q1, q2) ` 迭代器 [q1, q2) 区间内的数据作为 的元素构造
s.ínsert (t)
s.insert( p1 , t)
s.insert(q1 , q2)
s.erase( p1) 删除 p1 所指向的元素。
s.erase( p1 , p2) 删除 [p1 p2) 区间内的元素。
s.erase( k) 删除所有键为 的元素,返回被删除元素的个数。
s.find(k) 找到任意一个键为k的元素,返回该元素的迭代器,如果没有键为k的元素,则返回 s.end()
s.lower_bound(k) 得到 s中第一个键值不小于k的元素的迭代器。
s.upper_bound(k) 得到 s 中第一个键值大于k的元素的迭代器。
s.equal_range(k) 得到一个用 pair<S::iterator , S::iterator> 表示的区间,记为 [pl. p2) 。该区间刚好包含所有键为k的元素 p1==s.lower_bound(k) p2==s.upper_bound(k) 一定成立。
s.count(k) 得到容器中键为k的元素个数。
关联容器的插入和删除操作不会使任何已有的迭代器、指针或引用失效。

set

集合用来存储一组无重复的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
using namespace std;
#include<string>
#include<set>

int main() {
set<string> s;
s.insert("happy");
pair<set<string>::iterator, bool> res = s.insert("happy");
cout << res.second << endl; // 0
s.insert("good");
s.insert("funk");
copy(s.begin(), s.end(), ostream_iterator<string>(cout, " ")); // funk good happy
return 0;
}

map

映射的元素类型是由键和附加数据所构成的二元组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
using namespace std;
#include<string>
#include<set>
#include<map>

int main() {
map<string, int> m;
m.insert(pair<string, int>("zhangsan", 18));
m.insert(make_pair("lisi", 39));
m.insert(make_pair("wangwu", 10));
m.insert(make_pair("slacr", 92));
m.erase("slacr");
m["lisi"] = 100;
for (pair<string, int> entry : m) {
cout << entry.first << " " << entry.second << endl;
}
//lisi 100
//wangwu 10
//zhangsan 18
}

函数对象

函数对象 (function object / functor) STL 提供的一类主要组件,它使得 STL应用更加灵活方便,从而增强了算法的通用性。大多数 STL 算法可以用一个函数对象作为参数。所谓函数对象其实就是一个行为类似函数的对象,它可以不需参数,也可以带有若干参数,其功能是获取一个值,或者改变操作的状态。在 C++程序设计中.任何普通的函数和任何重载了调用运算符 operator() 的类的对象都满足函数对象的特征,因此都可以作为函数对象传递给算法作为参数使用。

常用的函数对象可分为产生器 (generator) 、一元函数 (unary function) 、二元函数( binary function) 、一元谓词 (unary predicate) 和二元谓词 (binary predicate) 函数对象五大类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<iostream>
using namespace std;
#include<string>
#include<algorithm>
#include<list>
#include<numeric>


template<class T>
T muti(T t1, T t2) { return t1 * t2; };

template<class T>
class myplus {
public:
T operator()(T t1, T t2) { return t1 + t2; }
};

int main() {
list<int> l;
l.push_back(4);
l.push_back(2);
l.push_back(5);
cout << accumulate(l.begin(), l.end(), 1, muti<int>) << endl;
cout << accumulate(l.begin(), l.end(), 0, myplus<int>()) << endl; // 仿函数, 这里的muplus<int>()整体是函数名,括号是重载的运算符

return 0;
}

产生器 (generator) 一元函数 (unary function) 二元函数 (binary function)

将具有 0 个、 1 个和 2 个传入参数的函数对象,分别称为产生器( generator) 、一元函数 (unary function) 和二元函数 (binary function).

调用这些标准函数对象,需要包含头文件 。标准函数对象是内联函数。

一元谓词 (unary predicate) 二元谓词 (binary predicate)

这种函数对象要求返回值为 bool 型,并具有一个或两个参数,称之为一元谓词或二元谓词函数对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;
#include<string>
#include<algorithm>
#include<functional>
#include<vector>

int main() {
int arr[] = { 1, 3, 4, 5, 2 };
vector<int> vi(arr, arr + 5);
sort(vi.begin(), vi.end(), less<int>());
copy(vi.begin(), vi.end(), ostream_iterator<int>(cout, " "));
return 0;
}

关联类型 (associated types)

STL 中,定义了两个函数对象的基类 unary_function binary_function ,分别用于设计一元函数对象和二元函数对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
using namespace std;
#include<string>
#include<algorithm>
#include<functional>
#include<vector>


class my_binary_pred : public binary_function<pair<string, int>, pair<string, int>, bool> {
public:
bool operator()(pair<string, int> p1, pair<string, int> p2) {
return p1.second < p2.second;
}
};

int main() {
vector<pair<string, int>> sheet;
sheet.push_back(make_pair("slacr", 98));
sheet.push_back(make_pair("Tim", 66));
sheet.push_back(make_pair("Jose", 54));
sheet.push_back(make_pair("frank", 99));
sort(sheet.begin(), sheet.end(), my_binary_pred());
for (pair<string, int> p : sheet) { cout << p.first << " " << p.second << endl; }

return 0;
}

函数适配器

函数适配器实现了这一功能,将一种函数对象转化为另一种符合要求的函数对象。函数适配器可以分为4大类: 绑定适配器(bind adaptor) 、组合适配器 (composite adaptor) 、指针函数适配器 (pointer function adaptor) 和成员函数适配器 (member function adaptor)

直接构造 STL 中的函数适配器通常会导致冗长的类型声明。为简化函数适配器的构造 .STL 还提供了函数适配器辅助函数. 借助于泛型自动推断技术,无
须显式的类型声明便可实现函数适配器的构造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
using namespace std;
#include<algorithm>
#include<functional>


greater<int> g;
bool greater5(int x) {
return g(x, 5);
}

int main() {
int arr[] = { 2, 3, 1, 4, 7 };
if (find_if(arr, arr + 5, greater5)) {
cout << "find it" << endl;
} // find it


// std::binder2nd<_Fn>::binder2nd(const _Fn & _Func, const typename _Fn::second_argument_type & _Right)
if (find_if(arr, arr + 5, bind2nd(greater<int>(), 5))) {
cout << "find it" << endl;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<iostream>
using namespace std;
#include<algorithm>
#include<functional>
#include<vector>

bool g(int x, int y) { return x > y; }

int main() {
int arr[] = { 30, 40, 50 , 60, 70 };
const int len = sizeof arr / sizeof arr[0];
vector<int> v(arr, arr + len);

vector<int>::iterator p;
p = find_if(v.begin(), v.end(), bind2nd(ptr_fun(g), 40)); // 指针函数适配器
if (p == v.end()) {
cout << "none" << endl;
} else {
cout << "found" << endl;
}
p = find_if(v.begin(), v.end(), not1(bind2nd(greater<int>(), 40)));
if (p == v.end()) {
cout << "none" << endl;
} else {
cout << "found" << endl;
}
p = find_if(v.begin(), v.end(), bind2nd(not2(greater<int>()), 40));
if (p == v.end()) {
cout << "none" << endl;
} else {
cout << "found" << endl;
}
return 0;
}

算法

算法本身就是一种函数模板。STL 的算法是通用的:每个算法都适合于若干种不同的数据结构.
算法不是直接使用容器作为参数,而是使用迭代器类型。
STL 的算法可以分为4大类: 不可变序列算法、可变序列算法、排序和搜索算法、数值算法

不可变序列算法

不可变序列算法 (non-mutating algorithms) 是指那些不直接修改所操作的容器内容的算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<iostream>
using namespace std;
#include<algorithm>
#include<functional>
#include<vector>

bool g(int x, int y) { return x > y; }

int main() {
int arr[] = { 1, 2, 3, 4, 5, 5, 6, 6, 7 };
int len = sizeof(arr) / sizeof(arr[0]);
vector<int> v1(arr, arr+len);

int sub_arr[] = { 3, 4 };
len = sizeof(sub_arr)/ sizeof(sub_arr[0]);
vector<int> v2(sub_arr, sub_arr + len);
cout << *adjacent_find(v1.begin(), v1.end()) << endl;
// 找出第一个相邻元素

cout << count_if(v1.begin(), v1.end(), bind2nd(greater<int>(), 5)) << endl;
// 统计大于5的元素个数

cout << *find_if(v1.begin(), v1.end(), bind2nd(less<int>(), 2)) << endl;
// 找出第一个小于2的元素

cout << *search(v1.begin(), v1.end(), v2.begin(), v2.end()) << endl;
// 找出子序列在原序列中第一次出现位置的元素

cout << *search_n(v1.begin(), v1.end(), 3, 4, greater<int>()) << endl;
// 找出第一次连续出现3个>4的元素的元素

cout << equal(v1.begin(), v1.end(), v2.begin(), v2.end()) << endl;
// 判断两个序列是否相等

pair<vector<int>::iterator, vector<int>::iterator> res = mismatch(v1.begin(), v1.end(), v2.begin(), v2.end());
// 查找v2在v1中不匹配的首位置
return 0;
}

可变序列算法

可变序列算法 (mutating algorithms) 可以修改它们所操作的容器的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include<iostream>
using namespace std;
#include<algorithm>
#include<functional>
#include<vector>
#include<random>
class myGenerator {
public:
int operator()() {
// 创建随机数生成器
random_device rd;
mt19937 gen(rd());
// 创建分布器
uniform_int_distribution<> dis(1, 9);
// 生成随机数
return dis(gen);
}
};

bool g(int x, int y) { return x > y; }

int main() {
int arr[] = { 1, 2, 3, 4, 5, 5, 6, 6, 7 };
int len = sizeof(arr) / sizeof(arr[0]);
vector<int> v1(arr, arr+len);

int sub_arr[] = { 3, 4 };
len = sizeof(sub_arr)/ sizeof(sub_arr[0]);
vector<int> v2(sub_arr, sub_arr + len);
ostream_iterator<int> output(cout, " ");

fill(v1.begin(), v1.end(), 0);
// 将v1填充 0
copy(v1.begin(), v1.end(), output);
cout << endl;

generate(v1.begin(), v1.end(), myGenerator());
// 生成每一个元素为随机值
copy(v1.begin(), v1.end(), output);
cout << endl;

remove_copy(v1.begin(), v1.end(), back_inserter(v2), 5);
// 删除v1小于5的元素并放到v2后
copy(v2.begin(), v2.end(), output);
cout << endl;

v2.erase(remove_if(v2.begin(), v2.end(), bind2nd(less<int>(), 4)));
// 删除v2小于4的值, 只用remove_if不会清除原来空间
copy(v2.begin(), v2.end(), output);
cout << endl;

replace(v2.begin(), v2.end(), 4, 0);
// 将v2中4改为0
copy(v2.begin(), v2.end(), output);
cout << endl;

reverse(v2.begin(), v2.end());
// 逆序
copy(v2.begin(), v2.end(), output);
cout << endl;

rotate_copy(v2.begin(), v2.begin() + 4, v2.end(), output);
// 将两个区间旋转输出, 不会改变原容器
cout << endl;


vector<int> temp(v2);
rotate_copy(temp.begin(), temp.begin() + 4, temp.end(), v2.begin());
// 旋转序列改变原序列.
copy(v2.begin(), v2.end(), output);

return 0;
}

排序和搜索算法

sort 要求 first last 必须是随机迭代器类型,因为 sort 的具体实现使用了快速排序算法,使用随机迭代器是出于效率上的考虑。
stable_sort 原型声明和 sort 几乎相同,它的不同之处在于,对于相等数值的元素,排序前后的相对位置将保持不变。

lower_bound函数返回指向第一个大于或等于指定值的元素的迭代器,如果所有元素都小于指定值,则返回容器的end()迭代器。
upper_bound函数返回指向第一个大于指定值的元素的迭代器,如果所有元素都小于或等于指定值,则返回容器的end()迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include<iostream>
using namespace std;
#include<algorithm>
#include<functional>
#include<vector>
#include <random>

int main() {
int arr[] = { 24, 32, 42, 51, 12, 34, 54 };
vector<int> v(arr, arr + sizeof(arr) / sizeof(arr[0]));
int arr1[] = { 39, 54, 87, 43 };
vector<int> v2(arr1, arr1 + sizeof(arr1) / sizeof(arr1[0]));


vector<int>::iterator p = max_element(v.begin(), v.end());
// 计算区间最值
int n = p - v.begin();
int dist = distance(v.begin(), p); // 这个函数也可以算
cout << "max_e: " << *p << " idx: " << n << " distance: " << dist << endl;

vector<int> v1(5);
partial_sort_copy(v.begin(), v.end(), v1.begin(), v1.end());
// 将v局部排序并拷贝到其他容器, v本身不变化
copy(v1.begin(), v1.end(), ostream_iterator<int>(cout, " "));
cout << endl;

sort(v.begin(), v.end(), greater<int>());
// 降序排列
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
cout << endl;

cout << *lower_bound(v.begin(), v.end(), 50, greater<int>()) << endl;
// 默认的lower_bound必须要求按照升序排序后才行, 之前是是按降序排序的,这里也要传一个相同的谓词
cout << *upper_bound(v.begin(), v.end(), 50, greater<int>()) << endl;

cout << binary_search(v.begin(), v.end(), 32, greater<int>()) << endl; // 1

vector<int> v3(v.size() + v2.size());
sort(v.begin(), v.end());
sort(v2.begin(), v2.end());
merge(v.begin(), v.end(), v2.begin(), v2.end(), v3.begin());
// 将v,v2合并到v3中
copy(v3.begin(), v3.end(), ostream_iterator<int>(cout, " "));
cout << endl;

random_device rd;
mt19937 g(rd());
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
cout << endl;
shuffle(v.begin(), v.end(), g);
// 打乱元素
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
cout << endl;
nth_element(v.begin(), v.begin() + 2, v.end());
// 保证在第n个位置之前的元素都小于等于第n个元素,而在第n个位置之后的元素都大于等于第n个元素。这个能更高效部分查找
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
cout << endl;

stable_sort(v.begin(), v.end());
// 稳定的排序, 保持相对位置
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
cout << endl;

inplace_merge(v.begin(), v.begin()+2, v.end());
// 合并两个有序序列, 保存到原序列
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
cout << endl;

cout << lexicographical_compare(v.begin(), v.end(), v2.begin(), v2.end()) << endl;
// 按字典顺序比较 // 1
return 0;
}

数值算法

STL 提供了 个通用数值算法。为了调用此类算法,需要包含头文件

扩展

利用函数对象的类型特征实现函数适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 将二元函数转换成一元函数
template<class BinaryFunction>
class binder1st : public unary_function<
typename BinaryFunction::second_argument_type,
typename BinaryFunction::result_type>
{
protected:
BinaryFunction op;
typename BinaryFunction::first_argument_type value;
public:
// 构造函数
binder1st(const BinaryFunction& op, const typename BinaryFunction::first_argument_type & value) : op(op), value(value) {}
// 重载()
typename Operation::result_type operator() (const typename BinaryFunction::second_argument_type& x) const {
return op(value, x);
}
};

c++ 标准规定,凡是在模板中引用的、依赖于模板参数的数据类型,必须用 typename 修饰,否则该标识符就不被解析为一个数据类型。

迭代器的类型特征

两个指针相减得到的数值的类型就是 ptrdiff_t

iterator_category 起到了非常特殊的作用,它可以是下面个结构体之一。

struct input iterator tag { };
struct output_iterator_tag { };
struct forward iterator tag: public input iterator tag { };
struct bidirectional_iterator_tag: public forward_iterator_tag { };
struct random access iterator tag: public bidirectional iterator tag { };

5个结构体全部定义在 头文件中,它们都不具有任何成员,只用来表示一个迭代器所属的分类。其中每个结构体对应于一种迭代器概念,迭代器之间的继承关系也与相应概念及其子概念之间的关系一致。

与函数对象的 unary_function binary_function 这两个关联类型类似,迭代器也有相应的关联类型 iterator ,它也定义在 头文件中

1
2
3
4
5
6
7
8
9
template<class Category, class T, class Distance = ptrdiff_t,
class Pointer = T*, class Reference = T&>
struct iterator {
typedef T value type;
typedef Distance difference type;
typedef Pointer poi teri
typedef Reference reference;
typedef Category iterator_category;
};

当定义自己的迭代器时,元须分别定义各个类型特征,只要继承 iterator 类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
using namespace std;
#include<algorithm>
#include<functional>
#include<vector>
#include<numeric>


template<class T>
class IncrementIterator : public iterator<input_iterator_tag, T, ptrdiff_t, const T*, const T&> {
private:
T value;
public:
typedef IncrementIterator<T> Self;
IncrementIterator(const T& value = T()) : value(value) {} // 构造函数
bool operator== (const Self& rhs) const { return value == rhs.value; }
bool operator!= (const Self& rhs) const { return value != rhs.value; }
Self& operator++() { value++; return *this; } // 前缀++
Self& operator++(int) { // 后缀++
Self tmp = *this;
value++;
return tmp;
}
const T& operator*() const { return value; }
const T* operator->() const { return &value; }
};


int main() {
copy(IncrementIterator<int>(), IncrementIterator<int>(10), ostream_iterator<int>(cout, " "));
cout << endl;
int s[] = { 1, 2, 3, 4 ,5 };
transform(s, s + sizeof(s) / sizeof(int), IncrementIterator<int>(), ostream_iterator<int>(cout, " "), plus<int>());
//template<class InputIt1, class InputIt2, class OutputIt, class BinaryOperation>
// OutputIt transform(InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt d_first, BinaryOperation binary_op);
// 这个transform是一个重载版本, 有两个输入迭代器, 处理函数是plus, 输出迭代器ostream_iterator

cout << endl;
// 0 1 2 3 4 5 6 7 8 9
// 1 3 5 7 9
return 0;
}

利用类型特征实现算法

头文件中提供了一个作为助手的模板 iterator_tralts ,使用它可以以统一的方式得到包括指针在内的各种迭代器的类型特征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<class Iterator>struct iterator_traits { 
typedef typename Iterator::difference_type difference_type;
typedef typename Iterator::value_type value_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator:: reference reference;
typedef typename Iterator::iterator_category iterator_category;
};

// <iterator> 中还对 iterator_traits 进行了偏特化:
template<class T>struct iterator_traits <T*>{
typedef ptrdiff_t difference_type;
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef random_access_iterator_tag iterator_category;
}
//
template< class T> struct iterator_traits<const T*> {
typedef ptrdiff_t difference_type;
typedef T value_type;
typedef const T* pointer;
typedef const T& reference;
typedef random_access_iterator_tag iterator_category;
};

可以把一个迭代器类型作为它的模板参数,该模板中定义了 种迭代器的类型特征,这些特征分别使用作为模板参数的迭代器的相应类型特征来定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
using namespace std;
#include<algorithm>
#include<functional>
#include<vector>
#include<numeric>

template<class InputIterator, class OutputIterator>
void mySort(InputIterator first, InputIterator last, OutputIterator result) {
vector<typename iterator_traits<InputIterator>::value_type> s(first, last);
sort(s.begin(), s.end());
copy(s.begin(), s.end(), result);
}

int main() {
double a[5] = { 3.1, 4,3, 8.4, 2.0};
mySort(a, a + 5, ostream_iterator<double>(cout, " "));
cout << endl;

mySort(istream_iterator<int>(cin), istream_iterator<int>(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}

知识点

  1. 删除向量容器的元素时,并不会使空闲的空间被释放,这时可以使用下面的语句达到释放多余空间的目的
    1
    vector<T> (s .begin() s.end()).swap(s);
  2. C++ 中,接口通常通过纯虚函数或接口类来定义,并被用于实现多态和接口隔离等设计模式。
  3. mem_fn函数是一个类模板,用于将一个成员函数转换为一个普通函数对象。
  4. Boost库: Boost 社区和 c++ 标准委员会具有非常密切的联系。Boost 中的一部分程序库是对 TR1 的实现,所谓 TR1 (C++ Technical Report 1)是指 c++ 标准委员会发布的一个文档,文档中定义了对C++标准库的一些扩展,TR1 即将被纳入下一版 c++ 标准。

参考

  1. 《C++语言程序设计(第4版)》 IBSN 9787302227984
  2. C++参考手册
  3. Microsoft C++文档
  • Author:

    slacr_

  • Copyright:

  • Published:

    May 2, 2023

  • Updated:

    May 2, 2023

Buy me a cup of coffee ☕.

1000000