🔷 第十二章 模板
📄 一句话概括

类的成员里如果带着"类型",这个类也能做成模板。template <typename T> class Box { T value; ... }; 就是一张"类的模具":Box<int> 和 Box<string> 是编译器照它造出来的两个真类。必须刻进骨头的两条:① 类模板不是类——Box b; 是错的,必须写 Box<int> b;(尖括号不能省);② Box<int> 与 Box<double> 是两个毫不相干的类——连静态成员都各有一份(10.4.3 的账在这里结清)。

📝 本节和前面几节的关系

12.1 组的函数模板解决了"同一套逻辑换类型"。本节把同一个念头搬到类上——而这次要处理的新问题是:类有成员、有构造、有静态成员、能拆文件,所以麻烦比函数多。 往回收旧账:10.4.3 静态类成员(模板一实例化,静态成员也各一份)、10.1.3 类的实现(类模板不能拆 .h / .cpp,理由同 12.1.1)、10.2 构造函数(模板类的构造要在初始化列表里把 T 的值传进成员)。

一、从函数模板到类模板

先看痛苦在哪里。假设要写"只能装一个东西的盒子":

class IntBox    { int    value; public: IntBox(int v)    : value(v) {} int    get() { return value; } };
class DoubleBox { double value; public: DoubleBox(double v) : value(v) {} double get() { return value; } };
class StringBox { string value; public: StringBox(string v) : value(v) {} string get() { return value; } };

除了类型名,一个字都不差——和 12.1 的 bigger 四胞胎一模一样。

改成类模板:

template <typename T>
class Box {
private:
    T value;                       // ★ 成员的类型,就是 T
public:
    Box(T v) : value(v) { }
    T get() const { return value; }
    void set(T v) { value = v; }
};

用的时候,必须在尖括号里说清 T 是什么:

Box<int>    a(3);
Box<string> b("hi");

cout << a.get() << endl;      // 3
cout << b.get() << endl;      // hi

编译器在这里干的事,和函数模板一样:照模具造出两个真类——Box<int> 和 Box<string>。

术语:尖括号里写的东西(int、string、5、double)叫模板实参;尖括号外面的名字(Box)叫模板名。Box<int> 整体才是一个类型名。

二、四条必须记住的性质

性质 1:类模板不是类型,不能直接用来定义对象

Box  a;             // ✗ 编译错:class template argument required
Box<int> a;         // ✓
Box<>  a;           // ✓ 只在"每个模板参数都有默认值"时才能这么写(12.2.3)

这与函数模板的感觉不同:函数模板调用时可以不写 <...>(编译器从参数推),类模板不行——因为没有"参数"可推,T 必须由你明说。

性质 2:Box<int> 与 Box<double> 是两个完全独立的类

这是最能看出"模板 = 造类机器"的一条:

对比项 说明
静态成员 各有一份! Box<int>::count 和 Box<double>::count 是两个不同的变量(10.4.3 的知识在这里升级)
互相赋值 Box<int> a; Box<double> b; a = b; 编译错(两个不同类的对象)
函数重载 void f(Box<int>) 和 void f(Box<double>) 是两个不同的重载
体积 可以完全不同(Box<char> 和 Box<double> 的 sizeof 不一样)
共同点 它们长得像(都从同一张图纸来),但在类型系统里毫无关系——既不是继承,也不是"同一个类的两个实例"

一句话:继承是"父子关系",模板实例化是"同一张图纸盖的两栋房子"——两栋房子之间没有关系。

性质 3:成员函数"用到才实例化"

编译器不会一上来就把 Box<int> 的所有成员函数都造出来,而是谁被调用,才造谁。

好处:只用 get() 时,set() 写错了也不影响编译(编译更快)。

坏处(很坑):一个从没被调用的成员函数,里面即使有明显错误也不会被发现——直到有人第一次调用它。

template <typename T>
class Box {
    T value;
public:
    Box(T v) : value(v) { }
    T get() const { return value; }
    void broken() { value.noSuchFunction(); }      // ★ 看起来要炸,但没人调用 → 编译通过
};

所以:模板代码"编过了"不等于"没问题"——每一处使用都要真正跑一遍。

性质 4:类模板(含所有成员函数定义)要整个放在头文件里

