C++函数模板(模板函数)详解

面向对象的继承和多态机制有效提高了程序的可重用性和可扩充性。在程序的可重用性方面,程序员还希望得到更多支持。举一个最简单的例子,为了交换两个整型变量的值,需要写下面的 Swap 函数:
void Swap(int & x, int & y)
{
    int tmp = x;
    x = y;
    y = tmp;
}

为了交换两个 double 型变量的值,还需要编写下面的 Swap 函数:
void Swap (double & xr double & y)
{
    double tmp = x;
    x = y;
    y = tmp;
}

如果还要交换两个 char 型变量的值,交换两个 CStudent 类对象的值……都需要再编写 Swap 函数。而这些 Swap 函数除了处理的数据类型不同外,形式上都是一样的。能否只写一遍 Swap 函数,就能用来交换各种类型的变量的值呢?继承和多态显然无法解决这个问题。因此,“模板”的概念就应运而生了。

众所周知,有了“模子”后,用“模子”来批量制造陶瓷、塑料、金属制品等就变得容易了。程序设计语言中的模板就是用来批量生成功能和形式都几乎相同的代码的。有了模板,编译器就能在需要的时候,根据模板自动生成程序的代码。从同一个模板自动生成的代码,形式几乎是一样的。

函数模板的原理

C++ 语言支持模板。有了模板,可以只写一个 Swap 模板,编译器会根据 Swap 模板自动生成多个 Sawp 函数,用以交换不同类型变量的值。

在 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);  //编译器自动生成 void Swap (int &, int &)函数
    double f = 1.2, g = 2.3;
    Swap(f, g);  //编译器自动生成 void Swap (double &, double &)函数
    return 0;
}
编译器在编译到Swap(n, m);时找不到函数 Swap 的定义,但是发现实参 n、m 都是 int 类型的,用 int 类型替换 Swap 模板中的 T 能得到下面的函数:
void Swap (int & x, int & y)
{
    int tmp = x;
    x = y;
    y = tmp;
}
该函数可以匹配Swap(n, m);这条语句。于是编译器就自动用 int 替换 Swap 模板中的 T,生成上面的 Swap 函数,将该 Swap 函数的源代码加入程序中一起编译,并且将Swap(n, m);编译成对自动生成的 Swap 函数的调用。

同理,编译器在编译到Swap(f, g);时会用 double 替换 Swap 模板中的 T,自动生成以下 Swap 函数:
void Swap(double & x, double & y)
{
    double tmp = x;
    x = y;
    y = tmp;
}
然后再将Swap(f, g);编译成对该 Swap 函数的调用。

编译器由模板自动生成函数的过程叫模板的实例化。由模板实例化而得到的函数称为模板函数。在某些编译器中,模板只有在被实例化时,编译器才会检查其语法正确性。如果程序中写了一个模板却没有用到,那么编译器不会报告这个模板中的语法错误。

编译器对模板进行实例化时,并非只能通过模板调用语句的实参来实例化模板中的类型参数,模板调用语句可以明确指明要把类型参数实例化为哪种类型。可以用:

模板名<实际类型参数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 Tl, class T2>
T2 print(T1 argl, T2 arg2)
{
    cout << arg1 << " " << arg2 << endl;
    return arg2;
}

【实例】一个求数组中最大元素的函数模板

例题:设计一个分数类 CFraction,再设计一个名为 MaxElement 的函数模板,能够求数组中最大的元素,并用该模板求一个 CFmction 数组中的最大元素。

示例程序如下:
#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 模板的写法可以看出,在函数模板中,类型参数不但可以用来定义参数的类型,还能用于定义局部变量和函数模板的返回值。