模板和泛型编程

在这里插入图片描述

函数模板

模板函数的定义和使用

1.模板函数的定义
函数模板看起来非常像是一个函数,但特别的语法使它只是看起来像而已,并不是一个真正的函数。当然,一旦函数模板被实例化,它就能履行真正的函数的功能。

函数模板的形式化描述如下:

template < typename T,[const类型 常量表达式,] >
返回值类型 函数名(参数列表)
{ 
	//函数体
}

可以使用关键字class来代替typename关键字,二者没有区别。但建议读者选择typename,因为class可能会误导程序员,使他们片面地认为,类型参数只能是类类型
T:类型参数,可以是任意类型
整个声明:模板参数
[]内内容:非类型参数,可以省略
函数体:函数模板

比较大小函数模板可以写成这样:

template <typename T>
bool Greater(const T& a, const T& b)
{
	return a > b;
}

为模板指定给一个整型常量min作为非类型参数:

template <typename T, const int min>
bool Greater(const T& a, const T& b)
{
	return a > b && b > min;
}

模板的非类型参数的类型不能是浮点型、类类型或void类型。它一般是整数类型、枚举类型。(思考:why?)

template <typename T,const int ci>
bool Greater(T  a, T b)
{
return a>b && b>ci;
}
int _tmain(int argc,_TCHAR* argv[])
{
Greater<int ,0>(2,1);
return 0;
}

2.模板函数的使用
如果要它真正地工作起来,必须将其变成真正的函数。这个变化过程成为“实例化(instantiation)”。由函数模板实例化出的函数称为“实例函数(instantiated function)”,或者更直接地称为“模板函数
需要说明的一点是,在C++中,函数模板的实例化只能是隐式的,正如在例中看到的那样,通过参数的类型来产生指定版本的模板函数。(思考:为什么只能隐式?)

重载模板函数和非模板函数

如果在代码中写出如下模板应用代码:
Greater(1.0, 2);
那么结果会怎样呢?
也许大家会认为隐式类型转换规则会起作用,int数据自动转换成double,然后再套用模板
bool Greater(const double& a, const double& b)
来完成工作
在有的情况下,隐式转换规则会工作的很好,但在此例中却行不通,编译器会报出类似于“**类型T不明确”(VC9)**或“no matching function for call to 'Greater(double, int)”(gcc)的错误。
消除错误的做法其实并不难。既然比较的两个数分属于不同的类型,那么就可以重载一个Greater模板,它可以接受两个类型参数:

template <typename T, typename U>
bool Greater(const T& a, const U& b)
{
	return a > b;
}	

在C++中,编译器在尝试调用函数模板还是同名的非模板函数时遵循下述约定:

  1. 寻找一个参数完全匹配的非模板函数,如果找到了,就调用它
  2. 否则,寻找一个函数模板,将其实例化产生一个匹配的模板函数,如果找到了,就调用它
  3. 否则,试一试低一级的对函数的重载方法,如通过类型转换可产生参数匹配等,如果找到了,就调用它
  4. 如果(1)(2)(3)均未找到匹配的函数,那么这个调用是一个错误

eg:

#include<iostream>

using namespace std;

template <typename T>
void test(T a,T b) {
	cout << a << "," << b << endl;
}

int main() {
	test<int>(1.01,2.01);
}

在这里插入图片描述

#include<iostream>

using namespace std;

template <typename T>
void test(T a,T b) {
	cout << a << "," << b << endl;
}

int main() {
	test(1.01,2.01);
}

在这里插入图片描述

#include<iostream>

using namespace std;

template < typename T >

int compare(const T& a, const T& b)
{
	cout << "调用模板函数" << endl;
	if (a > b)
		return 1;
	return 0;
}

int compare(const int& a, const int& b)
{	
	cout << "调用非模板函数" << endl;
	cout << a << endl;
	cout << b << endl;
	return 0;
}

int main() {
	cout << compare(2, 3) << endl;
	cout << compare<>(2, 3) << endl;
	cout << compare(2.0, 3.0) << endl;
	cout << compare(2.0, 3) << endl;
	cout << compare('a', 'b') << endl;
	cout << compare<double>(4, 5) << endl;
	cout << compare('a', 42.7) << endl;
	return 0;
}

