目录
泛型程序设计是一种算法在实现时不指定具体要操作的数据的类型的程序设计方法。所谓 “泛型”,指的是算法只要实现一遍,就能适用于多种数据类型。泛型程序设计方法的优势在于能欧减少重复代码的编写。泛型程序设计的概念最早出现于 1983 年的 Ada 语言,其最成功的应用就是C++的标准模板库(STL)。也可以说,泛型程序设计就是大量编写模板、使用模板的程序设计。泛型程序设计在C++中的重要性和带来的好处不亚于面向对象的特性。在C++中,模板分为函数模板和类模板两种。熟练的C++程序员,在编写函数时都会考虑能否将其写成函数模板,编写类时都会考虑能否将其写成类模板,以便实现重用。
9.1 函数模板
9.1.1 函数模板的作用
面向对象的继承和多态机制有效提高了程序的可重用性和可扩充性。在程序的可重用性方面,程序员还希望得到更多支持。举一个最简单的例子,为了交还两个整型变量的值,需要写下面的 Swap 函数:
void Swap(int & x, int & y)
{
int tmp = x;
x = y;
y = tmp;
}
为了交换两个 double 型的变量的值,还需要编写下面的 Swap 函数:
void Swap(idouble & x, double & y)
{
double tmp = x;
x = y;
y = tmp;
}
如果还要交换两个 char 型变量的值,交换两个 CStudent 类对象的值……都需要再编写 Swap 函数。而这些 Swap 函数除了处理的数据类型不同外,形式上都是一样的。能否只写一遍 Swap 函数,就能用来交换各种类型的变量的值呢?继承和多态显然无法解决这个问题。因此,“模板” 的概念就应运而生了。
程序设计语言中的模板就是用来批量生辰功能和形式都几乎相同的代码的。有了模板,编译器就能在需要的时候,根据模板自动生成程序的代码。从同一个模板自动生成的代码,形式几乎是一样的。
9.1.2 函数模板的原理
C++语言支持模板。有了模板,可以只写一个 Swap 模板,编译器会根据 Swap 模板自动生成多个 Swap 函数,用以交换不同类型变量的值。在C++中,模板分为函数模板和类模板两种。函数模板是用于生成函数的,类模板则是用于生成类的。函数模板的写法如下:
template <class 类型参数1, class 类型参数2, ...>
返回值类型 模板名(形参表)
{
函数体
}
其中的 class 关键字也可以用 typename 关键字替换,例如:
template <typename 类型参数1, typename 类型参数2, ...>
函数模板看上去就像一个函数。前面提到的 Swap 函数模板写法如下:
template<class T>
void Swap(T & x, T & y)
{
T tmp = x;
x = y;
y = tmp;
}
T 是类型参数,代表类型。编译器由模板自动生成函数时,会用具体的类型名对模板中所有的类型参数进行替换,其他部分则原封不动地保留。同一个类型参数只能替换为同一种类型。编译器在编译到调用函数模板的语句时,会根据实参的类型判断该如何替换模板中的类型参数。例如下面的程序:
#include <iostream>
using namespace std;
template <class T>
void Swap(T & x, T & y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int n = 1, m = 2;
Swap(n, m);
double f = 1.2, g = 2.3;
Swap(f, g);
return 0;
}
编译器由模板自动生成函数的过程叫模板的实例化,由模板实例化而得到的函数称为模板函数。在某些编译器中,模板只有在被实例化时,编译器才会检查其语法正确性。如果程序中写了一个模板却没有用到,那么编译器不会报告这个模板中的语法错误。
编译器对模板进行实例化时,并非只能通过模板调用语句的实参来实例化模板中的类型参数,模板调用语句可以明确指明要把类型参数实例化为哪种类型。可以用:
模板名 <实际类型参数1, 实际类型参数2, ...>
的方式告诉编译器应该如何实例化模板函数。例如下面的程序:
#include <iostream>
using namespace std;
template <class T>
T Inc(int n)
{
return 1+n;
}
int main()
{
cout << Inc<double> (4)/2;
return 0;
}
“Inc<double> (4)” 指明了此处实例化的模板函数原型应为:
double Inc(double);
编译器不会因为实参 4 是 int 类型,就生成原型为 int Inc(int) 的函数。因此,上面程序输出的结果是 2.5 而非 2.
函数模板中可以有不止一个类型参数。例如,下面这个函数模板的写法是合法的:
template<class T1, class T2>
T2 print(T1 arg1, T2 arg2>
{
cout << arg1 << " " << arg2 << endl;
return arg2;
}
9.1.3 一个求数组中最大元素的函数模板
例题:设计一个分数类 CFraction,在设计一个名为 MaxElement 的函数模板,能够求数组中的最大元素,并用该模板求一个 CFraction 数组中的最大元素。
示例程序如下:
#include <iostream>
using namespace std;
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;
}
class CFraction //分数类
{
int numerator; //分子
int denominator; //分母
public:
CFraction(int n,int d):numerator(n),denominator(d) { };
bool operator <(const CFraction & f) const
{//为避免除法产生的浮点误差,用乘法判断两个分数的大小关系
if(denominator * f.denominator > 0)
return numerator * f.denominator < denominator * f.numerator ;
else
return numerator * f.denominator > denominator * f.numerator ;
}
bool operator == (const CFraction & f) const
{//为避免除法产生的浮点误差,用乘法判断两个分数是否相等
return numerator * f.denominator == denominator * f.numerator;
}
friend ostream & operator <<(ostream & o,const CFraction & f);
};
ostream & operator <<(ostream & o,const CFraction & f)
{//重载 << 使得分数对象可以通过cout输出
o << f.numerator << "/" << f.denominator; //输出"分子/分母" 形式
return o;
}
int main()
{
int a[5] = { 1,5,2,3,4 };
CFraction f[4] = {CFraction(8,6),CFraction(-8,4),
CFraction(3,2), CFraction( 5,6)};
cout << MaxElement(a,5) << endl;
cout << MaxElement(f,4) << endl;
return 0;
}
编译到第 41 行时,根据实参 a 的类型,编译器通过 MaxElement 模板自动生成了一个 MaxElement 函数,原型为:
int MaxElement(int a[ ], int size);
编译到第 42 行时,根据 f 的类型,编译器又生成一个 MaxElement 函数,原型为:
CFraction MaxElement(CFraction a[ ], int size);
在该函数中,用到了 “<” 比较两个CFraction 对象的大小。如果没有对 “<” 进行适当的重载,编译时就会出错。
从 MaxElement 模板的写法可以看出,在函数模板中,类型参数不但可以用来定义参数的类型,还能用于定义局部变量和函数模板的返回值。
9.1.4 函数或函数模板调用语句的匹配顺序
函数模板可以重载,只要它们的形参表不同即可。例如,下面两个模板可以同时存在;
template<class T1, class T2>
void print(T1 arg1, T2 arg2>
{
cout << arg1 << " " << arg2 << endl;
}
template<class T>
void print(T arg1, T arg2>
{
cout << arg1 << " " << arg2 << endl;
}
在有多个函数和函数模板名字相同的情况下,一条函数调用语句到底应该被匹配成对哪个函数或哪个模板的调用呢?C++编译器遵循以下先后顺序:
(1)先找参数完全匹配的普通函数(非由模板实例化得到的函数)。
(2)再找参数完全匹配的模板函数。
(3)再找实参经过自动类型转换后能够匹配的普通函数。
(4)如果上面的都找不到,则报错。
例如下面的程序:
#include <iostream>
using namespace std;
template <class T>
T Max(T a, T b)
{
cout << "Template Max 1" << endl;
return 0;
}
template <class T, class T2>
T Max(T a, T2 b)
{
cout << "Template Max 2" << endl;
return 0;
}
double Max(double a, double b)
{
cout << "Function Max" << endl;
return 0;
}
int main()
{
int i = 4, j = 5;
Max(1.2, 3.4);
Max(i, j);
Max(1.2, 3);
return 0;
}
程序的输出结果是:
Function Max
Template Max 1
Template Max 2
如果把程序中的 Max 函数和第二个 Max 模板都去掉,按照上面所说的第 4 条匹配规则,第 23 行的 “Max(1.2, 3);” 编译时就会出错。因为从第一个 Max 模板没法生成与之类型完全匹配的模板函数 Max(double, int)。虽然从该 Max 模板可以生成 int Max(int ,int) 和 double Max(double, double),但是到底应该把 1.2 自动转换成 int 类型后调用前者,还是应该把 3 自动转换成 double 类型后调用后者呢?这是由二义性的,因此编译器会报错。
9.2 类模板
9.2.1 类模板的原理
人们需要编写多个形式和功能都相似的函数,因此有了函数模板来减少重复劳动;人们也需要编写多个形式和功能都相似的类,于是C++引入了类模板的概念,编译器从类模板可以自动生成多个类,避免了程序员的重复劳动。
例如,“运算符重载” 一章实现了一个可变长的整形数组类,可能还需要可变长的 double 数组类,可变长的 CStudent 数组类,等等。如果要把类似于可变长整型数组类的代码都重写一遍,无疑非常麻烦。有了类模板的机制,只需要写一个可变长的数组类模板,编译器就会由该类模板自动生成整型、double 型等各种类型的可变长数组了。
C++中类模板的写法如下:
templat <类型参数表>
class 类模板名{
成员函数和成员变量
};
类型参数表的写法如下:
class 类型参数1, class 类型参数2, ...
类模板中的成员函数放到类模板定义外面写时的语法如下:
template <类型参数表>
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表)
{
...
}
用类模板定义对象的写法如下:
类模板名<真实类型参数表> 对象名(构造函数实际参数表);
如果类模板有无参构造函数,那么也可以使用如下写法:
类模板名<真实类型参数表>对象名;
类模板看上去很像一个类。下面以 Pair 类模板为例来说明类模板的写法和用法。实践中常常会碰到,某项数据记录由两部分组成,一部分是关键字,另一部分是值。关键字用来对记录进行排序和检索,根据关键字能查到值。例如,学生记录由两部分组成,一部分是学号,另一部分是绩点。要能根据学号对学生进行排序,以方便地检索绩点,则学号就是关键字,绩点就是值。下面的 Pair 类模板就可用来处理这样的数据记录。
#include <iostream>
#include <string>
using namespace std;
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
//Pair的成员函数 operator <
{ //"小"的意思就是关键字小
return key < p.key;
}
int main()
{
Pair<string,int> student("Tom",19); //实例化出一个类 Pair<string,int>
cout << student.key << " " << student.value;
return 0;
}
程序的输出结果是:
Tom 19
实例化一个类模板使,如第21行,真是类型参数表中的参数是具体的类型名,如 string,int 或其他类的名字(如 CStudent)等,它们用来一一对应地替换类模板定义中 “类型参数表” 中的类型参数。“类模板名<真实类型参数表>” 就成为一个具体的类的名字。编译到第 21 行时,就会用 string 替换 Pair 模板中的 T1,用 int 替换 T2,其余部分原样保留,这样就自动生成了一个新的类。这个雷的名字编译器是如何处理的不需要知道,可以认为它的名字就是 Pair<string, int>。也可以说,student 对象的类型就是 Pair<string, int>。
Pair<string, int> 类的成员函数自然也是通过替换 Pair 模板的成员函数中的 T1、T2 得到的。
编译器由类模板生成类的过程叫类模板的实例化。由类模板实例化得到的类叫模板类。
9.2.2 函数模板作为类模板成员
类模板中的成员函数还可以是一个函数模板。成员函数只有在被调用时才会被实例化。例如下面的程序;
#include <iostream>
using namespace std;
template <class T>
class A{
public:
template <class T2>
void Func(T2 t) {cout << t;} //成员函数模板
};
int main()
{
A<int> a;
a.Func('K');
a.Func("hello");
return 0;
}
程序的输出结果是:
Khello
9.2.3 类模板实例:可变长数组类模板
为加深对类模板应用的理解,下面的程序给出一个可变长数组类模板的实现。
#include <iostream>
#include <cstring>
using namespace std;
template <class T>
class CArray
{
int size; //数组元素的个数
T *ptr; //指向动态分配的数组
public:
CArray(int s = 0); //s代表数组元素的个数
CArray(CArray & a);
~CArray();
void push_back(const T & v); //用于在数组尾部添加一个元素v
CArray & operator=(const CArray & a); //用于数组对象间的赋值
T length() { return size; }
T & operator[](int i)
{//用以支持根据下标访问数组元素,如a[i] = 4;和n = a[i]这样的语句
return ptr[i];
}
};
template<class T>
CArray<T>::CArray(int s):size(s)
{
if(s == 0)
ptr = NULL;
else
ptr = new T[s];
}
template<class T>
CArray<T>::CArray(CArray & a)
{
if(!a.ptr) {
ptr = NULL;
size = 0;
return;
}
ptr = new T[a.size];
memcpy(ptr, a.ptr, sizeof(T ) * a.size);
size = a.size;
}
template <class T>
CArray<T>::~CArray()
{
if(ptr) delete [] ptr;
}
template <class T>
CArray<T> & CArray<T>::operator=(const CArray & a)
{ //赋值号的作用是使"="左边对象里存放的数组,大小和内容都和右边的对象一样
if(this == & a) //防止a=a这样的赋值导致出错
return * this;
if(a.ptr == NULL) { //如果a里面的数组是空的
if( ptr )
delete [] ptr;
ptr = NULL;
size = 0;
return * this;
}
if(size < a.size) { //如果原有空间够大,就不用分配新的空间
if(ptr)
delete [] ptr;
ptr = new T[a.size];
}
memcpy(ptr,a.ptr,sizeof(T)*a.size);
size = a.size;
return *this;
}
template <class T>
void CArray<T>::push_back(const T & v)
{ //在数组尾部添加一个元素
if(ptr) {
T *tmpPtr = new T[size+1]; //重新分配空间
memcpy(tmpPtr,ptr,sizeof(T)*size); //拷贝原数组内容
delete []ptr;
ptr = tmpPtr;
}
else //数组本来是空的
ptr = new T[1];
ptr[size++] = v; //加入新的数组元素
}
int main()
{
CArray<int> a;
for(int i = 0;i < 5;++i)
a.push_back(i);
for(int i = 0; i < a.length(); ++i)
cout << a[i] << " ";
return 0;
}
9.3 类模板中的非类型参数
类模板的 “类型参数表” 中可以出现非类型参数,例如下面的例子:
template <class T, int size>
class CArray{
T array[size];
public:
void Print()
{
for(int i = 0; i < size; ++i)
cout << array[i] << endl;
}
};
可以用 CArray 模板定义对象,例如:
CArray<int, 40> a;
编译器自动生成名为 CArray<int, 40> 的类。该类是通过将 CArray 模板中的 T 换成 int、size 换成 40 后得到的。还可定义以下对象:
CArray<double, 40> a2;
CArray<int, 50> a3;
注意,CArray<int, 40> 和 CArray<int 50> 完全是两个类,这两个类的对象之间不能互相赋值。
9.4 类模板与继承
类模板和类模板之间、类模板和类之间可以互相继承。它们之间的派生关系有以下情况。
9.4.1 类模板从类模板派生
示例程序:
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;
}
编译到第 18 行,编译器用 int 替换类模板 B 中的 T1,用 double 替换 T2,生成 B<int, double> 类如下:
class B<int, double>:public A<double, int>{
int v3; double v4;
};
B<int, double> 的基类是 A<double, int>。于是编译器就要用 double 替换类模板 A 中的 T1,用 int 替换 T2,生成 A<double, int> 类如下:
class A<double, int>{
double v1; int v2;
};
编译到第 19 行,编译器生成类 C<int>,还有 C<int> 的直接基类 B<int, int>,以及 B<int, int> 的基类 A<int, int>。
9.4.2 类模板从模板类派生
示例程序:
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;}
第 4 行,A<int, double> 是一个具体的类的名字,而且它是一个模板类,因此说类模板 B 是从模板类派生而来的。
编译器编译到第 5 行 “B<char> obj1;” 时会自动生成两个模板类:A<int, double> 和 B<char>。
9.4.3 类模板从普通类派生
示例程序:
class A{int v1;};
template<class T>
class B:public A{T v;};
int main() {B<char> obj1; return 0;}
9.4.4 普通类从模板类派生
示例程序:
template <class T>
class A{T v1; int n;};
class B:public A<int>{double v;};
int main() {B obj; return 0;}
9.5 类模板和友元
9.5.1 函数、类、类的成员函数作为类模板的友元
函数、类、类的成员函数都可以作为类模板的友元。程序示例如下:
void Func1() { }
class A { };
class B
{
public:
void Func() { }
};
template <class T>
class Tmpl
{
friend void Func1();
friend class A;
friend void B::Func();
};
int main()
{
Tmpl<int> i;
Tmpl<double> f;
return 0;
}
类模板实例化时,除了类型参数被替换外,其他所有内容都原样保留,因此任何从 Tmpl 实例化得到的类都包含上面三条友元声明,因而也都会把 Func1、类 A 和 B::Func 当作友元。
9.5.2 函数模板作为类模板的友元
例题:改进 9.2.1 节 “类模板的原理” 中的 Pair 模板,将 “<<” 运算符重载为一个函数模板,并将该函数模板作为 Pair 模板的友元,这样,任何从 Pair 模板实例化得到的对象都能用 “<<” 运算符通过 cout 输出。
程序代码如下:
#include <iostream>
#include <string>
using namespace std;
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 << ")";
return o;
}
int main()
{
Pair<string, int> student("Tom", 29);
Pair<int, double> obj(12, 3.14);
cout << student << " " << obj;
return 0;
}
程序的输出结果是:
(Tom,29) (12,3,14)
第 13、14 行将函数模板 operator << 声明为类模板 Pair 的友元。在 Visual Studio中,这两行也可以用下面的写法替代:
friend ostream & operator <<<T1, T2> (ostream & o, const Pair<T1, T2> & p);
但在 Dev C++中,替代后编译就无法通过了。
编译本程序时,编译器自动生成了两个 operator << 函数,它们的原型分别是:
ostream & operator << (ostream & o, const Pair<string, int> & p);
ostream & operator << (ostream & o, const Pair<int, double> & p);
前者是 Pair<string, int> 类的友元,但不是 Pair<int, double> 类的友元;后者是 Pair<int, double> 类的友元,但不是 Pair<string, int> 类的友元。
9.5.3 函数模板作为类的友元
实际上,类也可以将函数模板声明为友元。程序示例如下:
#include <iostream>
using namespace std;
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);
return 0;
}
程序的输出结果是:
4
编译器编译到第 19 行 “Print(a);” 时,就从 Print 模板实例化出一个 Print 函数,原型如下:
void Print(const A & p);
这个函数本来不能访问 p 的私有成员。但是编译器发现,如果将类 A 的友元声明中的 T 换成 A,就能起到将该 Print 函数声明为友元的作用,因此编译器就认为该 Print 函数是类 A 的友元。
思考题:类还可以将类模板或类模板的成员函数声明为友元。自行研究这两种情况该怎么写。
答:声明方式参考 9.4,主要是注意如何进行实例化并合理编写代码。(该答案择日优化并给出示例)
9.5.4 类模板作为类模板的友元
一个类模板还可以将另一个类模板声明为友元。程序示例如下:
#include <iostream>
using namespace std;
template <class T>
class A{
public:
void Func(const T & p)
{
cout << p.v;
}
};
template <class T>
class B{
private:
T v;
public:
B(T n):v(n) { }
template <class T2>
friend class A;
};
int main()
{
B<int> b(5);
A<B<int>> a;
a.Func(b);
return 0;
}
程序的输出结果是:
5
本程序中,A<B<int>> 类成为 B<int> 类的友元。
9.6 类模板中的静态成员
类模板中可以定义静态成员,从该类模板实例化得到的所有类都包含同样的静态成员,程序示例如下:
#include <iostream>
using namespace std;
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;
}
程序的输出结果是:
1
1
第 14 行和第 15 行,对静态成员变量的类外部加以声明时必需的。在 Visual Studio 2008 中,这两行也可以简单的写成:
int A<int>::count = 0;
int A<double>::count = 0;
A<int> 和 A<double> 是两个不同的类。虽然它们都有静态成员变量 count,但是显然,A<int> 的对象 ia 和 A<double> 的对象 da 不会共享一份 count。
9.7 在多个文件中使用模板
在多文件的C++程序中,如果多个 .cpp 文件都要用到同一个模板,则可以将该模板的全部内容(包括类模板的成员函数的函数体)都写在一个头文件中,然后在多个 .cpp 文件中包含 ,而不用担心重复定义的问题。