模板

在前面的课程中,已经讲过了函数重载,以下是交换两个变量值的函数及其重载:

void my_swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

void my_swap(char& a, char& b) {
    char temp = a;
    a = b;
    b = temp;
}

void my_swap(double& a, double& b) {
    double temp = a;
    a = b;
    b = temp;
}

除了 int、char、double 外,还有很多数据类型没有覆盖到,这就意味着,我们就需要写更多的代码继续实现函数重载。我们能不能只写一套代码呢?答案是肯定可以的,这就是我们这节课要讲的 模板

在 C++ 中,模板 是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数,从而实现了真正的代码重用。模板主要有两种类型:函数模板类模板

函数模板

函数模板允许你定义一个函数,其操作可以应用于不同的数据类型。它们可以在编译时生成多个版本的函数,以适应不同的数据类型。

函数模板的一般说明形式如下:

template<typename 类型参数>
类型参数名 函数名(模板形参列表) {
    函数体
}

template 是一个声明模板的关键字,他表示声明一个模板。其中的 typename,也可以用 class 关键字来替换,表示任何类型。注意:此处的 class 并非类的意思。

我们可以把之前 my_swap 函数通过模板函数的方式重写一遍,代码如下:

#include <iostream>
using namespace std;

template<typename T>
void my_swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}


int main() {
    // int 类型
    int int_a = 1, int_b = 0;
    my_swap<int>(int_a, int_b);
    cout << "int_a: " << int_a << ",int_b: " << int_b << endl;

    // char 类型
    char char_a = 'a', char_b = 'b';
    my_swap<char>(char_a, char_b);
    cout << "char_a: " << char_a << ",char_b: " << char_b << endl;

    // string 类型
    string string_a = "罗大富", string_b = "罗小富";
    my_swap<string>(string_a, string_b);
    cout << "string_a: " << string_a << ",string_b: " << string_b << endl;

    return 0;
}

上述代码中,我们在主函数中调用了 3 次 my_swap 函数,每次调用传入的参数类型均不同,因此在函数名后面的尖括号 <>中写入的模板参数类型也不同。交换两个 int 变量的值,标识符 T 就会变成 int;交换两个 char 变量,标识符 T 就会变成 char;交换两个 string 变量,T 就会变成 string。

在这个案例中,模板只有一个参数 T,函数的参数 a 与 b 的类型是相同的,传递的实参类型也是相同的。在这种情况下,可以省略尖括号以及其中的类型参数,直接写成普通函数的调用形式即可,编译器会根据变量类型,自动推断出模板类型参数 T 的值究竟是 int、char 还是其他数据类型。

例如,把上述代码中 string 类型 的 my_swap 函数的模板参数省略,程序依然可以正常运行,代码如下:

string string_a = "罗大富", string_b = "罗小富";
my_swap(string_a, string_b);
cout << "string_a: " << string_a << ",string_b: " << string_b << endl;

实际上,C++ 的标准库中已经定义过 swap 函数了,相比我们的代码更加的优化。因此,推荐直接使用标准库中的 swap 函数。

还有很多情况下,是不可以省略模板参数类型的。例如,通过使用函数模板,比较两个数并返回较大值,代码如下:

#include <iostream>
using namespace std;

template<typename T>
T get_max(T a, T b) {
    if (a > b) {
        return a;
    }
    return b;
}


int main() {
    int int_a = 10, int_b = 20;
    int int_result = get_max(int_a, int_b);
    cout << "较大值为:" << int_result << endl;

    float float_a = 66.6;
    int int_c = 10;
    float float_result = get_max<float>(float_a, int_c);
    cout << "较大值为:" << float_result << endl;
    return 0;
}

在上述代码的第一个例子中,由于 int_a, 与 int_b 均为整型,直接调用该函数即可。但是第二个例子中,get_max 的两个参数的数据类型其中一个为 int,另一个为 float,如果省略掉模板参数类型 float 就会报错。因为在调用传参时,两个实参的参数类型不同,从而导致编译器无法自动判断出模板参数 T 的类型。因此,需要添加参数类型为 float,这样在把 int_c 传递到函数中的时候,就会被转换成浮点型。

有一点需要注意的是,函数模板只是描述了一个函数的外观,并不是真正定义了函数。编译器需要对模板所描述的函数进行实例化,才能成为真正的函数定义。

例如,调用了 max<int>(int_a, int_b),编译器就会生成一个对应 int 变量类型的函数定义;如果调用了 max<float>(float_a, float_b),编译器就会生成一个对应 float 变量类型的函数定义。像这种通过调用函数来实例化函数模板的方式,叫作 隐式实例化(Implicit Instantiation)。采用隐式实例化的好处是,只有需要的时候才生成对应的函数。采用隐式实例化的方式,通常会将函数模板放在头文件中。

如果在多个源文件中都使用相同类型的参数去调用函数模板,会不会出现重复定义呢?这在编译过程中是有可能出现相同情况的,但是编译器会采取措施,保证最终的可执行程序只有一份这样的函数定义。