在这里插入图片描述
注:注释掉非模板函数后,上述第四个和最后一个语句会报错。

3.函数模板的特化
函数模板不总是对所有类型都是最合适的,有时需要**模板特化(template specialization)**编写指定类型的模板

typedef char * cstring;
template <>
bool Greater<cstring>(const cstring& s1, const cstring& s2)
{
    out << "C-style string comparasion: ";
    return strcmp(s1, s2) > 0 ? true : false;
}

特化的函数模板的特点:
a.特化的模板仍然是个模板。
b.特化的模板没有模板参数(template<>尖括号中无参数)。
c.特定的类型跟在函数名后面,在类型信息足够明确的情况下,这对尖括号可省略。
d.参数的类型修饰要与普通模板一样。

本例中strcmp(s1,s2)为处理特定类型的方法。

类模板

类模板的定义和使用

1.类模板的定义和使用

1.类模板的定义
类模板的形式化定义如下:

template <typename T, [const 类型 常量表达式,]>
class 类名
{ 
	//成员定义;
};

模板参数与函数模板的规格一样

例如:

template <typename T>
class array
{
private:
    T* head;
	//other members
 
public:
    array() { ... }
    //其它成员
};

成员函数在声明时就给出定义。对于类模板来说,这是一种常用的做法。

如果成员函数的声明在类内,而定义在类外,那么必须用类似下列的语法:

template <typename T>
array<T>::array() { ... }

与函数模板一样,可以为类模板指定非类型参数。如果我们要限制链表存储的形体数目,那么可以用一个整型常量来完成:

template <typename T, const int maxLen> 
class array {}
array<double,10> a;

2).类模板的使用
类模板由于未绑定类型,因此不是一个真正意义上的类。要是类模板真正地工作,需要对其进行实例化,其语法如下:
模板名<参数类表> 对象名;
例如,我们要使列表存储int类型的数据,可以用如下语句:

array<int> li;

这里,编译器做了两件关键的事情:

  1. 按照array模板的布局,用类型int去实例化出一个真正的类;
  2. 为真类实例化出一个对象li。当然,一旦对象被定义,那么其构造函数就会被调用。
    可以用其它任意类型来实例化List模板:
    array ad;
    array ar;
    array<complex *> acp;

3)类模板的参数可以是默认的
类模板的各种参数都可以是默认的。例如:

template <typename T = int, const int maxLen = 1024> 
class array {}

与函数的默认参数一样,类模板的默认参数只能放在参数列表的最右边。或者说,一旦开始定义默认参数,那么其后的参数都必须是可默认的。
一旦定义了带默认参数的类模板,那么在该模板实例化时,可以不必给出参数,包括类型和非类型参数。例如:

array<> a1;		//T = int, maxLen = 1024
array<double> a2;	//T = double, maxLen = 1024
array<long, 100> a3;	//T = long, maxLen = 100
array<array<char>> a4; //T = array<char>, maxLen=1024

2.类模板的成员
一个类模板非常像是一个真正的类,它具有普通类的所有特征,能够拥有任意类型的成员。数据成员因其简单性就不做讨论,这里简单讨论一下类模板的成员函数、成员类的情况。
成员函数

template <typename T>
class A
{
public:
	void f(T& x) { return x; } 
	int g() { return 0; }
};

成员类:一个类模板中可能包含一个或多个内部类的定义

template <typename T>
class A
{
	public:
	class B {};
};

成员模板。如果类模板中的内部类声明中,有些被冠以template关键字,并且它们的类型参数不依赖于外部模板的类型参数,那么它们将成为类模板中的类模板,这称为“成员模板”

template <typename T>
class A
{
public:
	class B
	{
	public:
		template <typename U>
		void f(U& x) {}
	};
};

3.类模板的特化
与函数模板一样,类模板可以被指定类型特化。考虑设计的array类模板,我们可以用float类型去特化它:

template <>
class array<float>
{
	//member declarations
};
array<long *> lpr;  //普通模板
array<int> li; 		//普通模板
array<float> lf;    //特化的模板

4.类模板中的友元
与普通类一样,可以在类模板中声明友元。有三种类型的友元声明:
• 普通友元
• 普通模板
• 特化的模板

class Printer {};
void print() {}
 
