C++ 类模板

在 C++ 中,“类模板(class template)” 是一种通用的类定义。当多个类的成员变量和成员函数逻辑相同、仅仅是内部使用的数据类型不同时,我们就可以使用类模板把通用的类结构抽取出来,省去重复编写相似类的麻烦。

类模板的定义与使用

假设我们要编写一个用来保存单个数据的盒子类。如果不使用类模板,既想保存整数 int,又想保存字符串 std::string,就不得不分别定义 IntBox 和 StringBox 两个类:

// 专门保存 int 的类
class IntBox {
private:
    int value;

public:
    IntBox(int v) {
        value = v;
    }

    int getValue() const {
        return value;
    }
};

// 专门保存 std::string 的类
class StringBox {
private:
    std::string value;

public:
    StringBox(const std::string& v) {
        value = v;
    }

    std::string getValue() const {
        return value;
    }
};

对比上面两个类可以发现,除了把 int 换成了 std::string 之外,整个类的结构完全一样。既然变化的只有数据类型,我们就可以在 class 定义的正上方加上 template <typename T>,合并成一个通用的类模板。

语法:

// 定义类模板
template <typename T>
class 类名 {
    // 成员变量与成员函数
};

// 创建类模板对象
类名<具体类型> 对象名(构造实参);

说明:

定义类模板时,我们将 template <typename T> 放在 class 定义的正上方,用来声明类型参数 T。在整个类模板的花括号内部,T 可以直接作为成员变量的类型,也可以作为构造函数和普通成员函数的参数或返回值类型。

需要注意的是,类模板本身只是一份通用的 “类图纸”,还不是一个具体的类。创建对象时,我们需要在类名后面的尖括号 “<>” 中填入具体的数据类型,比如写成 Box<int> 或 Box<std::string>,编译器就会根据模板生成对应的类并创建对象。

示例 1:定义并使用类模板

#include <iostream>
#include <string>

template <typename T>
class Box {
private:
    T value;

public:
    Box(const T& v) {
        value = v;
    }

    void setValue(const T& v) {
        value = v;
    }

    T getValue() const {
        return value;
    }
};

int main() {
    Box<int> scoreBox(95);
    std::cout << scoreBox.getValue() << '\n';

    scoreBox.setValue(100);
    std::cout << scoreBox.getValue() << '\n';

    Box<std::string> nameBox("绿叶网");
    std::cout << nameBox.getValue() << '\n';
    return 0;
}

运行结果如下。

95
100
绿叶网

分析:

Box<int> scoreBox(95); 表示将类型参数 T 指定为 int,并创建对象 scoreBox。编译器在编译阶段看到这行代码时,会自动把类模板里的 T 全部替换成 int,生成一个类似前面 IntBox 的具体类。

同理,写下 Box<std::string> nameBox("绿叶网"); 时,编译器又会把 T 替换成 std::string,生成另一个具体的类。

另外,由于 T 有可能被指定为 std::string 这类占内存较大的类型,这里我们将形参写成了 const T& 形式的常量引用。这样既能直接接收字面量,又能在传入大对象时省去多余的拷贝开销。

提示 从 C++17 标准开始,如果构造函数的实参能明确推导出 T 的类型,创建对象时也可以省略尖括号,直接写成 Box scoreBox(95);。不过在实际开发中,我们依然推荐显式写出 Box<int>,这样代码的可读性更好。

类模板与具体类的关系

初学类模板时,为了避免把概念搞混,小伙伴们需要先把 “类模板”、“具体的类” 和 “对象” 这三层关系区分清楚,如下表所示。

类模板与对象的关系
代码写法 所属层级 说明
Box 类模板名称 通用的类定义图纸,本身还不是具体的类
Box<int> / Box<std::string> 具体的类 填入类型实参后生成的真实类,地位等同于普通类名
scoreBox / nameBox 具体的对象 使用具体的类创建出来的对象实例

虽然 Box<int> 和 Box<double> 都是由同一个 Box 类模板生成的,但只要尖括号 “<>” 里填入的类型实参不同,编译器生成的就是两个完全独立的类。只有当类型实参完全相同时,它们才属于同一种类类型。下面来看一个例子。

示例 2:不同类型实参生成独立的类

#include <iostream>

template <typename T>
class Box {
private:
    T value;

public:
    Box(const T& v) {
        value = v;
    }

    T getValue() const {
        return value;
    }
};

int main() {
    Box<int> a(10);
    Box<int> b(20);
    b = a;    // 类型相同可赋值

    Box<double> c(3.14);

    std::cout << b.getValue() << '\n';
    std::cout << c.getValue() << '\n';
    return 0;
}

运行结果如下。

10
3.14

分析:

在这个例子中,a 和 b 的类型都是 Box<int>,属于同一个具体的类,因此执行 b = a; 可以直接把 a 的值赋给 b。

而 c 的类型是 Box<double>。哪怕 int 和 double 本身可以互相转换,但 Box<int> 和 Box<double> 已经是两个互不相干的类了,彼此不能直接赋值。下面这种写法就是错误的:

Box<int> a(10);
Box<double> c(3.14);
c = a;    // 编译报错

类模板对象作为函数参数

