首页 > 编程笔记

C++函数模板(入门必读)

模板是 C++ 的新特性,这个概念在C语言中是没有的。函数模板不是一个实实在在的函数,而是对逻辑功能相同、但数据类型不同的一组函数的统一描述。

例如定义一个简单的求数组中最小值的函数,要求可以处理各种数据类型。如果不使用模板,那么开发者不得不针对每种类型定义一个函数:
int myMin(int arr[], int n){...};
int myMin(double arr[], double n){...};
......
这里只写出了针对 int 型和 double 型数组的函数。为了适应各种情形,还应当编写针对 float, char,short,long,unsigned int 等所有数据类型的函数,甚至还要支持自定义数据类型(如结构体、类)。

显然,这不是好的实现方案,比如一旦算法逻辑发生改变,或者要纠正某个错误,所有的重载函数都需要修改。

简单分析一下上述代码就会发现,所有这些函数在算法逻辑上都是相同的,不同的只是所处理数据的类型。如果能将函数的返回类型以及参数类型当做变量对待,当需要处理不同类型数据的函数时,只要将这些“类型变量”赋值为所需的类型就好了。这样,只编写一次通用算法逻辑就可以处理所有的数据类型,这就是函数模板的含义。

下面的代码使用函数模板改进了上述求最小值的 myMin() 函数:
template<typename T>
T myMin(T arr[], T n){...};
其中,T 是一个参数,它可以是任意指定的类型。显然通过使用函数模板,避免了定义多余的重载函数,提高了开发效率。

函数模板的定义

函数模板的定义以 template 关键字开始,后跟模板参数列表,该列表通常包含一个或多个类型参数,这些参数用于指定函数接受哪些类型的输入。

语法格式如下:
template <typename T,...>
返回值类型 函数名(T 参数1, T 参数2, ...) {
    // 函数体
}
其中,T 是一个类型参数,可以用于函数返回类型、参数类型以及函数体内的变量类型。一个函数模板中,类型参数可以指定多个,中间用逗号分隔即可。

typename 关键字也可以用 class 关键字代替,两者意义相同,都可以用来声明类型参数。但是用 typename 更好一些,可以明白地表示后面的参数是一个“类型名”。而且 typename 是 C++ 标准化的产物,而 class 关键字则是为了支持 C++ 标准化之前的程序而保留下来的。

函数模板的使用

下面是一个简单的例子,展示了如何使用函数模板来创建一个 swap() 函数:
#include <iostream>

// 函数模板定义
template <typename T>
void swap(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5, y = 10;
    std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
    swap(x, y);  // 使用int类型的swap
    std::cout << "After swap: x = " << x << ", y = " << y << std::endl;

    double m = 5.5, n = 10.5;
    std::cout << "Before swap: m = " << m << ", n = " << n << std::endl;
    swap(m, n);  // 使用double类型的swap
    std::cout << "After swap: m = " << m << ", n = " << n << std::endl;

    return 0;
}
输出结果为:

Before swap: x = 5, y = 10
After swap: x = 10, y = 5
Before swap: m = 5.5, n = 10.5
After swap: m = 10.5, n = 5.5

在这个示例中,我们定义了函数模板 swap(),该模板接受两个类型为 T 的引用参数。然后,我们在 main() 函数中用不同的数据类型(int 和 double)调用了这个模板函数。

函数模板的非类型参数

在函数模板的模板参数列表中,除了放置类型参数外,还可以使用非类型参数,目的就是为函数引入一个常量,以供定义函数时使用,当然这个常量也可以作为函数参数的默认值使用。

例如,对于一个数组求最小值,可以用一个非类型的参数表示数组长度。
template <typename T,int size>  // 类型参数 T,非类型参数 size 表示数组长度
T myMin(T arr[]) {
    T minVal = 0;
    for (int i = 0; i < size; i++) {
        if (arr[i] < minVal) {
            minVal = arr[i];
        }
    }
    return minVal;
}

函数模板的显式实例化

对函数模板做显式实例化,指的就是手动指定函数模板的模板参数。

以上面定义好的 myMin() 模板函数为例,调用它时可以显式指定模板参数:
unsigned int arr[5]={1,4,3,5,6};
myMin<unsigned int, 5>(arr);
上述例子中,通过模板实参表 <unsigned int> 强制将函数模板 myMin() 的模板参数 T 指定为 unsigned int。

显式实例化的作用主要是解决模板参数推演时的二义性问题。既然指定了模板参数,那么在使用函数模板时就不必进行参数推演了,也就避免了参数推演的二义性问题。

函数模板的特化

很多时候,定义一个适合所有类型的函数模板非常困难,这主要是因为对于某种逻辑操作,各种数据类型的实现并不相同。

例如,同样是比较大小,整型和浮点型数的比较就与字符串类型的比较不同。整型和浮点型数据只要调用各种比较运算符(<、>、== 等)即可,但是字符串的比较就需要调用专门的比较函数,例如 strcmp() 和 wstrcmp()。而自定义数据类型,如结构体、类,比较大小就更加特殊。在这种情况下就需要用到函数模板的特化。

例如,一个函数模板 myMax() 的定义如下:
template <typename T>
T myMax(T t1, T t2) {
    return t1 > t2 ? t1 : t2;
}
如果用 const char* 型实参实例化 myMax() 模板,程序员的本意是比较字符串的大小,但实例化的结果却是比较两个指针的大小。为了获得正确的语义,必须针对 const char* 类型,为函数模板 myMax() 提供特化的版本。

在函数模板特化定义中,先是关键字 template 和一对尖括号“<>”,然后是函数模板特化的定义。例如,针对 const char * 类型,模板 myMax() 显式特化为:
template <> const char * myMax(const char * t1, const char * t2) {
    return strcmp(t1, t2) > 0? t1 : t2;
}
由于有了这个特化版本,当在程序中调用函数 myMax(const char*,const char*) 时,真正被调用的是特化的版本,而不是用类型 const char* 实例化的模板。

总结

函数模板是 C++ 编程中一个非常有用的特性,通过它你可以编写灵活、可重用的代码。这不仅简化了代码维护,还提高了代码质量。一旦你熟悉了这一概念,你会发现它在许多不同的编程场景中都非常有用。

推荐阅读