(十二)C++学习 | 模板


1. 简介

C + + {\rm C}++ C中为了提高程序的可重用性,引入了两个方面的机制:前面介绍过的继承和范式程序设计,即模板。其中,模板主要分为函数模板类模板两种。本文将分别介绍这两种模板的定义与使用。


2. 函数模板

2.1 函数模板的定义

首先来看我们在程序设计中常用的交换两个元素的函数swap。如果我们需要交换元素的类型是整型,则函数原型是:

void swap(int& x, int& y) {
	int tmp = x;
	x = y;
	y = tmp;
}

如果我们需要交换元素的类型是双精度型,则函数原型是:

void swap(double& x, double& y) {
	double tmp = x;
	x = y;
	y = tmp;
}

如果又来一种新的类型,如字符型、浮点型等,我们需要对该函数再次重载。然而,在 C + + {\rm C}++ C中肯定不容忍存在这种存在大量冗余的写法。那么我们是否可以只写一个函数就能完成所有类型元素的交换呢?这就涉及到函数模板的使用。函数模板的结构如下:

template<class 类型参数1, class 类型参数2, ...>
返回值类型 模板名(形参表) {
	函数体
}

如上述交换函数的函数模板可写作:

template<class T>
void swap(T& x, T& y) {
	T tmp = x;
	x = y;
	y = tmp;
};

具体的使用方法如下:

int main() {
	int m = 1, n = 2;
	swap(m, n);	// 编译器自动生成void swap(int& , int& )函数
	double p = 1.2, q = 2.3;
	swap(p, q);	// 编译器自动生成void swap(double& , double& )函数
	return 0;
}

在函数调用时,编译器会自动根据传入实参的类型将模板定义中的T替换成变量的类型,然后对应于生成具体的函数(也成为模板的实例化),从而完成模板的使用。我们再来看一个求数组中的最大元素的函数模板:

template<class T>
T MaxElement(T a[], int size) // size是数组中的元素个数
{
	T tmpMax = a[0];
	for (int i = 1; i < size; ++i) {
		if (tmpMax < a[i])
			tmpMax = a[i];
	}
	return tmpMax;
}

上面的模板实例化是以参数的方式,我们也可以不通过参数的方式来实例化模板:

template<class T> 
T Inc(T n) {
	return 1 + n;
}
int main() {
	cout << Inc<double>(4) / 2;	// 指定类型为double,输出2.5
	return 0;
}

2.2 函数模板的重载

和函数一样,函数模板也可以根据不同的形参表进行重载。如:

template<class T1, class T2)
void print(T1 arg1, T2 args) {
	cout << arg1 << " " << arg2 << endl;
}	
template<class T)
void print(T arg1, T args) {
	cout << arg1 << " " << arg2 << endl;
}	
template<class T, class T2)
void print(T arg1, T2 args) {
	cout << arg1 << " " << arg2 << endl;
}	

对于函数模板和函数的次序,在有多个函数和函数模板名字相同的情况下,编译器如下处理一条函数语句调用:

  1. 先找参数完全匹配的普通函数(非由模板实例化得到的函数);
  2. 再找参数完全匹配的模板函数
  3. 再找实参经过自动类型转换后能够匹配的普通函数;
  4. 如果上面的都找不到,则会出现错误。如:
template<class T>
T Max(T a, T b) {
	cout << "TemplateMax" << endl;
	return 0;
}
template<class T, class T2>
T Max(T a, T2 b) {
	cout << "TemplateMax2" << endl;
	return 0;
}
double Max(double a, double b) {
	cout << "MyMax" << endl;
	return 0;
}
int main() {
	int i = 4, j = 5;
	Max(1.2, 3.4);	// 输出MyMax
	Max(i, j);		// 输出TemplateMax
	Max(1.2, 3);	// 输出TemplateMax2
	return 0;
}

同时,在匹配函数模板时,编译器不会进行自动类型转换。

2.3 函数模板示例:Map

函数Map(T s, T e, T x, Pred op);完成功能是将区间[ )内的元素经过变换op后存入以x为开始的区间内。

template<class T,class Pred>
void Map(T s, T e, T x, Pred op) {
	for (; s != e; ++s, ++x) {
		*x = op(*s);	// 将*s作变换op后存入*x中
	}
}
int Cube(int x) {
	return x * x * x;
}
double Square(double x) {
	return x * x;
}
int a[5] = { 1,2,3,4,5 }, b[5];	// a为原区间,b为目标区间
double d[5] = { 1.1,2.1,3.1,4.1,5.1 }, c[5];	// d为原区间,c为目标区间
int main() {
	Map(a, a + 5, b, Square);	// b: 1 4 9 16 25
	Map(a, a + 5, b, Cube);		// b: 1 8 27 64 125
	Map(d, d + 5, c, Square);	// c: 1.21 4.41 9.61 16.81 26.01
	return 0;
}

如调用Map(a, a + 5, b, Square);时,编译器会实例化以下函数:

void Map(int* s, int* e, int* x, double(*op) (double)) {
	for (: s != e; ++s, ++x) {
		*x = op(*s);
	}
}

3. 类模板

3.1 类模板的定义

同函数模板一样,为了多快好省地定义出一批相似的类,可以定义类模板,然后由类模板生成不同的类。例如,数组时一种常用的数据类型,元素类型可以是整型、字符等。现在考虑对数组的基本操作,如len查看数组的长度、getElement获取元素、setElement设置元素等。这些数组类,除了元素的类型不同,其他操作完全相同,则我们可以使用类模板避免重复定义的问题。类模板的定义如下:

