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

函数模板的定义只有一行是新的:template <typename T>——它声明"这段代码里的 T 是个类型"。语法上要注意三件事:① typename 和 class 完全等价;② T 的作用域只到紧跟的那个函数为止;③ 模板参数不会替你做类型转换(bigger(3, 4.5) 会报 no matching function)。工程上有一条铁规矩:模板的定义必须放在"看得见"的地方(头文件里),否则链接时会报 undefined reference。

📝 本节和前面几节的关系

12.1 讲了"模板是什么、为什么需要",本节把那一行 template <typename T> 拆开看清楚,并顺着它解释三个反复会踩的坑。它和 10.1.3 类的实现(.h 放声明、.cpp 放实现)正好形成一个对照:普通函数可以拆开写,模板不行——原因就在"实例化"这件事上。

一、语法:拆开看这四个部分

template <typename T>          // ① 模板头
T bigger(T a, T b) {           // ② 返回类型:T  ③ 参数类型:T
    return a > b ? a : b;      // ④ 函数体:只见 T,不见具体类型
}
部位 说明
template 关键字,意思是"接下来这个定义带参数"——只是这参数是类型
< ... > 里面叫模板参数表(和函数的参数表意思一样,只是装的是"类型")
typename T 声明一个类型参数,名字叫 T。typename 换成 class 完全等价(老书、老代码都用 class,因为它比 C++ 还老)
函数体 里面只认识 T——所以这段代码必须"在每一种用到的类型上都讲得通"

几个可以随便写、不影响正确性的地方:

一处不能随便写的:T 的作用域只到紧跟的那个定义为止。

template <typename T>
T bigger(T a, T b) { return a > b ? a : b; }

T other;                  // ✗ 错:这里没有 T 了(T 只活在上面那个函数里)

所以每写一个函数模板,都要重新来一行 template <...>——这一点在 12.2.1(类模板的成员函数在类外定义)会变成一个高频错误。

二、模板不是函数,是一张图纸

这是理解模板最要紧的一句话。

bigger(3, 7)

编译器看到这行,先做类型推导:两个参数都是 int → T = int → 然后照着图纸造出一个真函数:

int bigger(int a, int b) { return a > b ? a : b; }   // 编译器自动生成的

这个"照图纸造函数"的动作叫 实例化(instantiation),造出来的东西叫模板的实例。

三条由此推出的性质:

性质 说明
不用不生成 你没写过 bigger(long, long) 的调用,编译器就不会生成它——图纸放在那儿不花钱
每种类型各一份 bigger<int> 和 bigger<double> 是两个毫不相干的函数(不是一个函数的两副面孔)
两种实例化方式 隐式:让编译器从参数推(bigger(3, 7));显式:自己写出来(bigger<int>(3, 7))

"显式指定"非常有用,看下一节。

三、类型推导的三条规矩

规矩 1:T 必须能被唯一确定

bigger(3, 4.5);      // 一个 int、一个 double —— T 到底该是哪个?

编译错:

error: no matching function for call to 'bigger(int, double)'
note:  candidate: template<class T> T bigger(T, T)
note:  template argument deduction/substitution failed:
note:    deduced conflicting types for parameter 'T' ('int' and 'double')

注意这条报错:它不是说"类型不匹配",而是说"T 推出来有两个不同的答案,我推不下去"。

为什么编译器不"顺手把 int 转成 double"? 因为模板的类型推导发生在"选函数"阶段,此时还没有到"类型转换"阶段。编译器只做"看参数、定 T",不做"改参数"。

解药有两种:

bigger<double>(3, 4.5);       // ① 显式告诉它 T = double → 3 会被转成 3.0
bigger((double)3, 4.5);       // ② 自己先把类型统一

这就是"显式指定模板实参"最常用的场合:当编译器推不出来、或推出来的不是你要的。

心法:普通函数重载是"先转类型、再选函数";模板是"先定类型、不接受就报错"。 顺序反了,行为就反了。

规矩 2:传值会复制,传 T & 才不复制

template <typename T>
T bigger(T a, T b) { ... }        // ★ 按值:a、b 都是"拷贝品"

template <typename T>
const T &biggerRef(const T &a, const T &b) { return a > b ? a : b; }   // ★ 按引用:不复制

什么时候该用 const T &:

类型 建议写法 原因
int、double、char T(按值) 复制几个字节,比"解引用"还快
string、大结构体、自定义类 const T &(按常量引用) 避免整份复制(10.2.2 的复制构造函数会跑)

⚠️ 但 biggerRef 有个陷阱:它返回的引用指向传入的某个参数——只要参数还活着就没事(上面这样用是安全的,因为参数来自调用者)。

真正危险的是这种:

const T &bad(const T &a, const T &b) {
    T tmp = a;         // 局部变量
    return tmp;        // ✗ 错:返回局部变量的引用(悬空引用)
}

11.2.3 的 30 秒实验就是这个坑(Complex &operator+ 返回局部变量)——规则一句话:不要返回局部变量的引用。

规矩 3:T 一旦定下来,就是严格类型

bigger<int>(3, 4.5)      // T = int;4.5 被转成 4,结果是 4
bigger<double>(3.5, 4)   // T = double;4 被转成 4.0,结果是 4.0

显式指定之后,"类型转换"这条正常规则又回来了——但缩窄转换(double → int)会悄悄丢小数,编译器一般只给个 warning。别指望模板替你看着这件事。

四、两条工程铁律

铁律 1:模板的定义必须"看得见"(放头文件)

回想 10.1.3:普通类的做法是 .h 放声明、.cpp 放实现,编译各自进行、链接时拼起来。