template <typename T> class array;
template <typename T> void Tprint(const array<T>&) {}
template <typename T> class TPrinter {};
 
template <typename T>
class List
{
	//第一种:普通友元
	friend class Printer; 
	friend void print();
	//第二种:普通模板友元
	template <typename P> friend void Tprint(const array<P>&); 
	template <typename P> friend class TPrinter; 
	//第三种:特化的模板友元
	friend void Tprint<long *>(const array<long *>&);
	friend class TPrinter<long *>;
};

5.类模板的继承和派生
类模板可以成为其它类的基类

class chArray : public array<char>
{
public:
    chArray(size_t l, char *list) : array<char>(l, list) {}
};

提问:chArray还是模板吗?
可以将chArray定义成一个类模板,并且使基类的模板参数依赖于派生类的模板参数:

template <typename U>
class chArray : public array<U>
{
public:
    chArray(size_t l, U *list) : array<U>(l, list) {}
};

容器类和迭代器

容器类的迭代操作

array类(模板)是一种容器(container),用于容纳特定的对象。
一个容器拥有一些典型的操作:
• 插入元素
• 删除指定元素
• 查找指定元素
• 遍历
其实,删除和查找也可以归类为遍历操作,因为在这些操作工作时,它们都要从头到尾顺序地访问容器中的元素。

迭代操作举例:
观察类似于遍历这样的操作,可以发现这些操作的规律:使用一个循环,将每个元素依次从容器中提取出来,然后处理。这样的操作称为“迭代(iteration)”。
以下代码示意了一种典型的针对于数组的迭代操作:
int a[10], *p;
for (p = &a[0]; p != &a[10]; ++p) cout << *p << endl;
循环结束,所有元素处理完毕。但没有真正访问a[10](哨兵)单元,毕竟这个单元是不存在的。

一般的情况下,一个容器的创建者写出了容器的代码,而遍历容器的迭代操作却是客户程序员在自己的代码中进行的,以便达到灵活控制的目的。
然而,这样做产生了一个很大的问题:要使迭代操作能够顺利进行,客户程序员就必须知道容器的内部结构,否则就无法用指针获取首元素、尾元素和当前元素的地址。
从以上迭代操作可以总结出如下特点:
• 迭代用到了指针
• 迭代有起点。一般是用指针指向容器的首元素;
• 迭代有终点。一般是为迭代设置一个终点标记(本例中是a[10]的地址),并在迭代中测试指针是否与终点标记相等:如果不等,迭代继续,否则结束。这个终点标记我们称为“哨兵(sentinel)”。
• 指针推进。一般用**++**运算符推进指针;
• 元素提取。一般用作用在指针上的 ‘*’ 运算符完成

问题:
1.如何满足数据封装原则
我们可以将遍历操作封装在容器中,正如array封装了traverse()操作那样。然后客户程序员可以根据容器的要求编写访问函数来完成不同目的的遍历
2.如何做到类型无关
我们可以从容器中分离迭代操作,这需要我们为容器类绑定一个新的类:迭代器(iterator)。迭代器的功能可以简单描述为:以一种类型无关的方式从头到尾遍历容器,提取容器的元素,将其交给客户程序员去做适当的处理。显然,要做到类型无关,迭代器必须是一个类模板

迭代器

1.迭代器的成员函数
为迭代器设计两个关键成员,以及重载四个关键运算符:
begin():返回一个迭代器,该迭代器指向容器的首元素
end():返回一个迭代器,该迭代器指向容器尾部元素的“下一个”位置(绝大多数情况下该位置是个NULL)。这种“空”迭代器称为“哨兵(sentinel)”,主要用于标识容器的尾部;
=复制迭代器;
!=:比较两个迭代器是否不等
• ++:使迭代器指向当前元素的下一个元素;
• *:返回容器中当前的元素
显然,迭代器应该成为绑定它的容器类的友元,因为迭代器要访问该容器类的非公有成员

在这里插入图片描述
2.伪迭代器
为array类模板设计一个迭代器。这个类模板的特点是:内部采用与数组完全相同的顺序存储结构,而对于这类存储结构的遍历用指针就完全可以了,并且,指针的复制、比较、推进和提取都是编译器能够直接处理的
用typedef为指针类型取一个别名,即将T*取别名为iterator;