template<class 类型参数1, class 类型参数2, ...>
class 类模板名
{
	成员变量和成员函数
}

其中,类模板里成员函数的写法如下:

template<class 类型参数1, class 类型参数2, ...>
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表)
{
	...
}

用类模板定义对象的写法:类模板名<真实类型参数表> 对象名(构造函数实参表);

3.2 类模板示例:Pair

template<class T1,class T2>
class Pair
{
public:
	T1 key;
	T2 value;
	Pair(T1 k, T2 v) :key(k), value(v) {};	// 构造函数
	bool operator<(const Pair<T1, T2>& p)const;	// 运算符重载"<"
};
template<class T1, class T2>
bool Pair<T1, T2>::operator<(const Pair<T1, T2>& p)const	// 实现成员函数
{
	return key < p.key;
}
int main() {
	Pair<string, int> student("Tom", 19);	// 示例化类Pair<string, int>
	cout << student.key << " " << student.value;	// Tom 19
	return 0;
}

编译器由类模板生成类的过程叫做类模板的实例化。由类模板实例化得到的类,叫模板类。注意,由同一个类模板得到的两个模板类是不兼容的。如:

Pair<string, int>* p;
Pair<string, double> a;
p = &a;	// 出错

3.3 函数模板作为类模板的成员

同样地,在类模板中我们可以将成员函数定义为函数模板。如:

template<class T>
class A{
public:
	template<class T2>	// 成员函数模板
	void Func(T2 t) {	
		cout << t;
	}
};
int main() {
	A<int> a;
	a.Func("K");		// 成员函数模板Func被实例化
	a.Func("Hello");	// 成员函数模板Func再次被实例化
	return 0;
}
// KHello

在类模板的定义中,类模板的<类型参数表>中可以出现非类型参数。如下面程序中的int size

template<class T, int size>
class CArray{
	T array[size];
public:
	void Print(){
		for (int i = 0; i < size; ++i) {
			cout << array[i] << " "; 
		}
	}
};
CArray<double, 40> a2;
CArray<int, 50> a3;		// a3和a2属于从模板类中实例化出来的不同的类

4. 类模板与继承、派生

关于类模板的继承与派生存在以下几种情况:1) 类模板从类模板派生;2) 类模板从模板类派生;3) 类模板从普通类派生;4) 普通类从模板类派生。对于类模板从类模板派生:

template<class T1, class T2>
class A {
	T1 v1; T2 v2;
};

template<class T1, class T2>
class B:public A<T2, T1> {	// 类模板公有继承
	T1 v3; T2 v4;
};

template<class T>
class C:public B<T, T> {	// 类模板公有继承
	T v5;
};

int main() {
	B<int, double> obj1;
	C<int> obj2;
	return 0;
}

对于语句B<int, double> obj1;会实例化两个类:B<int, double>A<double, int>;同理语句C<int> obj3会示例化三个类。对于类模板从模板类(该类从其他类模板中实例化)派生:

template<class T1, class T2>
class A {
	T1 v1; T2 v2;
};

template<class T>
class B:public A<int, double> {
	T v;
};

int main(){
	B<char> obj1;
	return 0;
}

对于语句B<char> obj1;会生成两个类:A<int, double>B<char>。对于类模板从普通类派生:

class A {
	int v1;
};

template<class T>
class B:public A {	// 所有从B实例化得到的类,都以A为基类
	T v;
};

int main() {
	B<char> obj1;
	return 0;
}

对于普通类从模板类派生:

template<class T>
class A {
	T v1;
	int n;
};

class B:public A<int> {
	double v;
};

int main() {
	B obj1;
	return 0;
}

5. 类模板与友元

对于类模板与友元的关系,存在以下几种情况:1) 函数、类、类的成员函数作为类模板的友元;2) 函数模板作为类模板的友元;3) 函数模板作为类的友元;4) 类模板作为类模板的友元。对于函数、类、类的成员函数作为类模板的友元:

void Func1() { }

class A { };

class B {
public:
	void Func() { }
};

template<class T>
class Tmp1 {
	friend void Func1();
	friend class A;
	friend void B::Func();
};	// 任何从Tmp1实例化得到的类,都有以上三个友元

对于函数模板作为类模板的友元:

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 << ")";	// 输出对象p的内容
	return o;
}

int main() { 
	Pair<string, int> studeng("Tom", 19);
	Pair<int, double> obj(12, 3.14);
	cout << student << " " << obj;	// 重载后的左移运算符可以直接输出对象的内容
	return 0;
}

对于函数模板作为类的友元:

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);	// 输出4
	return 0;
}

对于类模板作为类模板的友元:

template<class T>
class B {
	T v;
public:
	B(T n):v(n) { }
	template<calss T2>
	friend class A;	// 类模板A作为类模板B的友元
};

template<class T>
class A {
public:
	void Func() {
		B<int> o(10);
		cout << o.v << endl;	// 友元可以直接访问B的私有成员
	}
};

int main() {
	A<double> a;
	a.Func();	// 输出10
	return 0;	
}

6. 类模板与静态成员

对于类模板与静态成员的关系,类模板中可以定义静态成员,并且从该类模板实例化得到的所有类都包含同样的静态成员。如:

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;
}

7. 总结

C + + {\rm C}++ C中的模板是泛型变成的基础,泛型编程及以一种独立于任何特定数据类型的方式编写代码。模板的引入又一次体现了 C + + {\rm C}++ C的简介、高效的特点,使其支持参数化多态。函数和类是我们在程序设计过程中使用最多的两种元素,而 C + + {\rm C}++ C提供的函数模板类模板可以大大提高开发程序的效率。


参考

  1. 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值