0%

序言

C++ 语言的核心优势之一就是便于软件的重用。C++ 中有两个方面体现重用:

  • 一是面向对象的继承和多态机制

  • 二是通过模板的概念实现了对泛型程序设计的支持

C++ 的标准模板库(Standard Template Library,STL)是泛型程序设计最成功应用的实例。STL 是一些常用数据结构(如链表、可变长数组、排序二叉树)和算法(如排序、查找)的模板的集合。

有了 STL,程序员就不必编写大多数常用的数据结构和算法。而 STL 是经过精心设计的,运行效率很高,比水平一般的程序员编写的同类代码速度更快。

有一种说法,C++ 是用来编写大程序的,如果只是编写几十上百行的小程序,用 C 语言就可以,没有必要用 C++。

这个说法是不准确的。可以说,小程序没必要用面向对象的方法,但是用 C++ 还是能够带来很大方便的,因为 C++ 中有 STL。哪怕编写只有十几行的程序,也可能会用到 STL 中提供的数据结构和算法。例如对数组排序,用 STL 中的 sort 算法往往只需要一条语句就能解决,而不用像 C 语言库函数 qsort 那样还要编写比较函数。

容器

容器(container)用于存放数据的类模板。可变长数组、链表、平衡二叉树等数据结构在 STL 中都被实现为容器。

程序员使用容器时,即将容器类模板实例化为容器类时,会指明容器中存放的元素是什么类型的。

容器中可以存放基本类型的变量,也可以存放对象。对象或基本类型的变量被插入容器中时,实际插入的是对象或变量的一个复制品。

STL 中的许多算法(即函数模板),如排序、查找等算法,在执行过程中会对容器中的元素进行比较。这些算法在比较元素是否相等时通常用运算符进行,比较大小通常用 < 运算符进行,因此,被放入容器的对象所属的类最好重载 ==< 运算符,以使得两个对象用 ==< 进行比较是有定义的。

容器分类两大类

顺序容器

顺序容器有以下三种:可变长动态数组 vector、双端队列 deque、双向链表 list。

它们之所以被称为顺序容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置(尾部、头部或中间某处)插入,元素就会位于什么位置。

关联容器

关联容器有以下四种:set、multiset、map、multimap。关联容器内的元素是排序的。插入元素时,容器会按一定的排序规则将元素放到适当的位置上,因此插入元素时不能指定位置。

默认情况下,关联容器中的元素是从小到大排序(或按关键字从小到大排序)的,而且用 < 运算符比较元素或关键字大小。因为是排好序的,所以关联容器在查找时具有非常好的性能。

除了以上两类容器外,STL 还在两类容器的基础上屏蔽一部分功能,突出或增加另一部分功能,实现了三种容器适配器:栈 stack、队列 queue、优先级队列 priority_queue。

容器都是类模板。它们实例化后就成为容器类。用容器类定义的对象称为容器对象。

例如,vector<int> 是一个容器类的名字,vector<int> a; 就定义了一个容器对象 a,a 代表一个长度可变的数组,数组中的每个元素都是 int 类型的变量;vector<double> b; 定义了另一个容器对象 b,a 和 b 的类型是不同的。本教程后文所说的 "容器",有时候也指 "容器对象"。

任何两个容器对象,只要它们的类型相同,就可以用 <、<=、>、>=、!= 进行词典式的比较运算。假设 a、b 是两个类型相同的容器对象,这些运算符的运算规则如下。

  • a == b: 若 a 和 b 中的元素个数相同,且对应元素均相等,则 a == b 的值为 true,否则值为 false。元素是否相等使用 == 运算符进行判断的。

  • a < b: 规则类似于词典中两个单次比较大小,从头到位依次比较每个元素,如果发生 a 中的元素小于 b 中元素的情况,则 a < b 的值为 true;如果没有发生 b 中的元素小于 a 中的元素的情况,且 a 中的元素个数比 b 少,a < b 的值也为 true;其他情况下值为 false。元素比较大小是通过 < 运算符进行的。

  • a != b: 等价于 !(a == b)

  • a > b: 等价于 b < a

  • a <= b: 等价于 !(b < a)

  • a >= b: 等价于 !(a < b)

所有容器都有以下两个成员函数:

  • int size(): 返回容器中元素的个数。

  • bool empty(): 判断容器对象是否为空。

