STL的六大组件中最主要的是容器和算法这两个,一个泛化数据的存储,一个泛化数据的操作。前面两篇文章我们简单的介绍了STL中的容器,这篇文章将会介绍STL算法以及粘合容器和算法的迭代器。STL是基于模板实现,容器基于模板类,而算法基于模板函数。在具体介绍算法和迭代器之前,我们先简单的回顾一下模板函数的语法。
模板函数
模板函数的语法其实很简单,只要把正常的函数的参数类型或者返回值类型都参数化就可以了。比如选择两个数中的最大值,我们可以使用std::max
:
template< class T >
const T& max( const T& a, const T& b );
这个模板函数中两个参数的类型和返回值的类型都参数化了。
自动类型推导
模板函数有一个非常重要的特性是——它支持类型的自动推导。我们实例化一个模板类的时候需要手动指定模板参数类型:
std::vector<int> iv;
但是我们实例化一个模板函数却通常不需要,因为参数可以自动推导【1】:
std::max(1, 999);
当然有些情况下自动推导会失败(下面这个例子中,两个实参的类型不一样,自动推导有歧义),这个时候我们可以显式指定模板函数的参数类型:
std::max<double>(1, 2.0);
简化模板类的创建
模板函数参数的自动推导在使用上非常方便,这一点被广泛的用于模板类的工厂方法的实现(广义上任何用于创建类的实例的方法都可以称为工厂方法)。比如我们要构造一个std::pair
,有两种方式。
第一,显式指定模板类参数:
std::pair<int, int> point(1, 2);
第二,使用模板函数自动推导:
std::make_pair(1, 2);
很显然后面这种方式用起来会比前面的方式舒服一些。标准库中存在大量的这一类型的工厂方法,比如:std::make_tuple
,std::make_excpetion_ptr
,std::make_shared<T>
等等。
数据和操作
容器和算法的关系,实际上对应着数据和操作的关系。 计算机领域有一个非常著名的公式:
程序 = 数据结构 + 算法
换句话说,我们可以认为【2】:
程序 = 数据 + 操作
在STL中,容器抽象了数据的存储,算法抽象了数据的操作。当然这个说法其实并不准确,因为数据的操作还有一部分(比如插入删除等)直接放到了容器的内部作为成员函数而存在。操作应该实现为成员函数还是算法,主要取决于这个操作是否和数据存储相关(还有可能和效率相关)。理论上说和存储方式无关的操作都可以实现为算法【3】。
算法和数据类型、数据存储方式无关
下面这段算法的定义来自《算法导论》
An algorithm is a sequence of computational steps thats transform the input into the output.
翻译成中文是说
算法是把输入变成输出的一系列计算步骤
这段定义有几个隐含的点值得讨论:
1. 算法和输入数据的类型无关
当我们描述快速排序算法的时候,我们其实在描述排序的步骤,至于数据类型是int
还是double
和算法本身其实没有关系。这个概念在C语言中比较不好表达,通常需要通过void*
加上函数指针来实现。比如C语言中的排序算法定义如下:
void qsort( void *ptr, size_t count, size_t size,
int (*comp)(const void *, const void *) );
C++中因为模板函数的存在,参数类型可以被参数化,所以这个问题解决起来就方便很多,C++中的std::sort
定义如下:
template< class RandomIt >
void sort( RandomIt first, RandomIt last );
现在,std::sort
只需要实现算法逻辑,不需要考虑数据类型。 这个定义显然比C的定义要简单很多也清晰很多
2. 算法和输入的数据是如何存储无关【4】
我们描述一个算法,说的是数据的操作步骤,至于如何完成这些操作,实际上并没有规定。在C语言中,操作如何完成和数据如何存储有很大的关系,比如我们要实现指针的递增操作,我们的数据必须是连续存储的。但是在C++中,通过操作符的重载可以让数据的操作和数据如何存储的解耦开来。比如在C语言中线性查找算法的一个典型例子:查找一个字符串中的指定字符的函数strchr
定义如下:
char *strchr( const char *str, int ch );
而在C++中,查找线性查找算法std::find
定义如下:
template< class InputIt, class T >
InputIt find( InputIt first, InputIt last, const T& value );
strchar
要求数据必须是连续存储,而且必须以\0
结尾,而std::find
却没有这个要求,你可以用它来查找链表中的数据。解耦数据的存储和算法实现的关键在于输入数据的泛化,而这个泛化的关键在于迭代器组成的区间。
区间
前面提到算法是把输入变成输出的计算步骤,所以要写一个范型算法,首先要解决的问题是如何表达输入。一个范型算法的输入通常是由两个迭代器组成的左闭右开的区间表示的,C语言中则通常是一个首地址+长度这种方式。使用半开闭区间的方式有下面这些好处:
- 空集的概念很好表示,首尾相同即可 [beg, beg)
- 比较容易返回错误值,数据查找,如果没有找到,我们不需要返回一个特殊值,直接返回
end
,就可以了,因为end
不在区间内部,返回它很好的表达没有找到这个概念。 - 比较容易表达迭代终止条件这个概念,
beg == end
即可表示迭代终止,这对于迭代器来说是很重要的,因为它不需要支持算数操作