文章目录
1 概念
1.1 泛型编程
函数对一个未知类型的参数的使用方式会对这个参数的类型产生约束。
例如:如果要计算x+y,为了让运算结果存在,对于x和y就必须具有这样的类型:对于x+y是可定义的。
模版函数:实现泛型函数的语言特征。模版允许我们为一个行为特性相似的函数族(或类型族)编写一个单独的定义,将族中各个函数(或类型)的差别归因于它们的模版参数的类型不同。
隐藏在模版之后的是不同类型的对象仍然可以享有共同的行为特性。模版参数允许我们按照共同的行为特性编写程序,即使我们并不知道模版参数相对应的特定的类型。
对于泛型函数,系统环境无需考虑对那些在运行期间会起变化的对象做什么处理,这个问题会在编译时期得到处理。
1.2 模版实例化
所有系统环境都以自己特定的方式处理实例化。我们无法准确说明编译器何时处理实例化的。
- 1 但对于沿用传统的编辑-编译-链接模式的系统环境来说实例化动作经常不是在编译期间而是在链接期间发生的。只有模版被实例化时,系统环境才能证实,模版代码是能被用于制定的类型。在链接期间,我们就能发现编译期间可能发生的错误。
- 2 为了对模版进行实例化,当前大多数系统环境都要求这个模版的定义(不仅是声明)必须是系统环境可以访问的。通常这意味着模版的源文件和头文件都必须是可访问的。源文件的定位方式由具体的系统环境决定,一般许多系统环境都要求模版的头文件直接或者通过#include指令而将源文件包含进去。
1.3 注意点
在模版里使用诸如vector<T>这样依赖于模版参数的类型,并且希望使用这个类型的一个诸如size_type这样的成员(本身也是一个类型),我们都必须要在整个名称前面加上typename,从而让系统环境知道应该将这个名称当作一个类型来处理。(简单说就是,声明中使用了由模版参数定义的类型,就必须使用关键字typename限定这个声明)
例如:
template<class T>
T func(vector<T> v){
//注意点:
typedef typename vector<T>::size_type vec_sz;
vec_sz size = v.size();
return *(v.begin());
}
1.4 使用格式
template<class type-parameter[, class type-parameter]...>
ret-type function-name(parameter-list)
每个type-parameter(参数类型)是一个名称,可以在函数定义内任何一个需要类型的位置上使用这个名称。那个这样的名称都应出现在函数的parameter-list(参数列表)中,来为一个或多个参数的类型命名。
如果这些类型不全部出现在参数列表中,那么调用程序就必须用具体的类型(我们无法推断什么类型)来限定function-name(函数名)。
例如:
template<class T> T zero(){ return 0;}
在调用这个函数时,必须明确提供返回类型。
double x = zero<double>();
2 迭代器种类
如果想理解模版是如何编写数据结构独立的程序,最简单的方法就是理解标准库函数的实现。
这些函数中在参数中都包含有迭代器来标示容器中的元素,从而对这些元素进行操作。
不同种类的迭代器提供了不同种类的操作。重点是理解不同算法对其使用的迭代器的具体要求以及不同种类的迭代器所分别支持的操作。
不管什么时候,如果两个迭代器支持同样的操作,它们就会给这个操作同样的名称。例如,所有迭代器都使用++使一个迭代器指向它的容器的下一个元素。
库定义了5种迭代器(iterator categories),其中每一种都对应一个特定的迭代器操作集合。这些迭代器种类划分每一个库容器提供的迭代器的类别。每种库算法都有关于迭代器参数期望的种类。因此,迭代器种类可以提供一种方法去理解那些容器可以使用那些算法。
2.1 输入迭代器(顺序只读访问)
条件:
- 应该支持++(前缀和后缀)
- ==
- !=
- 一元*运算符
- it->member和(*it).member之间的运算符是等价的
作用:只能被用于读一个序列的元素。
示例标准库函数find():
template<class In, class X>
In find(In begin, In end, const X& x){
while(begin != end && *begin != x){
++begin;
}
return begin;
}
实现方式二:
template<class In, class X>
In find(In begin, In end, const X& x){
if(begin == end || *begin == x){
return begin;
}
begin++;
return find(begin, end, x);
}
流迭代器(定义在<iterator>)中用于istream的迭代器就是输入迭代器。输入流迭代器一种名为istream_iterator的输入迭代器类型。
下面看这个例子:
vector<int> v;
//从标准输入中读整数值并将它们添加到v中
copy(istream_iterator<int>(cin), istream_iterator<int>(), back_inserter(v));
流迭代器是模版,定义一个流迭代器时,必须设法告诉它应该从流读入什么类型的数据或者应该写什么类型的数据到流。
copy的第一个参数构造了一个istream_iterator类型的迭代器,这个迭代器被链接到cin中,要求读入int类型的值。
第二个参数创建了一个默认(为空)istream_iterator<int>类型的迭代器,这个迭代器不会与任何文件连接在一起。istream_iterator有一个默认值,而这个默认值有一个性质,就是istream_iterator类型的迭代器一旦到达了文件末尾或者是错误状态中,那么它就会和这个默认值相等。
因此可以用这个默认值来为copy表示“超过末尾元素一个单位位置”的协定。
2.2 输出迭代器(顺序只写访问)
条件(it:迭代器):
- 不能在没有对it进行递增的情况下,对it进行多次赋值;
- 不能在对*it的两个赋值运算符之间执行超过一次的++it操作。
以及还应该支持
- *it(对于写);
- ++it和it++(但不用支持–it或it–);
- it->member(作为(*it).member的一个代替名)
作用:只用于被写一个序列的元素。
示例标准库函数copy():
template<class In, class Out>
Out copy(In begin, In end, Out dest){
while(begin != end)
*dest++ = *begin++;
return dest;
}
back_inserter ( c )就是一个输出迭代器,其中c是支持push_back的容器。
流迭代器(定义在<iterator>)中用于osream的迭代器就是输出迭代器。输出流迭代器一种名为ostream_iterator的输入迭代器类型。
下面看这个例子:
vector<int> v;
//输出v的元素,元素之间用一个空格隔开
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
将整个向量复制到标准输出。第三个参数构造了一个迭代器,此迭代器被连接到cout,它要求写int类型的值。
用于构造ostream_iterator<int>类型对象的第二个参数指定了一个值,这个值会被写到每个元素之后。通常这个值是一个字符串文字。如果我们不提供一个这样的值,那么istream_iterator类型的迭代器在写数值时就不会带任何的分隔。
如:
//两个元素之间没有分隔
copy(v.begin(), v.end(), ostream_iterator<int>(cout));
2.3 正向迭代器(顺序读-写访问)
条件:应该支持
- *it(对于读和写);
- ++it和it++(但不用支持–it或it–);
- it->member(作为(*it).member的一个代替名)
顺序读或者写(一旦处理了一个元素就绝不会在访问它)。(所有标准库容器都支持双向迭代器)
示例标准库函数repalce():
template<class For, class X>
void replace(For beg, For end, const X& x, const X& y){
while(beg != end){
if(*beg == x){
*beg = y;
}
++beg;
}
}
2.4 双向迭代器(可逆访问)
条件:应该支持
- *it(对于读和写);
- ++it和it++,同时支持–it或it–;
- it->member(作为(*it).member的一个代替名。
逆向顺序访问一个容器的元素。(所有标准库容器都支持双向迭代器)
示例标准库函数reverse():
template<class Bi>
void reverse(Bi begin, Bi end){
while(begin != end){
--end;
if(begin != end){
swap(*begin++, *end);
}
}
}
2.5 随机访问迭代器(随机访问)
条件(p和q是迭代器,n是一个整数):
- p + n, p - n , n + p
- p - q
- p[n](与*(p+q)等价)
- p < q, p > q, p <= q, p >= q
还应该支持
- *it(对于读和写);
- ++it和it++,同时支持–it或it–;
- it->member(作为(*it).member的一个代替名。
两个迭代器(p - q)相减会产生一个整数,表示p 和q指向的元素在容器中的间距。由于p-q可能是负值,因此它是一个带符号的整数类型。该类型到底是整型(int)还是长整型(long)取决于系统环境。标准库中在<cstddef>中提供了ptrdiff_t来表示这样的类型。
作用:在容器中各种跳转的读或写。
示例标准库函数binary_search()【简化版:要求有随机访问迭代器】:
template<class Ran, class X>
bool binary_search(Ran begin, Ran end, const X& x){
while(begin < end){
//查找区间的中点
Ran mid = begin + (end - begin)/2;
//查看举荐哪一部分含有x,指望下查找这一部分
if(x < *mid) end = mid;
else if(*mid < x) begin = mid + 1;
//得到了带查找的值,即*mid == x,完成查找
else return true;
}
return false;
}
常见的还有sort函数。向量和字符串迭代器都是随机访问迭代器,而链表跌打器则不是这样的迭代器,它仅支持双向迭代器。
因为链表是为了快速插入和删除而被优化,无法快速定位到表中的任意元素,定位的唯一方法是按顺序查看每一个元素。不过list有自己的sort成员函数来进行排序,即list.sort()。
3 迭代器区间和越界值
在编写程序时,经常会看到使用end()来指向区间最后的元素的后面那个位置(即上界)。
为什么我们要用指示了紧位于区间最后一个元素后面的那个位置的迭代器,而不是用一个直接指向最后的元素的迭代器标记区间终点?
原因:
- 1 简化程序建设。
- 如果区间根本没有元素,我们将无法找到一个最后的元素以标记终点。
- 这样的话,我们将不得不用一个迭代器来指明一个空区间,而这个迭代器将会紧位于区间开头之前的那个位置。
- 如果采取这样的策略,就必须将空区间和其他所有的区间都不同的特例来处理(类似与链表是否加头指针),这样会降低程序的可靠性以及使得难以理解。
- 2 可以比较迭代器来判断容器是否有元素。
- 如果我们用一个迭代器(紧位于区间最后一个元素后面的那个位置) 标记区间终点,那就可以用相等或不相等去比较迭代器,从而判断区间是否为空。
- 只有在两个迭代器(begin(),end())相等的情况下,区间才会为空。如果不相等,开始迭代器(begin())指向了一个元素。
- 例如;常用的判断
while(begin != end){//begin和end都是迭代器,区间[begin,end)
++begin;
}
- 3 能够以一种自然的方式来表示“区间之外”。
- 许多库算法都利用了“”区间之外“的值。它们返回区间的第二个迭代器来指示失败,如前面的库函数find()函数。
- 如果没有这个值,我们就必须创造一个(如string::npos,一个无符号整型,其值为-1或无符号整型的最大值),这样又增加算法以及使用算法的复杂度。