顺序容器和关联容器还有以下成员函数:

  • begin(): 返回指向容器中第一个元素的迭代器。

  • end(): 返回指向容器中最后一个元素后面的位置的迭代器。

  • rbegin(): 返回指向容器中最后一个元素的反向迭代器。

  • rend(): 返回指向容器中第一个元素前面的位置的反向迭代器。

  • erase(...): 从容器中删除一个或几个元素。该函数参数比较复杂,此处省略。

  • clear(): 从容器中删除所有元素。

如果一个容器是空的,则 begin()end() 的返回值相等,rbegin()rend() 的返回值也相等。

顺序容器还有以下常用成员函数:

  • front(): 返回容器中第一个元素的引用。

  • back(): 返回容器中最后一个元素的引用。

  • push_back(): 在容器末尾增加新元素。

  • pop_back(): 删除容器末尾的元素。

  • insert(...): 插入一个或多个元素。该函数参数较复杂,此处省略。

STL 提供能在各种容器中通用的算法(大约 70 种),如插入、删除、查找、排序等。算法就是函数模板。算法通过迭代器来操纵容器中的元素。

许多算法操作的是容器上的一个区间(也可以是整个容器),因此需要两个参数,一个是区间起点元素的迭代器,另一个是区间终点元素的后面一个元素的迭代器。例如,排序和查找算法都需要这两个参数来指明待排序或待查找的区间。

有的算法返回一个迭代器。例如,find 算法在容器中查找一个元素,并返回一个指向该元素的迭代器。

算法可以处理容器,也可以处理普通的数组。

有的算法会改变其所作用的容器。例如:

  • copy:讲一个容器的内容复制到另一个容器。

  • remove:在容器中删除一个元素。

  • random_shuffle:随机打乱容器中的元素

  • fill:用某个值填充容器。

有的算法不会改变其所作用的容器。例如:

  • find:在容器中查找元素。

  • count_if:统计容器中符合某种条件的元素的个数。

STL 中的大部分常用算法都在头文件 algorithm 中定义。此外,头文件 numeric 中也有一些算法。

下面介绍一个常用算法 find,以便对算法是什么、怎么用有一个基本的概念。find 算法和其他算法一样都是函数模板。find 模板的原型如下:

1
2
template<class Init, class T>
Init find(Init first, Init last, const T& val);

其功能可以是在迭代器 first、last 指定的容器的一个区间[first, last) 中,按顺序查找和 val 相等的元素。如果找到,就返回该元素的迭代器;如果找不到,就返回 last。

1
[first, last) 这个区间是一个坐闭右开的区间,即 last 指向的元素其实不在此区间内。

find 模板使用 == 运算符判断元素是否相等。因此,如果 [first, last) 区间中存放的是对象,则 == 运算符应该被适当重载,使得两个对象可以用 == 运算符比较。

注意:上一段话说的是 "其功能可以是",而不是 "其功能就是"。这是因为模板只是一种代码形式,这种代码形式具体能完成什么功能,取决于程序员对该模板写法的了解及其想象力。按照语法,调用 find 模板时,first 和 last 只要类型相同就可以,不一定必须是迭代器。

演示 find 用法的程序如下:

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<vector>
#include<algorithm>
#include<iostream>
using namespace std;
int main() {
int a[10] = {10, 20, 30, 40};
vector<int> v;
v.push_back(1); v.push_back(2);
v.push_back(3); v.push_back(4);
vector<int>::iterator p;
p = find(v.begin(), v.end(), 3); // 在 v 中查找 3
if (p != v.end()) // 若找不到,find 返回 v.end()
cout << "1)" << *p << endl; // 找到了
p = find(v.begin(), v.end(), 9);
if (p == v.end())
cout << "not found " << endl; // 没找到
p = find(v.begin() + 1, v.end() - 1, 4);
cout << "2)" << *p << endl;
int *pp = find(a, a + 4, 20);
if (pp == a + 4)
cout << "not found" << endl;
else
cout << "3)" << *pp << endl;
}

输出:

1
2
3
4
1)3
not found
2)4
3)20

第 11 行,要查找的区间是 [v.begin(), v.end()),v.end() 不在查找范围内,因此没有问题。本行的查找会成功,因此 p 指向找到的元素 3。

