文章目录
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;
}
对于函数模板和函数的次序,在有多个函数和函数模板名字相同的情况下,编译器如下处理一条函数语句调用:
- 先找参数完全匹配的普通函数(非由模板实例化得到的函数);
- 再找参数完全匹配的模板函数;
- 再找实参经过自动类型转换后能够匹配的普通函数;
- 如果上面的都找不到,则会出现错误。如:
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++提供的函数模板和类模板可以大大提高开发程序的效率。
参考
- 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.