类的成员里如果带着"类型",这个类也能做成模板。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 秒实验:
- 把
c.set("world")改成c.set(123)。 - 编译 → 报错:
no matching function for call to 'Box<std::string>::set(int)'。 - 看这个报错里的名字——
Box<std::string>::set。编译器已经把它当成一个叫Box<std::string>的普通类在处理了。 - 这说明:模板不是"运行时什么都能塞的万能盒子"——一旦
T定成string,它就是严格的string盒子。
五、常见误区
- "
Box就是一个通用的类"——不是。它是模具,不是类型,不能定义对象。必须写Box<int>。 - "
Box<int>和Box<double>是同一个类的两种用法"——是两个独立的类。静态成员各一份、不能互相赋值、不能互相转换。 - "类模板能像普通类那样分
.h/.cpp"——不能,会报undefined reference。全部写进头文件。 - "模板参数只能是类型"——还能是值(非类型模板参数),像 12.1.2 实例01 的
int N。12.2.4 / 12.2.5 会用它写"数组长度"。 - "编过了就没问题"——没被调用的成员函数,编译器不检查。这既是编译快的来源,也是"模板代码更容易藏 bug"的原因。
自测三问(点开对答案)
1. `Box- ☐ 12.2.1 类模板的定义与声明(下一节)