第 17 行,因为要查找的区间是 [v.begin() + 1, v.end() - 1),这个区间中只有 2、3 这两个元素,因此会查找失败,p 的值变为 v.end() - 1,因此 p 正好指向 4 这个元素。

第 19 行,数组 a 是一个容器。数组名 a 的类型是 int,可以做迭代器使用,表达式 a + 4 的类型也是 int,因此也能做迭代器。本次调用 find,查找区间是 [a, a + 4),即数组 a 的前 4 个元素。如果查找失败,find 就会返回 a + 4。

STL 中还有一个常用的算法 sort,对于容器排序,其原型为:

1
2
template<class_RandIt>
void sort(_RandIt first, _RandIt last);

该算法可以用来对区间 [first, last) 从小到大进行排序。下面两行程序就能对数组 a 排序:

1
2
int a[4] = {3, 4, 2, 1};
sort(a, a + 4);

要访问顺序容器和关联容器中的元素,需要通过 "迭代器(iterator)" 进行。迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。

迭代器按照定义方式分成以下四种。

  1. 正向迭代器,定义方法如下:
1
容器类名::iterator 迭代器名;
  1. 常量正向迭代器,定义方法如下:
1
容器类名::const_iterator 迭代器名;
  1. 反向迭代器,定义方法如下:
1
容器类名::reverse_iterator 迭代器名;
  1. 常量反向迭代器,定义方法如下:
1
容器类名::const_reverse_iterator 迭代器名;

迭代器用法示例

通过迭代器可以读取它指向的元素,*迭代器名 就表示迭代器指向的元素。通过非常量迭代器还能修改其指向的元素。

迭代器都可以进行 ++ 操作。反向迭代器和正向迭代器的区别在于:

  • 对正向迭代器进行 ++ 操作时,迭代器会指向容器中的后一个元素;

  • 而对反向迭代器进行 ++ 操作时,迭代器会指向容器的前一个元素。

下面的程序演示了如何通过迭代器遍历一个 vector 容器中的所有元素。

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>
#include <vector>
using namespace std;
int main()
{
// v 是存放 int 类型变量的可变长数组,开始时没有元素
vector<int > v;
for (int i = 0; i < 5; ++i) {
// push_back 成员函数在 vector 容器尾部添加一个元素
v.push_back(i);
}
// 定义正向迭代器
vector<int>::iterator it;
// 用迭代器遍历容器
for (it = v.begin(); it != v.end(); ++it) {
// *it 就是迭代器 it 指向的元素
cout << *it << " ";
// 每个元素变为原来的 2 倍
*it *= 2;
}
cout << endl;

// 用反向迭代器遍历容器
for (vector<int>::reverse_iterator j = v.rbegin(); j != v.rend(); ++j)
cout << *j << " ";
return 0;
}

输出:

1
2
0 1 2 3 4
8 6 4 2 0

vector<int > v; 容器有多个构造函数,如果无参构造函数初始化,则容器一开始是空的。

begin 成员函数返回指向容器中第一个元素的迭代器。++it 使得 it 指向容器中的下一个元素。end 成员函数返回的不是指向最后一个元素的迭代器,而是指向最后一个元素后面的位置的迭代器,因此循环的终止条件是 it != v.end()

vector<int>::reverse_iterator j = v.rbegin(); 定义了反向迭代器用以遍历容器。反向迭代器进行 ++ 操作后,会指向容器中的上一个元素。rbegin 成员函数返回指向容器中最后一个元素的迭代器,rend 成员函数返回指向容器中第一个元素前面的位置的迭代器,因此本循环实际上是从后往前遍历整个数组。

如果迭代器指向容器中最后一个元素的后面或第一个元素的前面,再通过该迭代器访问元素,就有可能导致程序崩溃,这和访问 NULL 或未初始化的指针指向的地方类似。

上面循环里面的 ++it++j 相比于 it++j++,程序的执行速度更快。

迭代器的功能分类

不同容器的迭代器,其功能强弱有所不同。容器的迭代器的功能强弱,决定了该容器是否支持 STL 中的某种算法。例如,排序算法需要通过随机访问迭代器中的元素,因此有的容器就不支持排序算法。