和 12.1.1 的函数模板同一个理由:编译器实例化时必须看到定义。

规则:

普通类:.h 放"长什么样"、.cpp 放"怎么干"(10.1.3)。 类模板:全部写在 .h 里(类定义 + 全部成员函数定义)。

这也是为什么 <vector> 的头文件打开一看全是实现代码。

三、本章路线图

节 讲什么
12.2.1 定义与声明:语法拆解、"类外定义成员函数"三处不能漏、类模板为什么不能拆文件
12.2.2 简单类模板:一个完整能用的 Pair<T1, T2>(两个模板参数)
12.2.3 默认模板参数:template <class T = int>——语法与三条铁律
12.2.4 为具体类型的参数提供默认值:为什么把"最常用的类型"设为默认(库设计惯例)
12.2.5 有界数组模板:把 int size 当模板参数 + 带边界检查的 operator[]——本章最像样的作品

四、动手 Demo:最小的类模板

#include <iostream>
#include <string>
using namespace std;

template <typename T>
class Box {
private:
    T value;
public:
    Box(T v) : value(v) { }
    T    get() const { return value; }
    void set(T v)   { value = v; }
    void show() const { cout << "盒子里的东西:" << value << endl; }
};

int main() {
    Box<int>    a(3);
    Box<double> b(1.5);
    Box<string> c("hello");

    a.show();
    b.show();
    c.show();

    c.set("world");            // ★ set 的参数也是 T
    c.show();

    // Box  d(1);              // ★ 取消注释 → 编译错(类模板不是类型)
    // a = c;                 // ★ 取消注释 → 编译错(Box<int> 和 Box<string> 是两个类)
    return 0;
}

看到什么算成功:

盒子里的东西:3
盒子里的东西:1.5
盒子里的东西:hello
盒子里的东西:world

读结果:

现象 说明
三次 show() 都正常 编译器造出了 Box<int>、Box<double>、Box<string> 三个真类
c.set("world") 能编过 在 Box<string> 里,set 的参数类型已经是 string——模板参数一确定,它就是普通类
两行注释会报错 ① Box d(1); → 类模板不给 <...> 不能定义对象;② a = c; → Box<int> 和 Box<string> 之间没有任何关系

30 秒实验:

  1. 把 c.set("world") 改成 c.set(123)。
  2. 编译 → 报错:no matching function for call to 'Box<std::string>::set(int)'。
  3. 看这个报错里的名字——Box<std::string>::set。编译器已经把它当成一个叫 Box<std::string> 的普通类在处理了。
  4. 这说明:模板不是"运行时什么都能塞的万能盒子"——一旦 T 定成 string,它就是严格的 string 盒子。

五、常见误区

  1. "Box 就是一个通用的类"——不是。它是模具,不是类型,不能定义对象。必须写 Box<int>。
  2. "Box<int> 和 Box<double> 是同一个类的两种用法"——是两个独立的类。静态成员各一份、不能互相赋值、不能互相转换。
  3. "类模板能像普通类那样分 .h / .cpp"——不能,会报 undefined reference。全部写进头文件。
  4. "模板参数只能是类型"——还能是值(非类型模板参数),像 12.1.2 实例01 的 int N。12.2.4 / 12.2.5 会用它写"数组长度"。
  5. "编过了就没问题"——没被调用的成员函数,编译器不检查。这既是编译快的来源,也是"模板代码更容易藏 bug"的原因。
自测三问(点开对答案) 1. `Box` 和 `Box` 是什么关系?——**两个完全独立的类**,由同一张模具(`template class Box`)分别造出来。它们**互相不能赋值/转换**,**静态成员各有一份**。区别在于:继承是"父子",模板实例化只是"同一个模具的两个产品"。 2. 类模板为什么不能像普通类那样把实现放在 `.cpp` 里?——因为**编译器在实例化 `Box` 时必须看到成员函数的定义**才能生成代码。定义在别的 `.cpp` 里 → 本文件看不到 → **链接期 `undefined reference`**。(这也是 12.1.1 那条铁律的类版本。) 3. 为什么说"模板代码编过了不代表没问题"?——因为**成员函数是"用到才实例化"的**:**从没被调用的成员函数,编译器根本不会去检查里面的错误**。所以模板(尤其是库)必须**把每种用法都跑一遍**。

相关笔记