#include<iostream>

using namespace std;

template<typename T>
class array_myself
{
public:
	array_myself() : head(nullptr), len(0) {}
	array_myself(size_t l, T* list) : head(new T[l]), len(l) {
		for (size_t i = 0; i < l; i++)
			head[i] = list[i];
	}
	~array_myself() { delete[] head; }

	typedef T* iterator;

	iterator begin() { return head; }
	iterator end() { return head + len; }//尾元素的后一个元素的地址

private:
	T* head;
	size_t len;
};

int main() {
	int d1[] = { 1,2,3,4,5,6,7 };
	array_myself<int> a(sizeof(d1) / sizeof(int), d1);

	for (array_myself<int>::iterator itr = a.begin(); itr != a.end(); ++itr)
		cout << *itr << endl;

	return 0;
}

输出:1 2 3 4 5 6 7

3.迭代器设计
Iterator模拟了指针的行为。因此,它是一个封装了一个指针的类,该指针能够指向包围容器的内部存储机构,例如数组或是链表。
在这里插入图片描述

原因
内部结构不同:数组、链表
迭代器的结构和功能组成
• 内部有一个指针,它与容器内部链式存储结构的头指针类型相同
• 能用构造函数产生一个首迭代器,其内部指针与容器的头指针有相同值,即指向相同的节点
• 能用构造函数产生一个哨兵迭代器,其内部指针为nullptr,因为容器链式存储的结尾就是一个nullptr
• 重载了=、!=、++、*运算符,分别完成复制、比较、推进和提取的功能

4.逆向迭代器

5.更多迭代器操作
后缀++
operator+()
operator==()

6.基于范围的for语句

for(auto val:list) std::cout<<val<<‘ ’;
等价于:
for(int val,linkedList<int>::iterator itr = list.begin();
itr!=list.end();++itr)
{val=*itr;str::cout<<val<<‘ ‘;}

泛型算法

类型无关的函数一般被称为“泛型算法(generic algorithm)”。
1.泛型算法函数的设计

  1. 仅使用迭代器的泛型算法
    考虑这样一种操作:统计linkedList列表中元素的数目。
    提问:如何设计?提示:统计是一种遍历
    为了实现类型无关,我们可以这么设计计数函数:它使用迭代器作为参数,用以标识统计的范围;其返回值是size_t类型,表示统计结果。
template <typename T>
size_t count(T itrBegin, T itrEnd)
{
	size_t cnt = 0;
	for (T itr = itrBegin; itr != itrEnd; ++itr) ++cnt;
	return cnt;
}
linkedList<int> list;
cout << count(list.begin(), list.end()) << endl;

从上面的代码可以看到,算法count是充分独立于类型的。这是一件好事,但在某些时候,这却是一个大问题:算法对类型一无所知,因此它可能无法完成必须与类型相关的操作。在这种情况下,算法必须有所改变。

  1. 使用附加类型的泛型算法
    考虑这样的问题:我们用设计的linkedList模板来存储整数,然后求容器中所有存数的累加和。算法取名为accumulate()。
    提问:如何设计?提示:仿照count算法。
    大家是不是遇到了问题:accumulate中的累加器变量应该是什么类型的?算法函数的返回值又该是什么类型呢?
template <typename T, typename U>
U accumulate (T itrBegin, T itrEnd, U init)
{
	U s = init;
	for (T itr = itrBegin; itr != itrEnd; ++itr) s += *itr;
	return s;
}

2.带谓词的泛型函数
考虑这样一个问题:统计列表中数值大于3的个数。显然,我们可以利用前面设计的count算法,但它必须做出改变以适应条件筛选的需求。
很明显,count算法应该带有第三个参数,用以过滤满足条件的元素。多数情况下,这个过滤功能应该用一个函数来完成,该函数返回bool类型以指示过滤是否成功。这样的函数术语上称为“谓词(predication)”。

template <typename T, typename U>
size_t count_if(T itrBegin, T itrEnd, U pred)
{
	size_t cnt = 0;
	for (T itr = itrBegin; itr != itrEnd; ++itr) 
		if (pred(*itr)) ++cnt; //!
	return cnt;
}
bool g3(int v){return v>3;}
std::cout<<count_if(list.begin(),list.end(),g3)<<std::endl;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值