模板不能这么干。

// math.h
template <typename T>
T bigger(T a, T b);          // ★ 只有声明

// math.cpp
template <typename T>
T bigger(T a, T b) { return a > b ? a : b; }     // ★ 实现在另一个"编译单元"里

// main.cpp
#include "math.h"
int main() { cout << bigger(3, 7); }

编译 main.cpp 时:编译器看到 bigger(3, 7),要造出 bigger(int,int)——可它手上只有声明,没有图纸(实现在 math.cpp 里,它看不见)。于是它只好留一个"待填的坑",希望链接器能找到。

链接时报:

undefined reference to `int bigger<int>(int, int)'

原因一句话:模板实例化需要"定义",而每个 .cpp 是分开编译的——别的 .cpp 里的模板定义,本文件根本看不到。

所以规矩是:

模板(以及类模板的所有成员函数定义),一律写在头文件里。 这是"头文件不放定义"这条规则的唯一例外——10.4.2 的内联成员函数也出于同一个理由破例(那里的原话是"定义必须进头文件")。

顺带解释一件事:为什么标准库的 <vector> 打开一看全是代码(不是"只有声明")?就是这个原因。

铁律 2:模板的报错"生在深处",要找最后一条 note

模板的报错又长又吓人。读法:

看哪里 找什么
第一行 error: 说"哪个调用失败了"(往往是用法的问题)
note: candidate: 列出它试过哪些版本——通常能看出"我给的参数和哪个模板对不上"
最后一条 note:(最重要) 往往直接说出根因,例如 deduced conflicting types for parameter 'T' ('int' and 'double')

经验:从最后一条 note 往回读,比从第一行往下读快得多。

五、动手 Demo:swap + 显式指定类型

#include <iostream>
using namespace std;

// ① 通用交换:传引用,才改得动原件(7.8.2)
template <typename T>
void mySwap(T &a, T &b) {
    T tmp = a;
    a = b;
    b = tmp;
}

// ② 取较大者
template <typename T>
T maxOf(T a, T b) {
    return a > b ? a : b;
}

int main() {
    int    i = 1,     j = 2;
    double x = 1.5,   y = 2.5;
    char   c1 = 'a',  c2 = 'b';

    mySwap(i, j);
    mySwap(x, y);
    mySwap(c1, c2);

    cout << i  << " " << j  << endl;
    cout << x  << " " << y  << endl;
    cout << c1 << " " << c2 << endl;

    // ★ 显式指定 T = double:3 会先被转成 3.0,再比较
    cout << maxOf<double>(3, 4.5) << endl;

    // cout << maxOf(3, 4.5) << endl;   // ★ 取消注释 → 编译错(T 推不出来)
    return 0;
}

看到什么算成功:

2 1
2.5 1.5
b a
4.5

读结果:

现象 说明
三种类型都交换成功 一份 mySwap 实例化出三个函数;T & 让它改得动原件
maxOf<double>(3, 4.5) 得 4.5 显式指定后,3 被转成 3.0,比较正常进行
注释掉的那行会报错 deduced conflicting types for parameter 'T' ('int' and 'double')

30 秒实验(把这个报错看一遍,以后不慌):

  1. 把 cout << maxOf(3, 4.5) 那行的注释去掉,编译。
  2. 你会看到十几行报错。别从上往下读,直接跳到最后几行 note:。
  3. 找到 deduced conflicting types for parameter 'T' ('int' and 'double') ——这就是根因。
  4. 改成 maxOf<double>(3, 4.5),编译通过。
  5. 记住这个手感:模板报错的答案,通常在最后一条 note 里。

六、常见误区

  1. "typename 和 class 有区别"——在模板参数里完全等价。template <class T> 和 template <typename T> 一模一样。(typename 还有一个"告诉编译器这是类型名"的用法,那是更高级的场合,以后遇到再说。)
  2. "模板会自动帮我做类型转换"——不会。bigger(3, 4.5) 直接报错。要么显式指定 bigger<double>(...),要么自己把参数类型统一。
  3. "模板可以像普通函数那样 .h 声明、.cpp 实现"——不行,会报 undefined reference。模板的定义(含类模板成员函数的定义)必须写在头文件里。
  4. "模板参数名必须是 T"——随便叫。T 只是"Type"的习惯缩写,多人协作时叫 T 最不容易让人误会。
  5. "T 声明一次能用好几个函数"——不能。T 的作用域只到紧跟的那个定义为止,每个模板函数都要重新写 template <...>。
  6. "模板报错太长,说明很难"——不是难,是啰嗦。从最后一条 note: 往回读,往往一眼看到根因。
自测三问(点开对答案) 1. `template ` 和 `template ` 有区别吗?`T` 能换成别的名字吗?——**没有区别**,完全等价;**名字随便取**(`T` 只是习惯)。但 **`T` 的作用域只到紧跟的那个定义**,下一个模板要重新声明。 2. `maxOf(3, 4.5)` 为什么编译不过?怎么改?——**因为 `T` 被推出两个互相矛盾的答案**(`int` 和 `double`),报 `deduced conflicting types`。改法:**显式指定 `maxOf(3, 4.5)`**,或**自己把参数类型统一**。注意:模板推导阶段**不做类型转换**。 3. 为什么模板的定义必须写在头文件里?——因为**编译器在"实例化"时必须看到模板的定义**才能生成代码。模板定义放在别的 `.cpp` 里时,本文件只看到声明 → 生成不了实例 → **链接期报 `undefined reference to 'int bigger(int, int)'`**。

相关笔记