常用的迭代器按功能强弱分为输入、输出、正向、双向、随机访问五种,这里只介绍常用的三种。

  1. 正向迭代器。假设 p 是一个正向迭代器,则 p 支持以下操作:++p、p++、*p。此外,两个正向迭代器可以互相赋值,还可以用 == 和 != 运算符进行比较。

  2. 双向迭代器。双向迭代器具有正向迭代器的全部功能。除此之外,若 p 是一个双向迭代器,则 --p 和 p-- 都是有定义的。--p 使得 p 朝和 ++p 相反的方向移动。

  3. 随机访问迭代器。随机访问迭代器具有双向迭代器的全部功能。若 p 是一个随机访问迭代器,i 是一个整型变量或常量,则 p 还支持以下操作:

  • p += i: 使得 p 往后移动 i 个元素

  • p -= i: 使得 p 往前移动 i 个元素

  • p + i: 返回 p 后面第 i 个元素的迭代器

  • p - i: 返回 p 前面第 i 个元素的迭代器

  • p[i]: 返回 p 后面第 i 个元素的引用。

此外,两个随机访问迭代器 p1、p2 还可以用 <、>、<=、>= 运算符进行比较。p1 < p2 的含义是:p1 经过若干次(至少一次)++ 操作后,就会等于 p2。其他比较方式的含义与此类似。

对于两个随机访问迭代器 p1、p2,表达式 p2 - p1 也是有定义的,其返回值是 p2 所指向元素和 p1 所指向元素的序号之差(也可以说是 p2 和 p1 之间的元素个数减 1)。

不同容器的迭代器的功能:

容器 迭代器功能
vector 随机访问
deque 随机访问
list 双向
set/multiset 双向
map/multimap 双向
stack 不支持迭代器
queue 不支持迭代器
priority_queue 不支持迭代器

例如,vector 的迭代器是随机迭代器,因此遍历 vector 有以下两种做法。下面的程序中,每个循环演示了一种做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v(100);
for (int i = 0; i < v.size(); ++i)
cout << v[i]; // 像普通数组一样使用 vector 容器
vector<int>::iterator i;
for (i = v.begin(); i != v.end(); ++i)
cout << *i;
for (i = v.begin(); i < v.end(); ++i)
cout << *i;
i = v.begin();
while (i < v.end()) {
cout << *i;
i += 2;
}
return 0;
}

list 容器的迭代器是双向迭代器。假设 v 和 i 的定义如下:

1
2
list<int> v;
list<int>::const_iterator i;

则以下代码是合法的:

1
2
for (i = v.begin(); i != v.end(); ++i)
cout << *i;

以下代码则不合法:

1
2
for (i = v.begin(); i < v.end(); ++i)
cout << v[i];

因为双向迭代器不支持用 "<" 进行比较。以下代码也不合法:

1
2
for (int i = 0; i < v.size(); ++i)
cout << v[i];

因为 list 不支持随机访问迭代器的容器,也不支持用下标随机访问其元素。

在 C++ 中,数组也是容器。数组的迭代器就是指针,而且是随机访问迭代器。例如,对于数组 int a[10]int * 类型的指针就是其迭代器。则 a、a + 1、a + 2 都是 a 的迭代器。

迭代器的辅助函数

STL 中有用于操作迭代器的三个函数模板,它们是:

  • advance(p, n): 使迭代器 p 向前或后移动 n 个元素

  • distance(p, q): 计算两个迭代器之间的距离,即迭代器 p 经过多少次 ++ 操作后和迭代器 q 相等。如果调用时 p 已经指向 q 的后面,则这个函数会陷入死循环。

  • iter_swap(p, q): 用于交换两个迭代器 p、q 指向的值。

要使用上述模板,需要包含头文件 algorithm。下面的程序演示了这三个函数模板的用法。

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

int main()
{
int a[5] = {1, 2, 3, 4, 5};
list<int> lst(a, a+5);
list<int>::iterator p = lst.begin();
advance(p, 2); // p 向后移动两个元素,指向 3
cout << "1) " << *p << endl; // 输出 1)3
advance(p, -1); // p 向前移动一个元素,指向 2
cout << "2)" << *p << endl; // 输出 2)2
list<int>::iterator q = lst.end();
q--; // q 指向 5
cout << "3)" << distance(p, q) << endl; // 输出 3)3
iter_swap(p, q); //交换 2 和 5
cout << "4)";
for (p = lst.begin(); p != lst.end(); ++p)
cout << *p << " ";
return 0;
}