函数模板可以有多个类型的参数,代码如下:

#include <iostream>
using namespace std;

template<typename T>
T get_max(T a, T b) {
    if (a > b) {
        return a;
    }
    return b;
}

template<class T1, class T2>
T1 get_max(T1 a, T2 b) {
    if (a > b) {
        return a;
    }
    return b;
}


int main() {
    int int_a = 10, int_b = 20;
    int int_result = get_max(int_a, int_b);
    cout << "较大值为:" << int_result << endl;

    float float_a = 66.6;
    int int_c = 10;
    float float_result = get_max<float, int>(float_a, int_c);
    cout << "较大值为:" << float_result << endl;
    return 0;
}

在原有代码的基础上,重写一个 get_max 函数模板,新的函数模板中有两个类型参数 T1、T2。在实例化时,就需要传递两个类型作为参数,如果编译器可以正常识别,也可以省略。

此时可以发现,函数模板也是可以重载的,上下两个函数模板并没有出现任何冲突。

类模板

C++ 除了支持函数模板,还支持类模板。函数模板中定义的类型参数可以用在函数声明和函数定义中,类模板中定义的类型参数可以用在类声明和类实现中。类模板的目的同样是将数据的类型参数化。

定义类模板的一般格式为:

template<typename T1, typename T2, ...>
class 类模板名 {
    类内成员定义
}

其中 T1、T2 表示任何类型。

类模板和函数模板都是以 template 开头(当然也可以使用 class,目前来讲它们没有任何区别),后跟类型参数;类型参数不能为空,多个类型参数用逗号隔开。

一但声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。

例如,定义一个类来表示坐标,要求坐标的数据类型可以是 int、float、string,例如:

x = 10、y = 10
x = 12.88、y = 129.65
x = "东经180度"、y = "北纬210度"

这个时候就可以使用类模板,代码如下:

template<typename T1, typename T2>
class Point {
private:
    T1 _x;
    T2 _y;

public:
    // 构造函数
    Point(T1 x, T2 y);
    // setter 与 getter 函数
    void set_x(T1 x);
    T1 get_x();
    void set_y(T2 y);
    T2 get_y();
};

以上代码仅仅是类的声明,我们还需要在类外定义成员函数。类模板的成员函数,与普通成员函数定义方法一样,也可以放在类模板的外部定义,类模板的成员函数其实都是函数模板,其定义形式如下:

template<typename T1, typename T2, ...>
返回值类型 类模板名<T1, T2, ...>::函数名(形参列表) {
    函数体
}

对 Point 类中的成员函数进行定义,代码如下:

template<typename T1, typename T2>
Point<T1, T2>::Point(T1 x, T2 y) {
    this->_x = x;
    this->_y = y;
}

template<typename T1, typename T2>
void Point<T1, T2>::set_x(T1 x) {
    this->_x = x;
}

template<typename T1, typename T2>
T1 Point<T1, T2>::get_x() {
    return this->_x;
}

template<typename T1, typename T2>
void Point<T1, T2>::set_y(T2 y) {
    this->_y = y;
}

template<typename T1, typename T2>
T2 Point<T1, T2>::get_y() {
    return this->_y;
}

上面两段代码完成了 Point 类的定义,接下来就可以使用该类创建对象了。使用类模板创建对象时,需要指明具体的数据类型,代码如下:

#include <iostream>
using namespace std;

// 类模板
template<typename T1, typename T2>
class Point {
private:
    T1 _x;
    T2 _y;

public:
    // 构造函数
    Point(T1 x, T2 y);

    void set_x(T1 x);
    T1 get_x();
    void set_y(T2 y);
    T2 get_y();
};

template<typename T1, typename T2>
Point<T1, T2>::Point(T1 x, T2 y) {
    this->_x = x;
    this->_y = y;
}

template<typename T1, typename T2>
void Point<T1, T2>::set_x(T1 x) {
    this->_x = x;
}

template<typename T1, typename T2>
T1 Point<T1, T2>::get_x() {
    return this->_x;
}

template<typename T1, typename T2>
void Point<T1, T2>::set_y(T2 y) {
    this->_y = y;
}

template<typename T1, typename T2>
T2 Point<T1, T2>::get_y() {
    return this->_y;
}


int main() {
    Point<int, int> point_1(1, 1);
    cout << "point_1 的坐标为 (" << point_1.get_x() << ", " << point_1.get_y() << ")" << endl;

    Point point_2(2, 2.2);              // 编译器会根据给定的数据推算出数据类型,因此,尖括号中的内容可以省略
    cout << "point_2 的坐标为 (" << point_2.get_x() << ", " << point_2.get_y() << ")" << endl;

    Point<int, string> point_3(3, "北纬 120°");
    cout << "point_3 的坐标为 (" << point_3.get_x() << ", " << point_3.get_y() << ")" << endl;

    return 0;
}