正因为 Box 只是类模板的名称,Box<具体类型> 才是真正的类类型,所以当我们编写函数来接收类模板对象时,形参里不能只写一个光秃秃的 Box。

在实际开发中,将类模板对象作为函数参数主要有以下 2 种方式:

  • 指定具体的类类型:如果函数只接收某一种固定类型的对象,形参直接写明具体类型即可,比如 const Box<int>&。
  • 将函数定义为函数模板:如果希望函数能接收任意类型的对象,可以把该函数定义为函数模板,将形参声明为 const Box<T>&。

示例 3:类模板对象作为函数参数

#include <iostream>
#include <string>

template <typename T>
class Box {
private:
    T value;

public:
    Box(const T& v) {
        value = v;
    }

    T getValue() const {
        return value;
    }
};

// 只接收 Box<int> 对象
void printIntBox(const Box<int>& b) {
    std::cout << "整数盒子:" << b.getValue() << '\n';
}

// 可接收任意类型的 Box<T> 对象
template <typename T>
void printAnyBox(const Box<T>& b) {
    std::cout << "通用盒子:" << b.getValue() << '\n';
}

int main() {
    Box<int> b1(100);
    Box<std::string> b2("lvye");

    printIntBox(b1);
    printAnyBox(b1);
    printAnyBox(b2);
    return 0;
}

运行结果如下。

整数盒子:100
通用盒子:100
通用盒子:lvye

分析:

printIntBox() 的形参写成了 const Box<int>& b,因此只能接收 Box<int> 类型的对象 b1。如果漏掉尖括号写成 void printIntBox(const Box& b),编译器会因为 Box 不是一个完整的类型名而报错。

下方的 printAnyBox() 本身是一个函数模板,形参写成了 const Box<T>& b。传入 b1 时,编译器会自动推导出 T 是 int。传入 b2 时,编译器又会推导出 T 是 std::string,这样就能同时接收不同类型的 Box 对象了。

类模板成员函数的类外定义

在前面的例子中,我们把构造函数和普通成员函数都直接写在了类模板内部。如果某个成员函数的代码比较长,我们想在类模板内部只保留函数声明,把具体实现移到类模板外部,写法上会比普通类多出两个要求。

语法:

template <typename T>
返回类型 类名<T>::成员函数名(参数列表) {
    // 函数体
}

说明:

为什么普通类在外部定义成员函数只需要写 Box::getValue(),而类模板在外部定义时却必须写成 Box<T>::getValue() 呢?

结合前面的三层关系就很好理解了:Box 只是类模板图纸的名称,只有带上模板参数写成 Box<T>,才代表这个类模板对应的类类型。因此在类模板外部定义成员函数时,我们需要同时满足以下 2 点:

  • 加上模板声明头:每一个类外定义的成员函数正上方,都必须重新写一行 template <typename T>。
  • 类名后带上模板参数:作用域限定符 :: 前面的类名必须写成 类名<T>,不能只写 类名。

初学者在编写类外定义时,最容易漏掉类名后面的 <T>,我们可以对比一下正确与错误写法:

// 正确方式
template <typename T>
T Box<T>::getValue() const {
    return value;
}

// 错误方式
template <typename T>
T Box::getValue() const {
    return value;
}

下面我们通过一个完整的例子,来看看类模板成员函数在类外是如何定义的。

示例 4:类外定义成员函数

#include <iostream>
#include <string>

template <typename T>
class Box {
private:
    T value;

public:
    Box(const T& v);
    T getValue() const;
};

// 类外定义构造函数
template <typename T>
Box<T>::Box(const T& v) {
    value = v;
}

// 类外定义普通成员函数
template <typename T>
T Box<T>::getValue() const {
    return value;
}

int main() {
    Box<int> idBox(1001);
    Box<std::string> userBox("Jack");

    std::cout << idBox.getValue() << '\n';
    std::cout << userBox.getValue() << '\n';
    return 0;
}

运行结果如下。

1001
Jack

分析:

在这个例子中,我们在类模板外部完成了构造函数和普通成员函数的定义。定义构造函数 Box<T>::Box(const T& v) 时,:: 前面的 Box<T> 代表所属的类类型,必须带上 <T>。而 :: 后面的 Box 是构造函数的名字,不需要再加 <T>。

定义普通成员函数 T Box<T>::getValue() const 时,开头的第 1 个 T 表示返回值类型,中间 Box<T>:: 里的第 2 个 <T> 表示该函数属于 Box<T> 类模板。另外,由于类内声明时末尾带有 const,类外定义时末尾的 const 也必须保持一致。

在实际开发中,如果类模板的成员函数比较简短,我们更推荐像示例 1 那样直接写在类模板内部,代码既紧凑又不容易写错语法。

模板的头文件编写规范

等后面学习多文件编程时我们会看到,大型项目通常会把普通类的声明放在 .h 头文件中,把成员函数的实现拆分到 .cpp 源文件中。但对于函数模板和类模板,我们不要把声明和实现拆成两个文件,而应该统一写在同一个 .h 头文件中。

这是因为模板只是一份 “代码图纸”,编译器在调用处把 T 替换成具体类型生成代码时,必须当场看到完整的函数体,否则就会报错。当然了,现阶段咱们都是在单个 .cpp 源文件里编写和运行代码,暂时不会遇到这个问题,大家先留个印象即可。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号