输出:

1
2
3
4
1)3
2)2
3)3
4)1 5 3 4 2

函数、类、类的成员函数作为类模板的友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Func1(){}
class A{};
class B
{
public:
void Func(){}
};
template<class T>
class Tmpl
{
friend void Func1();
friend class A;
friend void B::Func();
};

int main()
{
Tmpl<int> i;
Tmpl<double> f;
return 0;
}

类模板实例化时,除了类型参数被替换外,其他所有内容都原样保留,因此任何从 Tmpl 实例化得到的类都包含上面三条友元声明,因而也都会把 Func1、类A 和 B::Func 当作友元。

函数模板作为类模板的友元

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
#include<iostream>
#include<string>
using namespace std;
template<class T1, class T2>
class Pair
{
private:
T1 key; // 关键字
T2 value; // 值
public:
Pair(T1 k, T2 v): key(k), value(v) {};
bool operator<(const Pair<T1, T2> & p) const;
template<class T3, class T4>
friend ostream & operator<<(ostream & o, const Pair<T3, T4> &p);
};
template<class T1, class T2>
bool Pair<T1, T2>::operator<(const Pair<T1, T2> &p) const
{
return key < p.key;
}
template<class T1, class T2>
ostream & operator<<(ostream &o, const Pair<T1, T2> &p)
{
o << "(" << p.key << "," << p.value << ")";
return o;
}
int main()
{
Pair<string, int> student("Tom", 29);
Pair<int, double> obj(12, 3.14);
cout << student << " " << obj;
return 0;
}

程序的输出结果是:

1
(Tom, 29)(12, 3.14)

第 13、14 行将函数模板 operator<< 声明为类模板 Pair 的友元。

编译本程序时,编译器自动生成了两个 operator<< 函数,它们的原型分别是:

1
2
ostream& operator<<(ostream &o, const Pair<string, int> &p);
ostream& operator<<(ostream &o, const Pair<int, double> &p);

前者是 Pair<string, int> 类的友元,但不是 Pair<int, double> 类的友元。后者相反。

函数模板作为类的友元

实际上,类也可以将函数模板声明为友元。

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;
class A
{
int v;
public:
A(int n): v(n) {}
template<class T>
friend void Print(const T & p);
};
template<class T>
void Print(const T &p)
{
cout << p.v;
}
int main()
{
A a(4);
Print(a);
return 0;
}

程序的输出结果是:

1
4

编译器编译到第 19 行 Print(a); 时,就从 Print 模板实例化出一个 Print 函数,原型如下:

1
void Print(const A & p);

这个函数本来不能访问 p 的私有成员。但是编译器发现,如果将类 A 的友元声明中的 T 换成 A,就能起到将该 Print 函数声明为友元的作用,因此编译器就认为该 Print 函数是类 A 的友元。

类模板作为类模板的友元

一个类模板还可以将另一个类模板声明为友元。

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
#include<iostream>
using namespace std;
template<class T>
class A
{
public:
void Func(const T & p)
{
cout << p.v;
}
};
template<class T>
class B
{
private:
T v;
public:
B(T n): v(n) {}
template<class T2>
friend class A; // 把类模板 A 声明为友元,这样 A 里面的成员函数都是 B 的友元函数
};
int main()
{
B<int> b(5);
A< B<int> > a; // 用 B<int> 替换 A 模板中的 T
a.Func(b);
return 0;
}

程序输出结果:

1
5

在本程序中,A< B > 类称为 B 类的友元。

类模板中可以定义静态成员,从该类模板实例化得到的所有类都包含同样的静态成员。

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;
template<class T>
class A
{
private:
static int count;
public:
A() { count++; }
~A() { count--; }
A(A&) { count++; }
static void PrintCount() { cout << count << endl; }
};
template<> int A<int>::count = 0;
template<> int A<double>::count = 0;
int main()
{
A<int> ia;
A<double> da;
ia.PrintCount();
da.PrintCount();
return 0;
}

输出:

1
2
1
1

对静态成员在类外部加以声明是必需的。

A 和 A 是两个不同的类。虽然它们都有静态成员变量 count,但是显然,A 的对象 ia 和 A 的对象 da 不会共享一份 count。