[知识点] 1.5.4 模板与 STL
总目录 > 1 语言基础 > 1.5 C++ 进阶 > 1.5.4 模板与 STL
前言
模板是之前几乎没接触过的东西,在 C++ 中使用得其实也挺多的;STL 就不多说了,太好用了。
子目录列表
1、模板的概念
2、函数模板
3、类模板
4、STL
1.5.4 模板与 STL
1、模板的概念
模板(Template),是 C++ 实现代码重用机制的重要工具,是泛型技术(即与数据类型无关的通用程序设计技术)的基础。它把算法设计从具体数据类型中分离出来,能够设计出独立于数据类型的通用模板程序,分为函数模板和类模板两类。
有些时候,我们设计某些功能需要针对诸多不同的数据类型,但除此之外代码完全一致,为此不得不编写多个具体到不同数据类型的程序,比如一个简单的求最大值函数:
int max(int a, int b) { return a > b ? a : b; } double max(double a, double b) { return a > b ? a : b; } char max(char a, char b) { return a > b ? a : b; }
在 1.1 C 语言基础 中的预处理器部分,我们提到的宏定义 #define 可以解决这个问题,即:
#define max(a, b) (a < b ? a : b)
但宏定义一直存在的最大问题就是会避开类型检查机制,并且因为可操作性太强,以至于非常容易出现难以意料的错误。而模板的出现,让一切变得简单而规范。
模板就像工艺制造中的模具一样,功能已经被固定下来,而数据类型就像填充的材质一样,是可以选择的;最后生产出来的产品,便是函数或者类。以上述求最大值函数为例,其函数模板为:
template <typename T> T min(T a, T b) { return a < b ? a : b; }
关键字 template 表示定义模板(这个代码编辑器竟然不识别 template),< > 为参数表,typename 表示 T 可以为任何数据类型,称为类型参数,可以用 class 代替,两者完全等价。
2、函数模板
前面已经用函数模板举了个例子,其一般形式为:
template <模板参数表>
返回类型 函数名(参数表) {
...
}
注意,template 语句和函数模板的定义之间不能有任何其他语句。
当编译器遇到 template 和跟随其后的函数定义时,它只是简单地知道这个函数模板以后可能会用到,而并不会据此有任何操作。当编译器遇到对其的调用时,才会根据调用语句中实参的具体类型生成代码,这一过程叫做模板的实例化,生成的函数称作模板函数。
注意,使用函数模板时,编译器并不会进行任何参数类型的隐式转换,如果出现不匹配的情况,将会直接报错。诸如上述求最大值模板,如果调用 max(2, 3.2),系统会将 2 判定为 int 类型,而 3.2 为 double 类型,但模板并未允许两者为不同数据类型,故出现错误。解决这个问题,可以:
① 显式转换类型;
② 显式指定函数模板实例化的类型参数,比如:min<double>(2, 3.2);
③ 指定多个模板参数,比如:
template <typename T1, typename T2> T1 max(T1 a, T2 b) { return a > b ? a : b; }
(当然这样写其实依旧不够准确,但确实不会导致编译错误,更好的办法是将返回类型替换成 C++ 11 标准中的 auto,关于 auto 请参见 施工中)
模板参数还可以为确定的数据类型,称为非类型模板参数,和普通函数的形参定义方式和作用是类似的,举个例子:
template <typename T, int n> void sort(T a[n]) { for (int i = 1; i < n; i++) for (int j = i + 1; j <= n; j++) if (a[i] > a[j]) swap(a[i], a[j]); } int a[] = {1, 3, 4, 2, 9, 5, 8, 10, 6, 7}; sort <int, 10> (a);
注意,非类型参数只能被传递常数,而不能传递变量。
3、类模板
类模板与类的关系形如函数模板与函数的关系,其一般形式为:
template <模板参数表> class 类名 { ... };
关于类模板与模板类的关系,以及模板参数不再赘述,直接以一个自定义的栈类模板举例:
template <typename T, int n> class Stack { T a[n]; int top; public: Stack() { T = 0; } void push(T o); T pop() { if (top <= 0) { cout << "error" << endl; return 0; } top--; return a[top]; } bool empty() { return top == 0; } }; template <typename T, int n> void Stack <T, n> :: push(T o) { if (top == n) { cout << "error" << endl; return 0;
} a[top++] = o; } Stack <int, 10> s;
其中,pop() 函数使用的是类内定义,无异于普通类;push() 函数使用的是类外定义,则在定义时需要再次声明模板,并且加上完整的类型限定符。
由类模板实例化生成针对具体数据类型的模板类,再由模板类定义模板对象。
4、STL
① STL 简介
上面已经介绍了如何自定义模板,而 C++ 内置了一套非常完善的标准模板库,将诸多组件实现了标准化,极大便捷了开发者对程序的编写。
标准模板库(Standard Template Library, STL),包含:容器(Container),迭代器(Iterator),算法(Algorithm),空间配置器(Allocator),配接器(Adapter),仿函数(Functor),前三者为核心内容。它们被保存在如下 13 个头文件:
<algorithm>, <deque>, <functional>, <iterator>, <vector>, <list>, <map>, <memory.h>, <numeric>, <queue>, <set>, <stack>, <utility>
② STL 容器
容器(Container)是用来存储其他对象的对象。容器是容器类的实例,容器类是用类模板实现的,适用于各种数据类型,包含:顺序容器(序列式容器)、关联容器和容器适配器。
> 顺序容器
顺序容器是将同类型对象的有限集按顺序组织在一起的容器,用来表示线性数据结构,包括:向量(Vector),列表(List)和双端队列(Deque)。
还有一种特殊的顺序容器:值对(pair)。
> 关联容器
关联容器是非线性容器,用来根据键(key)进行快速存储、检索数据的容器,可以存储值的集合或者键-值对(Key-Value),主要包括:集合(Set),多重集合(Multiset),映射(Map)和多重映射(Multimap)。
> 容器适配器
容器适配器本质上并不是容器,因为它没有迭代器,但依旧归类于容器;它由栈(Stack),队列(Queue)和优先队列(Priority_queue)组成。
③ STL 迭代器
迭代器(Iterator)是一个对象,常用来遍历容器,可以认为是基于模板的指针,可以指示容器中的元素位置,遍历各个元素。迭代器本身提供了适用于多种容器类型的通用操作,开发者无需知道实际类型。
迭代器是 STL 的核心,用于定义哪些算法可以在容器中使用,是容器和算法之间的桥梁。如果某个容器需要使用迭代器,则需要定义它,一般形式为:
容器类型 <容器参数表> :: iterator 迭代器名;
举个例子:
list <int> l; list <int> :: iterator iter;
迭代器提供的主要操作如下:
施工中
④ STL 算法
算法(Algorithm)是用模板技术实现的适用于各个容器的通用程序,一般通过迭代器间接操作容器元素,并返回迭代器作为算法运算的结果。STL 共计提供了超过 100 个实现算法的模板函数,基本都包含在 <algorithm> 库中,其中许多算法不仅适用于容器类型,还适用于普通的 C++ 数组或者自定义容器。